use super::OAuthProviderPlugin;
use alibi_core::session::SessionRequest;
use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use serde_json::{Value, json};
use sha2::{Digest as _, Sha256, Sha384, Sha512};
fn reserved_claim(name: &str) -> bool {
matches!(
name,
"iss"
| "sub"
| "aud"
| "exp"
| "nbf"
| "iat"
| "jti"
| "auth_time"
| "nonce"
| "acr"
| "amr"
| "azp"
| "sid"
| "at_hash"
| "c_hash"
| "s_hash"
)
}
impl OAuthProviderPlugin {
pub(super) async fn issue_id_token<S: AuthSchema>(
&self,
params: &super::OAuthTokenIssueParams,
grant_type: &str,
proof: &Value,
access_token: &str,
req: &impl SessionRequest,
ctx: &AuthContext<S>,
) -> AuthResult<Result<Option<String>, alibi_core::endpoint::EndpointResponse>> {
let client = ¶ms.client;
let issued = chrono::Utc::now().timestamp();
let subject = self.resolve_subject(
proof
.get("userId")
.and_then(Value::as_str)
.ok_or_else(|| AuthError::internal("Missing ID token user"))?,
client,
)?;
let mut payload = if let Some(contributor) = &self.config.custom_id_token_claims {
contributor
.contribute(super::OAuthTokenClaimsInput {
context: super::extension_claims::callback_context(ctx, req),
options: &self.config,
params,
grant_type: Some(grant_type),
metadata: alibi_core::store::oauth_clients::metadata_to_columns(
client.metadata.clone(),
)?
.get("metadata")
.filter(|value| !value.is_null())
.cloned(),
})
.await?
.into_iter()
.filter(|(key, _)| !reserved_claim(key))
.collect()
} else {
serde_json::Map::new()
};
if self.config.disable_jwt_plugin
&& client.client_secret.as_ref().is_none_or(String::is_empty)
{
return Ok(Ok(None));
}
let jwt = if self.config.disable_jwt_plugin {
None
} else {
Some(
ctx.extensions
.get::<crate::jwt::JwtPlugin>()
.ok_or_else(|| AuthError::config("jwt_config"))?,
)
};
let mut options = crate::jwt::JwtSignOptions::default();
let resolved = if let Some(jwt) = jwt.as_ref() {
jwt.resolve_signing_key(&options, req.physical_request(), ctx)
.await?
} else {
None
};
let algorithm = if self.config.disable_jwt_plugin {
"HS256".into()
} else {
resolved
.as_ref()
.map(|key| key.algorithm.as_str().to_owned())
.or_else(|| {
ctx.extensions
.get::<crate::jwt::JwtDiscoveryOptions>()
.and_then(|options| options.signing_algorithms.first().cloned())
})
.ok_or_else(|| AuthError::config("JWT signing algorithm is missing"))?
};
let hash = match algorithm.as_str() {
"EdDSA" => Sha512::digest(access_token).to_vec(),
algorithm if algorithm.ends_with("384") => Sha384::digest(access_token).to_vec(),
algorithm if algorithm.ends_with("512") => Sha512::digest(access_token).to_vec(),
_ => Sha256::digest(access_token).to_vec(),
};
let access_hash = URL_SAFE_NO_PAD.encode(
hash.get(..hash.len() / 2)
.ok_or_else(|| AuthError::internal("Invalid OIDC token digest"))?,
);
let base = format!(
"{}{}",
ctx.config.base_url.trim_end_matches('/'),
ctx.config.base_path
);
let discovery = if self.config.disable_jwt_plugin {
None
} else {
ctx.extensions.get::<crate::jwt::JwtDiscoveryOptions>()
};
let issuer = discovery
.as_ref()
.and_then(|options| options.issuer.as_ref())
.unwrap_or(&base);
payload.extend(json!({"iss":issuer,"sub":subject,"aud":client.client_id,"iat":issued,"exp":super::issuance::expiration(issued,self.config.id_token_expires_in)?,"acr":"0","at_hash":access_hash}).as_object().cloned().ok_or_else(||AuthError::internal("Invalid OIDC claims"))?);
if let Some(time) = proof.get("authTime").and_then(Value::as_i64) {
_ = payload.insert("auth_time".into(), json!(time.div_euclid(1000)));
}
if let Some(nonce) = proof
.get("query")
.and_then(|query| query.get("nonce"))
.filter(|v| !v.is_null())
{
_ = payload.insert("nonce".into(), nonce.clone());
}
let include_sid = client.metadata.get("enable_end_session") == Some(&Value::Bool(true))
|| client
.metadata
.get("backchannel_logout_uri")
.and_then(Value::as_str)
.is_some_and(|value| !value.is_empty());
if include_sid && let Some(session) = proof.get("sessionId") {
_ = payload.insert("sid".into(), session.clone());
}
if let Some(claims) = proof.get("idTokenClaims").and_then(Value::as_object) {
for (key, value) in claims {
if !reserved_claim(key)
&& ![
"name",
"given_name",
"family_name",
"picture",
"email",
"email_verified",
]
.contains(&key.as_str())
&& !payload.contains_key(key)
{
_ = payload.insert(key.clone(), value.clone());
}
}
}
let token = if let Some(jwt) = jwt {
options.resolved_key = resolved.map(std::sync::Arc::new);
jwt.sign_jwt(payload, &options, req.physical_request(), ctx)
.await?
} else {
let stored = client
.client_secret
.as_deref()
.ok_or_else(|| AuthError::internal("Client secret is missing"))?;
let secret = super::client_secret::decrypt(ctx, &self.config, stored).await?;
let mut header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256);
header.typ = None;
jsonwebtoken::encode(
&header,
&payload,
&jsonwebtoken::EncodingKey::from_secret(secret.as_bytes()),
)
.map_err(|error| AuthError::internal(error.to_string()))?
};
let header = token
.split('.')
.next()
.and_then(|value| URL_SAFE_NO_PAD.decode(value).ok())
.and_then(|value| serde_json::from_slice::<Value>(&value).ok())
.ok_or_else(|| AuthError::internal("Invalid signed ID token header"))?;
if header.get("alg").and_then(Value::as_str) != Some(algorithm.as_str()) {
return super::clients::status_protocol_error(500,"server_error",&format!("ID token signed with {} but at_hash was computed for {algorithm}. Ensure jwt.sign uses the algorithm declared in keyPairConfig.alg.",header.get("alg").unwrap_or(&Value::Null))).map(Err);
}
Ok(Ok(Some(token)))
}
}