use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::client::ClientId;
use crate::error::{ErrorCode, ErrorResponse};
use crate::events::Event;
use crate::grant::GrantType;
use crate::scope::ScopeSet;
use crate::server::{AuthorizationServer, Bound, ClientCredential, Clock};
use crate::store::{Storage, StorageError};
pub const MAX_AUDIENCE_VALUES: usize = crate::server::MAX_RESOURCE_INDICATORS;
pub const MAX_ACT_CHAIN_DEPTH: usize = 8;
use crate::token::TokenType;
pub use crate::grant::TOKEN_EXCHANGE_GRANT_URN;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TokenTypeIdentifier {
#[serde(rename = "urn:ietf:params:oauth:token-type:access_token")]
AccessToken,
#[serde(rename = "urn:ietf:params:oauth:token-type:refresh_token")]
RefreshToken,
#[serde(rename = "urn:ietf:params:oauth:token-type:id_token")]
IdToken,
#[serde(rename = "urn:ietf:params:oauth:token-type:saml1")]
Saml1,
#[serde(rename = "urn:ietf:params:oauth:token-type:saml2")]
Saml2,
#[serde(rename = "urn:ietf:params:oauth:token-type:jwt")]
Jwt,
}
impl TokenTypeIdentifier {
pub fn parse(s: &str) -> Option<Self> {
match s {
"urn:ietf:params:oauth:token-type:access_token" => {
Some(TokenTypeIdentifier::AccessToken)
}
"urn:ietf:params:oauth:token-type:refresh_token" => {
Some(TokenTypeIdentifier::RefreshToken)
}
"urn:ietf:params:oauth:token-type:id_token" => Some(TokenTypeIdentifier::IdToken),
"urn:ietf:params:oauth:token-type:saml1" => Some(TokenTypeIdentifier::Saml1),
"urn:ietf:params:oauth:token-type:saml2" => Some(TokenTypeIdentifier::Saml2),
"urn:ietf:params:oauth:token-type:jwt" => Some(TokenTypeIdentifier::Jwt),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
TokenTypeIdentifier::AccessToken => "urn:ietf:params:oauth:token-type:access_token",
TokenTypeIdentifier::RefreshToken => "urn:ietf:params:oauth:token-type:refresh_token",
TokenTypeIdentifier::IdToken => "urn:ietf:params:oauth:token-type:id_token",
TokenTypeIdentifier::Saml1 => "urn:ietf:params:oauth:token-type:saml1",
TokenTypeIdentifier::Saml2 => "urn:ietf:params:oauth:token-type:saml2",
TokenTypeIdentifier::Jwt => "urn:ietf:params:oauth:token-type:jwt",
}
}
}
impl fmt::Display for TokenTypeIdentifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownTokenTypeIdentifier(String);
impl UnknownTokenTypeIdentifier {
pub fn identifier(&self) -> &str {
&self.0
}
}
impl fmt::Display for UnknownTokenTypeIdentifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown token type identifier {:?}", self.0)
}
}
impl std::error::Error for UnknownTokenTypeIdentifier {}
impl FromStr for TokenTypeIdentifier {
type Err = UnknownTokenTypeIdentifier;
fn from_str(s: &str) -> Result<Self, Self::Err> {
TokenTypeIdentifier::parse(s).ok_or_else(|| UnknownTokenTypeIdentifier(s.to_string()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExchangeSemantics {
Impersonation,
Delegation,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActClaim {
pub sub: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub act: Option<Box<ActClaim>>,
}
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct TokenExchangeRequest<'a> {
pub client_id: &'a ClientId,
pub client_secret: Option<&'a str>,
#[cfg(feature = "client-assertion")]
pub client_assertion_type: Option<&'a str>,
#[cfg(feature = "client-assertion")]
pub client_assertion: Option<&'a str>,
pub subject_token: &'a str,
pub subject_token_type: TokenTypeIdentifier,
pub actor_token: Option<&'a str>,
pub actor_token_type: Option<TokenTypeIdentifier>,
pub resource: &'a [String],
pub audience: &'a [String],
pub scope: Option<&'a ScopeSet>,
pub requested_token_type: Option<TokenTypeIdentifier>,
}
impl<'a> TokenExchangeRequest<'a> {
pub fn new(
client_id: &'a ClientId,
subject_token: &'a str,
subject_token_type: TokenTypeIdentifier,
) -> Self {
TokenExchangeRequest {
client_id,
client_secret: None,
#[cfg(feature = "client-assertion")]
client_assertion_type: None,
#[cfg(feature = "client-assertion")]
client_assertion: None,
subject_token,
subject_token_type,
actor_token: None,
actor_token_type: None,
resource: &[],
audience: &[],
scope: None,
requested_token_type: None,
}
}
}
impl fmt::Debug for TokenExchangeRequest<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn redact_opt<T>(value: &Option<T>) -> Option<&'static str> {
value.as_ref().map(|_| "[redacted]")
}
let mut out = f.debug_struct("TokenExchangeRequest");
out.field("client_id", &self.client_id)
.field("client_secret", &redact_opt(&self.client_secret));
#[cfg(feature = "client-assertion")]
out.field("client_assertion_type", &self.client_assertion_type)
.field("client_assertion", &redact_opt(&self.client_assertion));
out.field("subject_token", &"[redacted]")
.field("subject_token_type", &self.subject_token_type)
.field("actor_token", &redact_opt(&self.actor_token))
.field("actor_token_type", &self.actor_token_type)
.field("resource", &self.resource)
.field("audience", &self.audience)
.field("scope", &self.scope)
.field("requested_token_type", &self.requested_token_type)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenExchangeResponse {
pub access_token: String,
pub issued_token_type: TokenTypeIdentifier,
pub token_type: TokenType,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_in: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
}
impl fmt::Debug for TokenExchangeResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TokenExchangeResponse")
.field("access_token", &"[redacted]")
.field("issued_token_type", &self.issued_token_type)
.field("token_type", &self.token_type)
.field("expires_in", &self.expires_in)
.field("scope", &self.scope)
.field(
"refresh_token",
&self.refresh_token.as_ref().map(|_| "[redacted]"),
)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExchangedToken {
pub response: TokenExchangeResponse,
pub semantics: ExchangeSemantics,
pub act: Option<ActClaim>,
}
pub trait TokenExchange {
fn exchange_token(
&self,
request: &TokenExchangeRequest<'_>,
) -> impl std::future::Future<Output = Result<ExchangedToken, ErrorResponse>> + Send;
}
impl<S: Storage, C: Clock> TokenExchange for AuthorizationServer<S, C> {
async fn exchange_token(
&self,
request: &TokenExchangeRequest<'_>,
) -> Result<ExchangedToken, ErrorResponse> {
let outcome = exchange(self, request).await;
if let Err(error) = &outcome {
self.hooks().emit(|| Event::GrantRefused {
client_id: request.client_id.as_str(),
grant_type: GrantType::TokenExchange,
error: error.error,
});
}
outcome
}
}
fn storage_error(e: StorageError) -> ErrorResponse {
let _ = e;
ErrorResponse::new(ErrorCode::ServerError)
}
#[cfg(any(feature = "dpop", feature = "mtls"))]
fn sender_constrained_refusal(mechanism: &str) -> ErrorResponse {
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(format!(
"subject_token is sender constrained by {mechanism} and cannot be exchanged for a token \
that is not, because the issued token would belong to a client that cannot prove \
possession of the binding key"
))
}
fn act_chain_depth(act: &ActClaim) -> usize {
let mut depth = 1;
let mut current = &act.act;
while let Some(next) = current {
depth += 1;
if depth >= MAX_ACT_CHAIN_DEPTH {
return depth;
}
current = &next.act;
}
depth
}
async fn exchange<S: Storage, C: Clock>(
server: &AuthorizationServer<S, C>,
request: &TokenExchangeRequest<'_>,
) -> Result<ExchangedToken, ErrorResponse> {
let issued_token_type = match request.requested_token_type {
None | Some(TokenTypeIdentifier::AccessToken) => TokenTypeIdentifier::AccessToken,
Some(_) => {
return Err(
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
"this server issues only urn:ietf:params:oauth:token-type:access_token",
),
)
}
};
let bound = Bound {
cred: ClientCredential {
client_secret: request.client_secret,
#[cfg(feature = "client-assertion")]
client_assertion_type: request.client_assertion_type,
#[cfg(feature = "client-assertion")]
client_assertion: request.client_assertion,
#[cfg(feature = "mtls")]
certificate: None,
},
#[cfg(feature = "dpop")]
jkt: None,
};
let client = server
.authenticate_client(request.client_id, &bound.cred)
.await?;
if !client.auth.is_confidential() {
server.hooks().emit(|| Event::ClientAuthenticationFailed {
client_id: request.client_id.as_str(),
failure: crate::events::ClientAuthFailure::NotConfidential,
});
return Err(ErrorResponse::new(ErrorCode::InvalidClient));
}
if !client.allows_grant(GrantType::TokenExchange) {
return Err(ErrorResponse::new(ErrorCode::UnauthorizedClient)
.with_description("client registration does not include the token-exchange grant"));
}
if request.subject_token_type != TokenTypeIdentifier::AccessToken {
return Err(
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
"subject_token_type must be urn:ietf:params:oauth:token-type:access_token",
),
);
}
let subject = server
.introspect(request.subject_token)
.await
.map_err(storage_error)?
.ok_or_else(|| {
ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("subject_token is not a live access token")
})?;
#[cfg(feature = "dpop")]
if subject.jkt.is_some() && !server.config().allow_sender_constrained_exchange {
return Err(sender_constrained_refusal("DPoP (RFC 9449)"));
}
#[cfg(feature = "mtls")]
if subject.x5t_s256.is_some() && !server.config().allow_sender_constrained_exchange {
return Err(sender_constrained_refusal("mutual TLS (RFC 8705)"));
}
let (semantics, act) = match (request.actor_token, request.actor_token_type) {
(None, None) => (ExchangeSemantics::Impersonation, None),
(None, Some(_)) => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("actor_token_type is meaningless without actor_token"))
}
(Some(_), None) => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("actor_token_type is required when actor_token is present"))
}
(Some(actor_token), Some(actor_token_type)) => {
if actor_token_type != TokenTypeIdentifier::AccessToken {
return Err(
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
"actor_token_type must be urn:ietf:params:oauth:token-type:access_token",
),
);
}
let unusable = || {
ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("actor_token is not a live access token")
};
let actor = server
.introspect(actor_token)
.await
.map_err(storage_error)?
.ok_or_else(unusable)?;
if actor.client_id != client.client_id {
return Err(unusable());
}
if let Some(prior) = &subject.act {
if act_chain_depth(prior) >= MAX_ACT_CHAIN_DEPTH {
return Err(
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
"the subject token's act chain is already at this server's maximum \
delegation depth",
),
);
}
}
let act = ActClaim {
sub: actor
.subject
.clone()
.unwrap_or_else(|| actor.client_id.as_str().to_string()),
client_id: Some(actor.client_id.as_str().to_string()),
act: subject.act.clone(),
};
(ExchangeSemantics::Delegation, Some(act))
}
};
let scope = match request.scope {
None => subject.scope.clone(),
Some(s) if s.is_subset(&subject.scope) => s.clone(),
Some(_) => {
return Err(
ErrorResponse::new(ErrorCode::InvalidScope).with_description(
"token exchange may narrow the subject token scope, never widen it",
),
)
}
};
if !scope.is_subset(&client.allowed_scopes) {
return Err(ErrorResponse::new(ErrorCode::InvalidScope)
.with_description("scope exceeds the exchanging client registration"));
}
#[cfg(feature = "rar")]
if !subject.authorization_details.is_empty()
&& subject.client_id != client.client_id
&& !server.config().allow_authorization_details_exchange
{
return Err(
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
"subject_token carries authorization_details and cannot be exchanged for a token \
issued to a different client, because this server has no per-client registration of \
the detail types that client may hold",
),
);
}
let mut targets = server.validate_resources(request.resource.iter().map(|r| r.as_str()))?;
if request.audience.len() > MAX_AUDIENCE_VALUES {
return Err(ErrorResponse::new(ErrorCode::InvalidTarget)
.with_description("too many audience values (RFC 8693 s2.1.1)"));
}
for audience in request.audience {
server.target_is_permitted(audience)?;
if !targets.iter().any(|t| t == audience) {
targets.push(audience.clone());
}
}
let resource = server.narrow_and_permit(&subject.resource, &targets)?;
let issued = server
.issue(
&client,
&bound,
GrantType::TokenExchange,
subject.grant_established_at,
subject.subject.clone(),
scope,
resource,
crate::server::GrantedDetails::of_token(&subject),
None,
false,
crate::server::GrantedAuthentication::default(),
crate::server::GrantedActor {
act: act.clone().map(Box::new),
},
Some(subject.expires_at),
)
.await?;
Ok(ExchangedToken {
response: TokenExchangeResponse {
access_token: issued.access_token,
issued_token_type,
token_type: issued.token_type,
expires_in: Some(issued.expires_in),
scope: issued.scope,
refresh_token: None,
},
semantics,
act,
})
}
#[cfg(test)]
#[path = "tests/token_exchange.rs"]
mod tests;