use crate::errors::{CoreError, CoreResult};
use crate::time::SharedClock;
use crate::tokens::{verify_access_token, AccessTokenClaims};
use chrono::DateTime;
use sui_id_shared::ids::ClientId;
use sui_id_store::repos::{
clients, refresh_tokens, revoked_access_tokens as deny_list, users,
};
use sui_id_store::Database;
#[derive(Debug, Clone)]
pub struct IntrospectionResponse {
pub active: bool,
pub scope: Option<String>,
pub client_id: Option<String>,
pub username: Option<String>,
pub token_type: Option<&'static str>,
pub exp: Option<i64>,
pub iat: Option<i64>,
pub sub: Option<String>,
pub aud: Option<String>,
pub iss: Option<String>,
pub kind: Option<&'static str>,
}
impl IntrospectionResponse {
pub fn inactive() -> Self {
Self {
active: false,
scope: None,
client_id: None,
username: None,
token_type: None,
exp: None,
iat: None,
sub: None,
aud: None,
iss: None,
kind: None,
}
}
}
pub async fn introspect(
db: &Database,
clock: &SharedClock,
authenticating_client: ClientId,
token: &str,
hint: Option<&str>,
) -> CoreResult<IntrospectionResponse> {
let order: &[&str] = match hint {
Some("refresh_token") => &["refresh_token", "access_token"],
_ => &["access_token", "refresh_token"],
};
for kind in order {
let resp = match *kind {
"access_token" => try_introspect_access(db, clock, authenticating_client, token).await,
"refresh_token" => try_introspect_refresh(db, authenticating_client, token).await,
_ => Ok(IntrospectionResponse::inactive()),
}?;
if resp.active {
return Ok(resp);
}
}
Ok(IntrospectionResponse::inactive())
}
async fn try_introspect_access(
db: &Database,
clock: &SharedClock,
authenticating_client: ClientId,
token: &str,
) -> CoreResult<IntrospectionResponse> {
let claims = match verify_access_token(db, clock, token).await {
Ok(c) => c,
Err(_) => return Ok(IntrospectionResponse::inactive()),
};
let aud_id = claims
.aud
.parse::<ClientId>()
.ok()
.ok_or(())
.map_err(|_| CoreError::Internal)?;
if aud_id != authenticating_client {
return Ok(IntrospectionResponse::inactive());
}
if deny_list::is_revoked(db, &claims.jti).await? {
return Ok(IntrospectionResponse::inactive());
}
Ok(active_from_access(&claims, db).await)
}
async fn active_from_access(claims: &AccessTokenClaims, db: &Database) -> IntrospectionResponse {
let username = {
let uid_opt = claims.sub.parse::<sui_id_shared::ids::UserId>().ok();
if let Some(uid) = uid_opt {
users::get(db, uid).await.ok().map(|u| u.username)
} else {
None
}
};
IntrospectionResponse {
active: true,
scope: Some(claims.scope.clone()),
client_id: Some(claims.aud.clone()),
username,
token_type: Some("Bearer"),
exp: Some(claims.exp),
iat: Some(claims.iat),
sub: Some(claims.sub.clone()),
aud: Some(claims.aud.clone()),
iss: Some(claims.iss.clone()),
kind: Some("access_token"),
}
}
async fn try_introspect_refresh(
db: &Database,
authenticating_client: ClientId,
token: &str,
) -> CoreResult<IntrospectionResponse> {
let row = match refresh_tokens::find_active(db, token).await {
Ok(r) => r,
Err(_) => return Ok(IntrospectionResponse::inactive()),
};
if row.client_id != authenticating_client {
return Ok(IntrospectionResponse::inactive());
}
let username = users::get(db, row.user_id).await.ok().map(|u| u.username);
Ok(IntrospectionResponse {
active: true,
scope: Some(row.scope),
client_id: Some(row.client_id.to_string()),
username,
token_type: Some("Bearer"),
exp: Some(row.expires_at.timestamp()),
iat: Some(row.created_at.timestamp()),
sub: Some(row.user_id.to_string()),
aud: Some(row.client_id.to_string()),
iss: None,
kind: Some("refresh_token"),
})
}
pub async fn revoke(
db: &Database,
clock: &SharedClock,
authenticating_client: ClientId,
token: &str,
hint: Option<&str>,
) -> CoreResult<()> {
let order: &[&str] = match hint {
Some("refresh_token") => &["refresh_token", "access_token"],
_ => &["access_token", "refresh_token"],
};
for kind in order {
let revoked = match *kind {
"access_token" => try_revoke_access(db, clock, authenticating_client, token).await?,
"refresh_token" => try_revoke_refresh(db, authenticating_client, token).await?,
_ => false,
};
if revoked {
return Ok(());
}
}
Ok(())
}
async fn try_revoke_access(
db: &Database,
clock: &SharedClock,
authenticating_client: ClientId,
token: &str,
) -> CoreResult<bool> {
let claims = match verify_access_token(db, clock, token).await {
Ok(c) => c,
Err(_) => return Ok(false),
};
let aud_id = match claims.aud.parse::<ClientId>() {
Ok(c) => c,
Err(_) => return Ok(false),
};
if aud_id != authenticating_client {
return Ok(false);
}
let exp = DateTime::from_timestamp(claims.exp, 0).ok_or(CoreError::Internal)?;
let user_id = claims.sub.parse::<sui_id_shared::ids::UserId>().ok();
deny_list::insert(
db,
&sui_id_store::repos::revoked_access_tokens::RevokedAccessTokenRow {
jti: claims.jti.clone(),
revoked_at: clock.now(),
exp,
revoked_by_user: user_id,
revoked_by_client: Some(authenticating_client),
},
).await?;
Ok(true)
}
async fn try_revoke_refresh(
db: &Database,
authenticating_client: ClientId,
token: &str,
) -> CoreResult<bool> {
let row = match refresh_tokens::find_active(db, token).await {
Ok(r) => r,
Err(_) => return Ok(false),
};
if row.client_id != authenticating_client {
return Ok(false);
}
refresh_tokens::revoke(db, &row.id).await?;
Ok(true)
}
pub async fn authenticate_client(
db: &Database,
client_id: &str,
client_secret: &str,
) -> CoreResult<ClientId> {
let id = client_id
.parse::<ClientId>()
.map_err(|_| CoreError::Unauthenticated)?;
let row = clients::get(db, id).await.map_err(|e| match e {
sui_id_store::StoreError::NotFound => CoreError::Unauthenticated,
other => CoreError::from(other),
})?;
if !row.confidential {
return Err(CoreError::Unauthenticated);
}
if row.is_disabled || row.is_deleted {
return Err(CoreError::Unauthenticated);
}
let hash = row
.secret_hash
.as_deref()
.ok_or(CoreError::Unauthenticated)?;
crate::password::verify_password(client_secret, hash)
.map_err(|_| CoreError::Unauthenticated)?;
Ok(id)
}