use super::OAuthProviderPlugin;
use alibi_core::session::SessionRequest;
use alibi_core::store::OAuthClient;
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};
impl OAuthProviderPlugin {
pub(super) async fn issue_id_token<S: AuthSchema>(
&self,
client: &OAuthClient,
proof: &Value,
access_token: &str,
req: &impl SessionRequest,
ctx: &AuthContext<S>,
) -> AuthResult<Result<Option<String>, alibi_core::endpoint::EndpointResponse>> {
if self.config.disable_jwt_plugin && client.client_secret.is_none() {
return Ok(Ok(None));
}
let issued = chrono::Utc::now().timestamp();
let jwt = ctx
.extensions
.get::<crate::jwt::JwtPlugin>()
.ok_or_else(|| AuthError::config("jwt_config"))?;
let mut options = crate::jwt::JwtSignOptions::default();
let resolved = jwt
.resolve_signing_key(&options, req.physical_request(), ctx)
.await?;
let algorithm = 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 = ctx.extensions.get::<crate::jwt::JwtDiscoveryOptions>();
let issuer = discovery
.as_ref()
.and_then(|options| options.issuer.as_ref())
.unwrap_or(&base);
let mut payload=json!({"iss":issuer,"sub":proof.get("userId"),"aud":client.client_id,"iat":issued,"exp":issued+36_000,"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")) {
_ = 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());
}
options.resolved_key = resolved.map(std::sync::Arc::new);
let token = jwt
.sign_jwt(payload, &options, req.physical_request(), ctx)
.await?;
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)))
}
}