use anyhow::{Result, anyhow};
use systemprompt_identifiers::ClientId;
use systemprompt_models::Config;
use systemprompt_models::auth::AuthenticatedUser;
use systemprompt_oauth::OAuthState;
use systemprompt_oauth::repository::OAuthRepository;
use systemprompt_oauth::services::{JwtConfig, JwtSigningParams, generate_jwt_with_act};
use super::super::TokenResponse;
use super::RequestOrigin;
mod claims;
mod delegation;
mod id_jag_subject;
mod issue;
mod oidc;
mod subject;
pub use claims::{build_act_chain, intersect_scopes};
#[cfg(feature = "test-api")]
pub use delegation::validate_resource;
#[cfg(not(feature = "test-api"))]
use delegation::validate_resource;
pub use subject::peek_issuer;
#[cfg(feature = "test-api")]
pub mod test_api {
pub use super::claims::resolve_audience;
pub use super::id_jag_subject::validate_id_jag_subject;
pub use super::issue::issue_id_jag;
pub use super::oidc::validate_oidc_subject;
pub use super::subject::{SubjectIdentity, jwks_host_allowlist, validate_subject_token};
pub use super::{ACCESS_TOKEN_TYPE, ID_TOKEN_TYPE, JWT_TOKEN_TYPE, validate_resource};
}
use claims::resolve_audience;
use delegation::{ensure_session, resolve_delegate, resolve_resource};
use id_jag_subject::validate_id_jag_subject;
use issue::issue_id_jag;
use subject::validate_subject_token;
use systemprompt_oauth::services::validation::id_jag::ID_JAG_TOKEN_TYPE;
#[cfg_attr(
not(feature = "test-api"),
expect(
unreachable_pub,
reason = "items are re-exported via `test_api` only when the feature is on"
)
)]
pub const ACCESS_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:access_token";
#[cfg_attr(
not(feature = "test-api"),
expect(
unreachable_pub,
reason = "items are re-exported via `test_api` only when the feature is on"
)
)]
pub const ID_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:id_token";
#[cfg_attr(
not(feature = "test-api"),
expect(
unreachable_pub,
reason = "items are re-exported via `test_api` only when the feature is on"
)
)]
pub const JWT_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:jwt";
#[derive(Debug, Default)]
pub struct TokenExchangeRequest<'a> {
pub subject_token: &'a str,
pub subject_token_type: &'a str,
pub actor_token: Option<&'a str>,
pub actor_token_type: Option<&'a str>,
pub requested_token_type: Option<&'a str>,
pub scope: Option<&'a str>,
pub audience: Option<&'a str>,
pub resource: Option<&'a str>,
}
pub async fn handle_token_exchange(
repo: &OAuthRepository,
client_id: &ClientId,
request: TokenExchangeRequest<'_>,
origin: RequestOrigin<'_>,
state: &OAuthState,
) -> Result<TokenResponse> {
let global = Config::get()?;
if request.requested_token_type == Some(ID_JAG_TOKEN_TYPE) {
return issue_id_jag(client_id, &request, global).await;
}
let subject = if request.subject_token_type == ID_JAG_TOKEN_TYPE {
validate_id_jag_subject(request.subject_token, client_id, repo, global).await?
} else {
validate_subject_token(request.subject_token, request.subject_token_type, global).await?
};
let resource = resolve_resource(&subject, request.resource, global)?;
let (delegate, final_perms) =
resolve_delegate(repo, state, client_id, &subject, request.scope).await?;
let audience = resolve_audience(request.audience, global)?;
let issuer = &global.jwt_issuer;
let act = build_act_chain(client_id, issuer, subject.prior_act);
let delegate_uuid = uuid::Uuid::parse_str(delegate.user_id.as_str())
.map_err(|e| anyhow!("Delegated user has a non-uuid id ({e})"))?;
let delegated_user = AuthenticatedUser::new(
delegate_uuid,
delegate.name,
delegate.email,
final_perms.clone(),
);
let session_id = ensure_session(state, origin, &delegate.user_id, global).await?;
let config = JwtConfig {
permissions: final_perms.clone(),
audience: audience.clone(),
expires_in_hours: Some(global.jwt_access_token_expiration / 3600),
resource,
plugin_id: None,
client_id: Some(client_id.clone()),
};
let signing = JwtSigningParams {
issuer: &global.jwt_issuer,
};
let access_token = generate_jwt_with_act(
&delegated_user,
config,
uuid::Uuid::new_v4().to_string(),
&session_id,
&signing,
act,
)?;
let scope_string = final_perms
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ");
Ok(TokenResponse {
access_token,
token_type: "Bearer".to_owned(),
expires_in: global.jwt_access_token_expiration,
refresh_token: None,
scope: Some(scope_string),
issued_token_type: Some(ACCESS_TOKEN_TYPE.to_owned()),
})
}