use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::{BTreeMap, BTreeSet};
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use base64::{engine::general_purpose, Engine as _};
use hashbrown::HashSet;
pub use compact_jwt::{compact::JwkKeySet, OidcToken};
use compact_jwt::{
crypto::JwsRs256Signer, jws::JwsBuilder, JwsCompact, JwsEs256Signer, JwsSigner,
JwsSignerToVerifier, JwsVerifier, OidcClaims, OidcSubject,
};
use concread::cowcell::*;
use fernet::Fernet;
use hashbrown::HashMap;
use kanidm_proto::constants::*;
pub use kanidm_proto::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse, GrantTypeReq,
OAuth2RFC9068Token, OAuth2RFC9068TokenExtensions, Oauth2Rfc8414MetadataResponse,
OidcDiscoveryResponse, PkceAlg, TokenRevokeRequest,
};
use kanidm_proto::oauth2::{
AccessTokenType, ClaimType, DeviceAuthorizationResponse, DisplayValue, GrantType,
IdTokenSignAlg, ResponseMode, ResponseType, SubjectType, TokenEndpointAuthMethod,
};
use openssl::sha;
use serde::{Deserialize, Serialize};
use serde_with::{formats, serde_as};
use time::OffsetDateTime;
use tracing::trace;
use uri::{OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT};
use url::{Host, Origin, Url};
use crate::idm::account::Account;
use crate::idm::server::{
IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
};
use crate::prelude::*;
use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE};
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Oauth2Error {
AuthenticationRequired,
InvalidClientId,
InvalidOrigin,
InvalidRequest,
InvalidGrant,
UnauthorizedClient,
AccessDenied,
UnsupportedResponseType,
InvalidScope,
ServerError(OperationError),
TemporarilyUnavailable,
InvalidToken,
InsufficientScope,
UnsupportedTokenType,
SlowDown,
AuthorizationPending,
ExpiredToken,
}
impl std::fmt::Display for Oauth2Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Oauth2Error::AuthenticationRequired => "authentication_required",
Oauth2Error::InvalidClientId => "invalid_client_id",
Oauth2Error::InvalidOrigin => "invalid_origin",
Oauth2Error::InvalidGrant => "invalid_grant",
Oauth2Error::InvalidRequest => "invalid_request",
Oauth2Error::UnauthorizedClient => "unauthorized_client",
Oauth2Error::AccessDenied => "access_denied",
Oauth2Error::UnsupportedResponseType => "unsupported_response_type",
Oauth2Error::InvalidScope => "invalid_scope",
Oauth2Error::ServerError(_) => "server_error",
Oauth2Error::TemporarilyUnavailable => "temporarily_unavailable",
Oauth2Error::InvalidToken => "invalid_token",
Oauth2Error::InsufficientScope => "insufficient_scope",
Oauth2Error::UnsupportedTokenType => "unsupported_token_type",
Oauth2Error::SlowDown => "slow_down",
Oauth2Error::AuthorizationPending => "authorization_pending",
Oauth2Error::ExpiredToken => "expired_token",
})
}
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct ConsentToken {
pub client_id: String,
pub session_id: Uuid,
pub ident_id: IdentityId,
pub state: String,
#[serde_as(
as = "Option<serde_with::base64::Base64<serde_with::base64::UrlSafe, formats::Unpadded>>"
)]
pub code_challenge: Option<Vec<u8>>,
pub redirect_uri: Url,
pub scopes: BTreeSet<String>,
pub nonce: Option<String>,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
struct TokenExchangeCode {
pub account_uuid: Uuid,
pub session_id: Uuid,
#[serde_as(
as = "Option<serde_with::base64::Base64<serde_with::base64::UrlSafe, formats::Unpadded>>"
)]
pub code_challenge: Option<Vec<u8>>,
pub redirect_uri: Url,
pub scopes: BTreeSet<String>,
pub nonce: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub(crate) enum Oauth2TokenType {
Refresh {
scopes: BTreeSet<String>,
parent_session_id: Uuid,
session_id: Uuid,
exp: i64,
uuid: Uuid,
iat: i64,
nbf: i64,
nonce: Option<String>,
},
ClientAccess {
scopes: BTreeSet<String>,
session_id: Uuid,
uuid: Uuid,
exp: i64,
iat: i64,
nbf: i64,
},
}
impl fmt::Display for Oauth2TokenType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Oauth2TokenType::Refresh { session_id, .. } => {
write!(f, "refresh_token ({session_id}) ")
}
Oauth2TokenType::ClientAccess { session_id, .. } => {
write!(f, "client_access_token ({session_id})")
}
}
}
}
#[derive(Debug)]
pub enum AuthoriseResponse {
AuthenticationRequired {
client_name: String,
login_hint: Option<String>,
},
ConsentRequested {
client_name: String,
scopes: BTreeSet<String>,
pii_scopes: BTreeSet<String>,
consent_token: String,
},
Permitted(AuthorisePermitSuccess),
}
#[derive(Debug)]
pub struct AuthorisePermitSuccess {
pub redirect_uri: Url,
pub state: String,
pub code: String,
}
#[derive(Clone)]
enum OauthRSType {
Basic {
authz_secret: String,
enable_pkce: bool,
},
Public {
allow_localhost_redirect: bool,
},
}
impl OauthRSType {
fn allow_localhost_redirect(&self) -> bool {
match self {
OauthRSType::Basic { .. } => false,
OauthRSType::Public {
allow_localhost_redirect,
} => *allow_localhost_redirect,
}
}
}
impl std::fmt::Debug for OauthRSType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut ds = f.debug_struct("OauthRSType");
match self {
OauthRSType::Basic { enable_pkce, .. } => {
ds.field("type", &"basic").field("pkce", enable_pkce)
}
OauthRSType::Public {
allow_localhost_redirect,
} => ds
.field("type", &"public")
.field("allow_localhost_redirect", allow_localhost_redirect),
};
ds.finish()
}
}
#[derive(Clone)]
enum Oauth2JwsSigner {
ES256 { signer: JwsEs256Signer },
RS256 { signer: JwsRs256Signer },
}
#[derive(Clone, Debug)]
struct ClaimValue {
join: OauthClaimMapJoin,
values: BTreeSet<String>,
}
impl ClaimValue {
fn merge(&mut self, other: &Self) {
self.values.extend(other.values.iter().cloned())
}
fn to_json_value(&self) -> serde_json::Value {
let join_str = match self.join {
OauthClaimMapJoin::JsonArray => {
let arr: Vec<_> = self
.values
.iter()
.cloned()
.map(serde_json::Value::String)
.collect();
return serde_json::Value::Array(arr);
}
joiner => joiner.to_str(),
};
let joined = str_concat!(&self.values, join_str);
serde_json::Value::String(joined)
}
}
#[derive(Clone)]
pub struct Oauth2RS {
name: String,
displayname: String,
uuid: Uuid,
origins: HashSet<Origin>,
opaque_origins: HashSet<Url>,
redirect_uris: HashSet<Url>,
origin_https_required: bool,
strict_redirect_uri: bool,
claim_map: BTreeMap<Uuid, Vec<(String, ClaimValue)>>,
scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
client_scopes: BTreeSet<String>,
client_sup_scopes: BTreeSet<String>,
token_fernet: Fernet,
jws_signer: Oauth2JwsSigner,
iss: Url,
authorization_endpoint: Url,
token_endpoint: Url,
revocation_endpoint: Url,
introspection_endpoint: Url,
userinfo_endpoint: Url,
jwks_uri: Url,
scopes_supported: BTreeSet<String>,
prefer_short_username: bool,
type_: OauthRSType,
has_custom_image: bool,
device_authorization_endpoint: Option<Url>,
}
impl Oauth2RS {
pub fn is_basic(&self) -> bool {
match self.type_ {
OauthRSType::Basic { .. } => true,
OauthRSType::Public { .. } => false,
}
}
pub fn is_pkce(&self) -> bool {
match self.type_ {
OauthRSType::Basic { .. } => false,
OauthRSType::Public { .. } => true,
}
}
pub fn require_pkce(&self) -> bool {
match &self.type_ {
OauthRSType::Basic { enable_pkce, .. } => *enable_pkce,
OauthRSType::Public { .. } => true,
}
}
pub fn device_flow_enabled(&self) -> bool {
self.device_authorization_endpoint.is_some()
}
}
impl std::fmt::Debug for Oauth2RS {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("Oauth2RS")
.field("name", &self.name)
.field("displayname", &self.displayname)
.field("uuid", &self.uuid)
.field("type", &self.type_)
.field("origins", &self.origins)
.field("opaque_origins", &self.opaque_origins)
.field("scope_maps", &self.scope_maps)
.field("sup_scope_maps", &self.sup_scope_maps)
.field("claim_map", &self.claim_map)
.field("has_custom_image", &self.has_custom_image)
.finish()
}
}
#[derive(Clone)]
struct Oauth2RSInner {
origin: Url,
fernet: Fernet,
rs_set: HashMap<String, Oauth2RS>,
}
pub struct Oauth2ResourceServers {
inner: CowCell<Oauth2RSInner>,
}
pub struct Oauth2ResourceServersReadTransaction {
inner: CowCellReadTxn<Oauth2RSInner>,
}
pub struct Oauth2ResourceServersWriteTransaction<'a> {
inner: CowCellWriteTxn<'a, Oauth2RSInner>,
}
impl TryFrom<(Vec<Arc<EntrySealedCommitted>>, Url, DomainVersion)> for Oauth2ResourceServers {
type Error = OperationError;
fn try_from(
value: (Vec<Arc<EntrySealedCommitted>>, Url, DomainVersion),
) -> Result<Self, Self::Error> {
let (value, origin, domain_level) = value;
let fernet =
Fernet::new(&Fernet::generate_key()).ok_or(OperationError::CryptographyError)?;
let oauth2rs = Oauth2ResourceServers {
inner: CowCell::new(Oauth2RSInner {
origin,
fernet,
rs_set: HashMap::new(),
}),
};
let mut oauth2rs_wr = oauth2rs.write();
oauth2rs_wr.reload(value, domain_level)?;
oauth2rs_wr.commit();
Ok(oauth2rs)
}
}
impl Oauth2ResourceServers {
pub fn read(&self) -> Oauth2ResourceServersReadTransaction {
Oauth2ResourceServersReadTransaction {
inner: self.inner.read(),
}
}
pub fn write(&self) -> Oauth2ResourceServersWriteTransaction {
Oauth2ResourceServersWriteTransaction {
inner: self.inner.write(),
}
}
}
impl Oauth2ResourceServersWriteTransaction<'_> {
pub fn reload(
&mut self,
value: Vec<Arc<EntrySealedCommitted>>,
domain_level: DomainVersion,
) -> Result<(), OperationError> {
let rs_set: Result<HashMap<_, _>, _> = value
.into_iter()
.map(|ent| {
let uuid = ent.get_uuid();
trace!(?uuid, "Checking OAuth2 configuration");
if !ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into()) {
error!("Missing class oauth2_resource_server");
return Err(OperationError::InvalidEntryState);
}
let type_ = if ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerBasic.into()) {
let authz_secret = ent
.get_ava_single_secret(Attribute::OAuth2RsBasicSecret)
.map(str::to_string)
.ok_or(OperationError::InvalidValueState)?;
let enable_pkce = ent
.get_ava_single_bool(Attribute::OAuth2AllowInsecureClientDisablePkce)
.map(|e| !e)
.unwrap_or(true);
OauthRSType::Basic {
authz_secret,
enable_pkce,
}
} else if ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerPublic.into()) {
let allow_localhost_redirect = ent
.get_ava_single_bool(Attribute::OAuth2AllowLocalhostRedirect)
.unwrap_or(false);
OauthRSType::Public {
allow_localhost_redirect
}
} else {
error!("Missing class determining OAuth2 rs type");
return Err(OperationError::InvalidEntryState);
};
let name = ent
.get_ava_single_iname(Attribute::Name)
.map(str::to_string)
.ok_or(OperationError::InvalidValueState)?;
let displayname = ent
.get_ava_single_utf8(Attribute::DisplayName)
.map(str::to_string)
.ok_or(OperationError::InvalidValueState)?;
let landing_url = ent
.get_ava_single_url(Attribute::OAuth2RsOriginLanding)
.cloned()
.ok_or(OperationError::InvalidValueState)?;
let maybe_extra_urls = ent.get_ava_set(Attribute::OAuth2RsOrigin).and_then(|s| s.as_url_set());
let len_uris = maybe_extra_urls.map(|s| s.len() + 1).unwrap_or(1);
let strict_redirect_uri = cfg!(test) ||
domain_level >= DOMAIN_LEVEL_8 ||
ent.get_ava_single_bool(Attribute::OAuth2StrictRedirectUri)
.unwrap_or(false);
let mut redirect_uris_v = Vec::with_capacity(len_uris);
redirect_uris_v.push(landing_url);
if let Some(extra_origins) = maybe_extra_urls {
for x_origin in extra_origins {
redirect_uris_v.push(x_origin.clone());
}
}
let mut origins = HashSet::with_capacity(len_uris);
let mut redirect_uris = HashSet::with_capacity(len_uris);
let mut opaque_origins = HashSet::with_capacity(len_uris);
let mut origin_https_required = false;
for mut uri in redirect_uris_v.into_iter() {
uri.set_fragment(None);
if uri.scheme() == "https" {
origin_https_required = true;
origins.insert(uri.origin());
redirect_uris.insert(uri);
} else if uri.scheme() == "http" {
origins.insert(uri.origin());
redirect_uris.insert(uri);
} else {
opaque_origins.insert(uri);
}
}
let token_fernet = ent
.get_ava_single_secret(Attribute::OAuth2RsTokenKey)
.ok_or(OperationError::InvalidValueState)
.and_then(|key| {
Fernet::new(key).ok_or(OperationError::CryptographyError)
})?;
let scope_maps = ent
.get_ava_as_oauthscopemaps(Attribute::OAuth2RsScopeMap)
.cloned()
.unwrap_or_default();
let sup_scope_maps = ent
.get_ava_as_oauthscopemaps(Attribute::OAuth2RsSupScopeMap)
.cloned()
.unwrap_or_default();
let (client_scopes, client_sup_scopes) = if let Some(client_member_of) = ent.get_ava_refer(Attribute::MemberOf) {
let client_scopes =
scope_maps
.iter()
.filter_map(|(u, m)| {
if client_member_of.contains(u) {
Some(m.iter())
} else {
None
}
})
.flatten()
.cloned()
.collect::<BTreeSet<_>>();
let client_sup_scopes = sup_scope_maps
.iter()
.filter_map(|(u, m)| {
if client_member_of.contains(u) {
Some(m.iter())
} else {
None
}
})
.flatten()
.cloned()
.collect::<BTreeSet<_>>();
(client_scopes, client_sup_scopes)
} else {
(BTreeSet::default(), BTreeSet::default())
};
let e_claim_maps = ent
.get_ava_set(Attribute::OAuth2RsClaimMap)
.and_then(|vs| vs.as_oauthclaim_map());
let claim_map = if let Some(e_claim_maps) = e_claim_maps {
let mut claim_map = BTreeMap::default();
for (claim_name, claim_mapping) in e_claim_maps.iter() {
for (group_uuid, claim_values) in claim_mapping.values().iter() {
match claim_map.entry(*group_uuid) {
BTreeEntry::Vacant(e) => {
e.insert(
vec![
(
claim_name.clone(), ClaimValue {
join: claim_mapping.join(),
values: claim_values.clone()
}
)
]
);
}
BTreeEntry::Occupied(mut e) => {
e.get_mut().push((
claim_name.clone(), ClaimValue {
join: claim_mapping.join(),
values: claim_values.clone()
}
));
}
}
}
}
claim_map
} else {
BTreeMap::default()
};
trace!("{}", Attribute::OAuth2JwtLegacyCryptoEnable);
let jws_signer = if ent.get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable).unwrap_or(false) {
trace!("{}", Attribute::Rs256PrivateKeyDer);
ent
.get_ava_single_private_binary(Attribute::Rs256PrivateKeyDer)
.ok_or(OperationError::InvalidValueState)
.and_then(|key_der| {
JwsRs256Signer::from_rs256_der(key_der)
.map(|signer| Oauth2JwsSigner::RS256 { signer })
.map_err(|e| {
admin_error!(err = ?e, "Unable to load Legacy RS256 JwsSigner from DER");
OperationError::CryptographyError
})
})?
} else {
trace!("{}", Attribute::Es256PrivateKeyDer);
ent
.get_ava_single_private_binary(Attribute::Es256PrivateKeyDer)
.ok_or(OperationError::InvalidValueState)
.and_then(|key_der| {
JwsEs256Signer::from_es256_der(key_der)
.map(|signer| Oauth2JwsSigner::ES256 { signer })
.map_err(|e| {
admin_error!(err = ?e, "Unable to load ES256 JwsSigner from DER");
OperationError::CryptographyError
})
})?
};
let prefer_short_username = ent
.get_ava_single_bool(Attribute::OAuth2PreferShortUsername)
.unwrap_or(false);
let has_custom_image = ent.get_ava_single_image(Attribute::Image).is_some();
let mut authorization_endpoint = self.inner.origin.clone();
authorization_endpoint.set_path("/ui/oauth2");
let mut token_endpoint = self.inner.origin.clone();
token_endpoint.set_path(uri::OAUTH2_TOKEN_ENDPOINT);
let mut revocation_endpoint = self.inner.origin.clone();
revocation_endpoint.set_path(OAUTH2_TOKEN_REVOKE_ENDPOINT);
let mut introspection_endpoint = self.inner.origin.clone();
introspection_endpoint.set_path(OAUTH2_TOKEN_INTROSPECT_ENDPOINT);
let mut userinfo_endpoint = self.inner.origin.clone();
userinfo_endpoint.set_path(&format!("/oauth2/openid/{name}/userinfo"));
let mut jwks_uri = self.inner.origin.clone();
jwks_uri.set_path(&format!("/oauth2/openid/{name}/public_key.jwk"));
let mut iss = self.inner.origin.clone();
iss.set_path(&format!("/oauth2/openid/{name}"));
let scopes_supported: BTreeSet<String> =
scope_maps
.values()
.flat_map(|bts| bts.iter())
.chain(
sup_scope_maps
.values()
.flat_map(|bts| bts.iter())
)
.cloned()
.collect();
let device_authorization_endpoint: Option<Url> = match cfg!(feature="dev-oauth2-device-flow") {
true => {
match ent.get_ava_single_bool(Attribute::OAuth2DeviceFlowEnable).unwrap_or(false) {
true => {
let mut device_authorization_endpoint = self.inner.origin.clone();
device_authorization_endpoint.set_path(uri::OAUTH2_AUTHORISE_DEVICE);
Some(device_authorization_endpoint)
},
false => None
}
},
false => {None}
};
let client_id = name.clone();
let rscfg = Oauth2RS {
name,
displayname,
uuid,
origins,
opaque_origins,
redirect_uris,
origin_https_required,
strict_redirect_uri,
scope_maps,
sup_scope_maps,
client_scopes,
client_sup_scopes,
claim_map,
token_fernet,
jws_signer,
iss,
authorization_endpoint,
token_endpoint,
revocation_endpoint,
introspection_endpoint,
userinfo_endpoint,
jwks_uri,
scopes_supported,
prefer_short_username,
type_,
has_custom_image,
device_authorization_endpoint,
};
Ok((client_id, rscfg))
})
.collect();
rs_set.map(|mut rs_set| {
let inner_ref = self.inner.get_mut();
std::mem::swap(&mut inner_ref.rs_set, &mut rs_set);
})
}
pub fn commit(self) {
self.inner.commit();
}
}
impl IdmServerProxyWriteTransaction<'_> {
#[instrument(level = "debug", skip_all)]
pub fn oauth2_token_revoke(
&mut self,
client_auth_info: &ClientAuthInfo,
revoke_req: &TokenRevokeRequest,
ct: Duration,
) -> Result<(), Oauth2Error> {
let Some(client_authz) = client_auth_info.basic_authz.as_ref() else {
admin_warn!("OAuth2 client_id not provided by basic authz");
return Err(Oauth2Error::AuthenticationRequired);
};
let (client_id, secret) = parse_basic_authz(client_authz.as_str())?;
let o2rs = self.oauth2rs.inner.rs_set.get(&client_id).ok_or_else(|| {
admin_warn!("Invalid OAuth2 client_id");
Oauth2Error::AuthenticationRequired
})?;
match &o2rs.type_ {
OauthRSType::Basic { authz_secret, .. } => {
if authz_secret != &secret {
security_info!("Invalid OAuth2 client_id secret, this can happen if your RS is public but you configured a 'basic' type.");
return Err(Oauth2Error::AuthenticationRequired);
}
}
OauthRSType::Public { .. } => {}
};
let (session_id, expiry, uuid) = if let Ok(jwsc) = JwsCompact::from_str(&revoke_req.token) {
let access_token = match &o2rs.jws_signer {
Oauth2JwsSigner::ES256 { signer } => signer
.get_verifier()
.and_then(|verifier| verifier.verify(&jwsc)),
Oauth2JwsSigner::RS256 { signer } => signer
.get_verifier()
.and_then(|verifier| verifier.verify(&jwsc)),
}
.map_err(|err| {
admin_error!(?err, "Unable to verify access token");
Oauth2Error::InvalidRequest
})
.and_then(|jws| {
jws.from_json().map_err(|err| {
admin_error!(?err, "Unable to deserialise access token");
Oauth2Error::InvalidRequest
})
})?;
let OAuth2RFC9068Token::<_> {
sub: uuid,
exp,
extensions: OAuth2RFC9068TokenExtensions { session_id, .. },
..
} = access_token;
(session_id, exp, uuid)
} else {
let token: Oauth2TokenType = o2rs
.token_fernet
.decrypt(&revoke_req.token)
.map_err(|_| {
admin_error!("Failed to decrypt token revoke request");
Oauth2Error::InvalidRequest
})
.and_then(|data| {
serde_json::from_slice(&data).map_err(|e| {
admin_error!("Failed to deserialise token - {:?}", e);
Oauth2Error::InvalidRequest
})
})?;
match token {
Oauth2TokenType::ClientAccess {
session_id,
exp,
uuid,
..
}
| Oauth2TokenType::Refresh {
session_id,
exp,
uuid,
..
} => (session_id, exp, uuid),
}
};
if expiry <= ct.as_secs() as i64 {
security_info!(?uuid, "token has expired, returning inactive");
return Ok(());
}
let modlist: ModifyList<ModifyInvalid> = ModifyList::new_list(vec![Modify::Removed(
Attribute::OAuth2Session,
PartialValue::Refer(session_id),
)]);
self.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(uuid))),
&modlist,
)
.map_err(|e| {
admin_error!("Failed to modify - revoke OAuth2 session {:?}", e);
Oauth2Error::ServerError(e)
})
}
#[instrument(level = "debug", skip_all)]
pub fn check_oauth2_token_exchange(
&mut self,
client_auth_info: &ClientAuthInfo,
token_req: &AccessTokenRequest,
ct: Duration,
) -> Result<AccessTokenResponse, Oauth2Error> {
let (client_id, secret) = if let Some(client_authz) = client_auth_info.basic_authz.as_ref()
{
let (client_id, secret) = parse_basic_authz(client_authz.as_str())?;
(client_id, Some(secret))
} else {
match (&token_req.client_id, &token_req.client_secret) {
(Some(a), b) => (a.clone(), b.clone()),
_ => {
security_info!(
"Invalid OAuth2 authentication - no basic auth or missing client_id in access token request"
);
return Err(Oauth2Error::AuthenticationRequired);
}
}
};
let o2rs = self.get_client(&client_id)?;
let client_authentication_valid = match &o2rs.type_ {
OauthRSType::Basic { authz_secret, .. } => {
match secret {
Some(secret) => {
if authz_secret == &secret {
true
} else {
security_info!("Invalid OAuth2 client_id secret");
return Err(Oauth2Error::AuthenticationRequired);
}
}
None => {
security_info!(
"Invalid OAuth2 authentication - no secret in access token request - this can happen if you're expecting a public client and configured a basic one."
);
return Err(Oauth2Error::AuthenticationRequired);
}
}
}
OauthRSType::Public { .. } => false,
};
match &token_req.grant_type {
GrantTypeReq::AuthorizationCode {
code,
redirect_uri,
code_verifier,
} => self.check_oauth2_token_exchange_authorization_code(
&o2rs,
code,
redirect_uri,
code_verifier.as_deref(),
ct,
),
GrantTypeReq::ClientCredentials { scope } => {
if client_authentication_valid {
self.check_oauth2_token_client_credentials(&o2rs, scope.as_ref(), ct)
} else {
security_info!(
"Unable to proceed with client credentials grant unless client authentication is provided and valid"
);
Err(Oauth2Error::AuthenticationRequired)
}
}
GrantTypeReq::RefreshToken {
refresh_token,
scope,
} => self.check_oauth2_token_refresh(&o2rs, refresh_token, scope.as_ref(), ct),
GrantTypeReq::DeviceCode { device_code, scope } => {
self.check_oauth2_device_code_status(device_code, scope)
}
}
}
fn get_client(&self, client_id: &str) -> Result<Oauth2RS, Oauth2Error> {
let s = self
.oauth2rs
.inner
.rs_set
.get(client_id)
.ok_or_else(|| {
admin_warn!("Invalid OAuth2 client_id {}", client_id);
Oauth2Error::AuthenticationRequired
})?
.clone();
Ok(s)
}
#[instrument(level = "info", skip(self))]
pub fn handle_oauth2_start_device_flow(
&mut self,
_client_auth_info: ClientAuthInfo,
_client_id: &str,
_scope: &Option<BTreeSet<String>>,
_eventid: Uuid,
) -> Result<DeviceAuthorizationResponse, Oauth2Error> {
Err(Oauth2Error::InvalidGrant)
}
#[instrument(level = "info", skip(self))]
fn check_oauth2_device_code_status(
&mut self,
device_code: &str,
scope: &Option<BTreeSet<String>>,
) -> Result<AccessTokenResponse, Oauth2Error> {
error!(
"haven't done the device grant yet! Got device_code={} scope={:?}",
device_code, scope
);
Err(Oauth2Error::AuthorizationPending)
}
#[instrument(level = "debug", skip_all)]
pub fn check_oauth2_authorise_permit(
&mut self,
ident: &Identity,
consent_token: &str,
ct: Duration,
) -> Result<AuthorisePermitSuccess, OperationError> {
let Some(account_uuid) = ident.get_uuid() else {
error!("consent request ident does not have a valid uuid, unable to proceed");
return Err(OperationError::InvalidSessionState);
};
let consent_req: ConsentToken = self
.oauth2rs
.inner
.fernet
.decrypt_at_time(consent_token, Some(300), ct.as_secs())
.map_err(|_| {
admin_error!("Failed to decrypt consent request");
OperationError::CryptographyError
})
.and_then(|data| {
serde_json::from_slice(&data).map_err(|e| {
admin_error!(err = ?e, "Failed to deserialise consent request");
OperationError::SerdeJsonError
})
})?;
if consent_req.ident_id != ident.get_event_origin_id() {
security_info!("consent request ident id does not match the identity of our UAT.");
return Err(OperationError::InvalidSessionState);
}
if consent_req.session_id != ident.get_session_id() {
security_info!("consent request session id does not match the session id of our UAT.");
return Err(OperationError::InvalidSessionState);
}
let o2rs = self
.oauth2rs
.inner
.rs_set
.get(&consent_req.client_id)
.ok_or_else(|| {
admin_error!("Invalid consent request OAuth2 client_id");
OperationError::InvalidRequestState
})?;
let xchg_code = TokenExchangeCode {
account_uuid,
session_id: ident.get_session_id(),
code_challenge: consent_req.code_challenge,
redirect_uri: consent_req.redirect_uri.clone(),
scopes: consent_req.scopes.clone(),
nonce: consent_req.nonce,
};
let code_data = serde_json::to_vec(&xchg_code).map_err(|e| {
admin_error!(err = ?e, "Unable to encode xchg_code data");
OperationError::SerdeJsonError
})?;
let code = o2rs.token_fernet.encrypt_at_time(&code_data, ct.as_secs());
let modlist = ModifyList::new_list(vec![
Modify::Removed(
Attribute::OAuth2ConsentScopeMap,
PartialValue::Refer(o2rs.uuid),
),
Modify::Present(
Attribute::OAuth2ConsentScopeMap,
Value::OauthScopeMap(o2rs.uuid, consent_req.scopes.iter().cloned().collect()),
),
]);
self.qs_write.internal_modify(
&filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(account_uuid))),
&modlist,
)?;
Ok(AuthorisePermitSuccess {
redirect_uri: consent_req.redirect_uri,
state: consent_req.state,
code,
})
}
#[instrument(level = "debug", skip_all)]
fn check_oauth2_token_exchange_authorization_code(
&mut self,
o2rs: &Oauth2RS,
token_req_code: &str,
token_req_redirect_uri: &Url,
token_req_code_verifier: Option<&str>,
ct: Duration,
) -> Result<AccessTokenResponse, Oauth2Error> {
let code_xchg: TokenExchangeCode = o2rs
.token_fernet
.decrypt_at_time(token_req_code, Some(60), ct.as_secs())
.map_err(|_| {
admin_error!("Failed to decrypt token exchange request");
Oauth2Error::InvalidRequest
})
.and_then(|data| {
serde_json::from_slice(&data).map_err(|e| {
admin_error!("Failed to deserialise token exchange code - {:?}", e);
Oauth2Error::InvalidRequest
})
})?;
if let Some(code_challenge) = code_xchg.code_challenge {
let code_verifier = token_req_code_verifier
.ok_or_else(|| {
security_info!("PKCE code verification failed - code challenge is present, but no verifier was provided");
Oauth2Error::InvalidRequest
})?;
let mut hasher = sha::Sha256::new();
hasher.update(code_verifier.as_bytes());
let code_verifier_hash: Vec<u8> = hasher.finish().to_vec();
if code_challenge != code_verifier_hash {
security_info!(
"PKCE code verification failed - this may indicate malicious activity"
);
return Err(Oauth2Error::InvalidRequest);
}
} else if o2rs.require_pkce() {
security_info!(
"PKCE code verification failed - no code challenge present in PKCE enforced mode"
);
return Err(Oauth2Error::InvalidRequest);
} else if token_req_code_verifier.is_some() {
security_info!(
"PKCE code verification failed - a code verifier is present, but no code challenge in exchange"
);
return Err(Oauth2Error::InvalidRequest);
}
if token_req_redirect_uri != &code_xchg.redirect_uri {
security_info!("Invalid OAuth2 redirect_uri (differs from original request uri)");
return Err(Oauth2Error::InvalidOrigin);
}
let parent_session_id = code_xchg.session_id;
let session_id = Uuid::new_v4();
let scopes = code_xchg.scopes;
let account_uuid = code_xchg.account_uuid;
let nonce = code_xchg.nonce;
self.generate_access_token_response(
o2rs,
ct,
scopes,
account_uuid,
parent_session_id,
session_id,
nonce,
)
}
#[instrument(level = "debug", skip_all)]
fn check_oauth2_token_refresh(
&mut self,
o2rs: &Oauth2RS,
refresh_token: &str,
req_scopes: Option<&BTreeSet<String>>,
ct: Duration,
) -> Result<AccessTokenResponse, Oauth2Error> {
let token: Oauth2TokenType = o2rs
.token_fernet
.decrypt(refresh_token)
.map_err(|_| {
admin_error!("Failed to decrypt refresh token request");
Oauth2Error::InvalidRequest
})
.and_then(|data| {
serde_json::from_slice(&data).map_err(|e| {
admin_error!("Failed to deserialise token - {:?}", e);
Oauth2Error::InvalidRequest
})
})?;
match token {
Oauth2TokenType::ClientAccess { .. } => {
admin_error!("attempt to refresh with an access token");
Err(Oauth2Error::InvalidRequest)
}
Oauth2TokenType::Refresh {
scopes,
parent_session_id,
session_id,
exp,
uuid,
iat,
nbf: _,
nonce,
} => {
if exp <= ct.as_secs() as i64 {
security_info!(?uuid, "refresh token has expired, ");
return Err(Oauth2Error::InvalidGrant);
}
let valid = self
.check_oauth2_account_uuid_valid(
uuid,
session_id,
Some(parent_session_id),
iat,
ct,
)
.map_err(|_| admin_error!("Account is not valid"));
let Ok(Some(entry)) = valid else {
security_info!(
?uuid,
"access token has no account not valid, returning inactive"
);
return Err(Oauth2Error::InvalidGrant);
};
let oauth2_session = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
.and_then(|map| map.get(&session_id))
.ok_or_else(|| {
security_info!(
?session_id,
"No OAuth2 session found, unable to proceed with refresh"
);
Oauth2Error::InvalidGrant
})?;
if iat < oauth2_session.issued_at.unix_timestamp() {
security_info!(
?session_id,
"Attempt to reuse a refresh token detected, destroying session"
);
let modlist = ModifyList::new_list(vec![Modify::Removed(
Attribute::OAuth2Session,
PartialValue::Refer(session_id),
)]);
self.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(uuid))),
&modlist,
)
.map_err(|e| {
admin_error!("Failed to modify - revoke OAuth2 session {:?}", e);
Oauth2Error::ServerError(e)
})?;
return Err(Oauth2Error::InvalidGrant);
}
let update_scopes = if let Some(req_scopes) = req_scopes {
if req_scopes.is_subset(&scopes) {
debug!("oauth2 scopes requested, checked as valid.");
req_scopes.clone()
} else {
warn!("oauth2 scopes requested, invalid.");
return Err(Oauth2Error::InvalidScope);
}
} else {
debug!("No OAuth2 scopes requested, this is valid.");
scopes
};
let account_uuid = uuid;
self.generate_access_token_response(
o2rs,
ct,
update_scopes,
account_uuid,
parent_session_id,
session_id,
nonce,
)
}
}
}
#[instrument(level = "debug", skip_all)]
fn check_oauth2_token_client_credentials(
&mut self,
o2rs: &Oauth2RS,
req_scopes: Option<&BTreeSet<String>>,
ct: Duration,
) -> Result<AccessTokenResponse, Oauth2Error> {
let req_scopes = req_scopes.cloned().unwrap_or_default();
validate_scopes(&req_scopes)?;
let avail_scopes: Vec<String> = req_scopes
.intersection(&o2rs.client_scopes)
.map(|s| s.to_string())
.collect();
if avail_scopes.len() != req_scopes.len() {
admin_warn!(
ident = %o2rs.name,
requested_scopes = ?req_scopes,
available_scopes = ?o2rs.client_scopes,
"Client does not have access to the requested scopes"
);
return Err(Oauth2Error::AccessDenied);
}
let granted_scopes = avail_scopes
.into_iter()
.chain(o2rs.client_sup_scopes.iter().cloned())
.collect::<BTreeSet<_>>();
let odt_ct = OffsetDateTime::UNIX_EPOCH + ct;
let iat = ct.as_secs() as i64;
let exp = iat + OAUTH2_ACCESS_TOKEN_EXPIRY as i64;
let odt_exp = odt_ct + Duration::from_secs(OAUTH2_ACCESS_TOKEN_EXPIRY as u64);
let expires_in = OAUTH2_ACCESS_TOKEN_EXPIRY;
let session_id = Uuid::new_v4();
let scope = granted_scopes.clone();
let uuid = o2rs.uuid;
let access_token_raw = Oauth2TokenType::ClientAccess {
scopes: granted_scopes,
session_id,
uuid,
exp,
iat,
nbf: iat,
};
let access_token_data = serde_json::to_vec(&access_token_raw).map_err(|e| {
admin_error!(err = ?e, "Unable to encode token data");
Oauth2Error::ServerError(OperationError::SerdeJsonError)
})?;
let access_token = o2rs
.token_fernet
.encrypt_at_time(&access_token_data, ct.as_secs());
let session = Value::Oauth2Session(
session_id,
Oauth2Session {
parent: None,
state: SessionState::ExpiresAt(odt_exp),
issued_at: odt_ct,
rs_uuid: o2rs.uuid,
},
);
let modlist =
ModifyList::new_list(vec![Modify::Present(Attribute::OAuth2Session, session)]);
self.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(uuid))),
&modlist,
)
.map_err(|e| {
admin_error!("Failed to persist OAuth2 session record {:?}", e);
Oauth2Error::ServerError(e)
})?;
Ok(AccessTokenResponse {
access_token,
token_type: AccessTokenType::Bearer,
expires_in,
refresh_token: None,
scope,
id_token: None,
})
}
fn generate_access_token_response(
&mut self,
o2rs: &Oauth2RS,
ct: Duration,
scopes: BTreeSet<String>,
account_uuid: Uuid,
parent_session_id: Uuid,
session_id: Uuid,
nonce: Option<String>,
) -> Result<AccessTokenResponse, Oauth2Error> {
let odt_ct = OffsetDateTime::UNIX_EPOCH + ct;
let iat = ct.as_secs() as i64;
let expiry = odt_ct + Duration::from_secs(OAUTH2_ACCESS_TOKEN_EXPIRY as u64);
let expires_in = OAUTH2_ACCESS_TOKEN_EXPIRY;
let refresh_expiry = iat + OAUTH_REFRESH_TOKEN_EXPIRY as i64;
let odt_refresh_expiry = odt_ct + Duration::from_secs(OAUTH_REFRESH_TOKEN_EXPIRY);
let scope = scopes.clone();
let iss = o2rs.iss.clone();
let exp = expiry.unix_timestamp();
let aud = o2rs.name.clone();
let client_id = o2rs.name.clone();
let id_token = if scopes.contains(OAUTH2_SCOPE_OPENID) {
let amr = None;
let entry = match self.qs_write.internal_search_uuid(account_uuid) {
Ok(entry) => entry,
Err(err) => return Err(Oauth2Error::ServerError(err)),
};
let account = match Account::try_from_entry_rw(&entry, &mut self.qs_write) {
Ok(account) => account,
Err(err) => return Err(Oauth2Error::ServerError(err)),
};
let s_claims = s_claims_for_account(o2rs, &account, &scopes);
let extra_claims = extra_claims_for_account(&account, &o2rs.claim_map, &scopes);
let oidc = OidcToken {
iss: iss.clone(),
sub: OidcSubject::U(account_uuid),
aud: aud.clone(),
iat,
nbf: Some(iat),
exp,
auth_time: None,
nonce: nonce.clone(),
at_hash: None,
acr: None,
amr,
azp: Some(o2rs.name.clone()),
jti: None,
s_claims,
claims: extra_claims,
};
trace!(?oidc);
let jwt_signed = match &o2rs.jws_signer {
Oauth2JwsSigner::ES256 { signer } => signer.sign(&oidc),
Oauth2JwsSigner::RS256 { signer } => signer.sign(&oidc),
}
.map_err(|e| {
admin_error!(err = ?e, "Unable to encode uat data");
Oauth2Error::ServerError(OperationError::InvalidState)
})?;
Some(jwt_signed.to_string())
} else {
None
};
let access_token_data = OAuth2RFC9068Token {
iss: iss.to_string(),
sub: account_uuid,
aud,
exp,
nbf: iat,
iat,
jti: None,
client_id,
extensions: OAuth2RFC9068TokenExtensions {
auth_time: None,
acr: None,
amr: None,
scope: scopes.clone(),
nonce: nonce.clone(),
session_id,
parent_session_id: Some(parent_session_id),
},
};
let access_token_data = JwsBuilder::into_json(&access_token_data)
.map(|builder| builder.set_typ(Some("at+jwt")).build())
.map_err(|e| {
admin_error!(err = ?e, "Unable to encode access token data");
Oauth2Error::ServerError(OperationError::InvalidState)
})?;
let access_token = match &o2rs.jws_signer {
Oauth2JwsSigner::ES256 { signer } => signer.sign(&access_token_data),
Oauth2JwsSigner::RS256 { signer } => signer.sign(&access_token_data),
}
.map_err(|e| {
admin_error!(err = ?e, "Unable to sign access token data");
Oauth2Error::ServerError(OperationError::InvalidState)
})?;
let refresh_token_raw = Oauth2TokenType::Refresh {
scopes,
parent_session_id,
session_id,
exp: refresh_expiry,
uuid: account_uuid,
iat,
nbf: iat,
nonce,
};
let refresh_token_data = serde_json::to_vec(&refresh_token_raw).map_err(|e| {
admin_error!(err = ?e, "Unable to encode token data");
Oauth2Error::ServerError(OperationError::SerdeJsonError)
})?;
let refresh_token = o2rs
.token_fernet
.encrypt_at_time(&refresh_token_data, ct.as_secs());
let session = Value::Oauth2Session(
session_id,
Oauth2Session {
parent: Some(parent_session_id),
state: SessionState::ExpiresAt(odt_refresh_expiry),
issued_at: odt_ct,
rs_uuid: o2rs.uuid,
},
);
let modlist = ModifyList::new_list(vec![
Modify::Present(Attribute::OAuth2Session, session),
]);
self.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(account_uuid))),
&modlist,
)
.map_err(|e| {
admin_error!("Failed to persist OAuth2 session record {:?}", e);
Oauth2Error::ServerError(e)
})?;
Ok(AccessTokenResponse {
access_token: access_token.to_string(),
token_type: AccessTokenType::Bearer,
expires_in,
refresh_token: Some(refresh_token),
scope,
id_token,
})
}
#[cfg(test)]
fn reflect_oauth2_token(
&mut self,
client_auth_info: &ClientAuthInfo,
token: &str,
) -> Result<Oauth2TokenType, OperationError> {
let Some(client_authz) = client_auth_info.basic_authz.as_ref() else {
admin_warn!("OAuth2 client_id not provided by basic authz");
return Err(OperationError::InvalidSessionState);
};
let (client_id, secret) = parse_basic_authz(client_authz.as_str()).map_err(|_| {
admin_warn!("Invalid client_authz base64");
OperationError::InvalidSessionState
})?;
let o2rs = self.oauth2rs.inner.rs_set.get(&client_id).ok_or_else(|| {
admin_warn!("Invalid OAuth2 client_id");
OperationError::InvalidSessionState
})?;
if let OauthRSType::Basic { authz_secret, .. } = &o2rs.type_ {
if o2rs.is_basic() && authz_secret != &secret {
security_info!("Invalid OAuth2 secret for client_id={}", client_id);
return Err(OperationError::InvalidSessionState);
}
}
o2rs.token_fernet
.decrypt(token)
.map_err(|_| {
admin_error!("Failed to decrypt token reflection request");
OperationError::CryptographyError
})
.and_then(|data| {
serde_json::from_slice(&data).map_err(|e| {
admin_error!("Failed to deserialise token exchange code - {:?}", e);
OperationError::SerdeJsonError
})
})
}
}
impl IdmServerProxyReadTransaction<'_> {
#[instrument(level = "debug", skip_all)]
pub fn check_oauth2_authorisation(
&self,
maybe_ident: Option<&Identity>,
auth_req: &AuthorisationRequest,
ct: Duration,
) -> Result<AuthoriseResponse, Oauth2Error> {
trace!(?auth_req);
if auth_req.response_type != "code" {
admin_warn!("Invalid OAuth2 response_type (should be 'code')");
return Err(Oauth2Error::UnsupportedResponseType);
}
let o2rs = self
.oauth2rs
.inner
.rs_set
.get(&auth_req.client_id)
.ok_or_else(|| {
admin_warn!(
"Invalid OAuth2 client_id ({}) Have you configured the OAuth2 resource server?",
&auth_req.client_id
);
Oauth2Error::InvalidClientId
})?;
if o2rs.type_.allow_localhost_redirect() && check_is_loopback(&auth_req.redirect_uri) {
debug!("Loopback redirect_uri detected, allowing for localhost");
} else {
let origin_uri_matched =
!o2rs.strict_redirect_uri && o2rs.origins.contains(&auth_req.redirect_uri.origin());
let strict_redirect_uri_matched =
o2rs.strict_redirect_uri && o2rs.redirect_uris.contains(&auth_req.redirect_uri);
let opaque_origin_matched = o2rs.opaque_origins.contains(&auth_req.redirect_uri);
if !(strict_redirect_uri_matched || origin_uri_matched || opaque_origin_matched) {
if o2rs.strict_redirect_uri {
warn!(
"Invalid OAuth2 redirect_uri (must be an exact match to a redirect-url) - got {}",
auth_req.redirect_uri.as_str()
);
} else {
warn!(
"Invalid OAuth2 redirect_uri (must be related to origin) - got {:?}",
auth_req.redirect_uri.origin()
);
}
return Err(Oauth2Error::InvalidOrigin);
}
if (o2rs.origin_https_required && auth_req.redirect_uri.scheme() != "https")
&& !opaque_origin_matched
{
admin_warn!(
"Invalid OAuth2 redirect_uri scheme (must be https for secure origin) - got {}",
auth_req.redirect_uri.to_string()
);
return Err(Oauth2Error::InvalidOrigin);
}
}
let code_challenge = if let Some(pkce_request) = &auth_req.pkce_request {
if !o2rs.require_pkce() {
security_info!(?o2rs.name, "Insecure OAuth2 client configuration - PKCE is not enforced, but client is requesting it!");
}
if pkce_request.code_challenge_method != CodeChallengeMethod::S256 {
admin_warn!("Invalid OAuth2 code_challenge_method (must be 'S256')");
return Err(Oauth2Error::InvalidRequest);
}
Some(pkce_request.code_challenge.clone())
} else if o2rs.require_pkce() {
security_error!(?o2rs.name, "No PKCE code challenge was provided with client in enforced PKCE mode.");
return Err(Oauth2Error::InvalidRequest);
} else {
security_info!(?o2rs.name, "Insecure client configuration - PKCE is not enforced.");
None
};
let Some(ident) = maybe_ident else {
debug!("No identity available, assume authentication required");
return Ok(AuthoriseResponse::AuthenticationRequired {
client_name: o2rs.displayname.clone(),
login_hint: auth_req.oidc_ext.login_hint.clone(),
});
};
let Some(account_uuid) = ident.get_uuid() else {
error!("Consent request ident does not have a valid UUID, unable to proceed");
return Err(Oauth2Error::InvalidRequest);
};
if account_uuid == UUID_ANONYMOUS {
admin_error!(
"Invalid OAuth2 request - refusing to allow user that authenticated with anonymous"
);
return Err(Oauth2Error::AccessDenied);
}
let req_scopes: BTreeSet<String> = auth_req.scope.clone();
if req_scopes.is_empty() {
admin_error!("Invalid OAuth2 request - must contain at least one requested scope");
return Err(Oauth2Error::InvalidRequest);
}
validate_scopes(&req_scopes)?;
let uat_scopes: BTreeSet<String> = o2rs
.scope_maps
.iter()
.filter_map(|(u, m)| {
if ident.is_memberof(*u) {
Some(m.iter())
} else {
None
}
})
.flatten()
.cloned()
.collect();
let avail_scopes: Vec<String> = req_scopes
.intersection(&uat_scopes)
.map(|s| s.to_string())
.collect();
debug!(?o2rs.scope_maps);
if avail_scopes.len() != req_scopes.len() {
admin_warn!(
%ident,
requested_scopes = ?req_scopes,
available_scopes = ?uat_scopes,
"Identity does not have access to the requested scopes"
);
return Err(Oauth2Error::AccessDenied);
}
drop(avail_scopes);
let openid_requested = req_scopes.contains(OAUTH2_SCOPE_OPENID);
let granted_scopes: BTreeSet<String> = o2rs
.sup_scope_maps
.iter()
.filter_map(|(u, m)| {
if ident.is_memberof(*u) {
Some(m.iter())
} else {
None
}
})
.flatten()
.cloned()
.chain(req_scopes)
.collect();
let consent_previously_granted =
if let Some(consent_scopes) = ident.get_oauth2_consent_scopes(o2rs.uuid) {
trace!(?granted_scopes);
trace!(?consent_scopes);
granted_scopes.eq(consent_scopes)
} else {
false
};
let session_id = ident.get_session_id();
if consent_previously_granted {
if event_enabled!(tracing::Level::DEBUG) {
let pretty_scopes: Vec<String> =
granted_scopes.iter().map(|s| s.to_owned()).collect();
debug!(
"User has previously consented, permitting with scopes: {}",
pretty_scopes.join(",")
);
}
let xchg_code = TokenExchangeCode {
account_uuid,
session_id,
code_challenge,
redirect_uri: auth_req.redirect_uri.clone(),
scopes: granted_scopes.into_iter().collect(),
nonce: auth_req.nonce.clone(),
};
let code_data = serde_json::to_vec(&xchg_code).map_err(|e| {
admin_error!(err = ?e, "Unable to encode xchg_code data");
Oauth2Error::ServerError(OperationError::SerdeJsonError)
})?;
let code = o2rs.token_fernet.encrypt_at_time(&code_data, ct.as_secs());
Ok(AuthoriseResponse::Permitted(AuthorisePermitSuccess {
redirect_uri: auth_req.redirect_uri.clone(),
state: auth_req.state.clone(),
code,
}))
} else {
let mut pii_scopes = BTreeSet::default();
if openid_requested {
if granted_scopes.contains(OAUTH2_SCOPE_EMAIL) {
pii_scopes.insert(OAUTH2_SCOPE_EMAIL.to_string());
pii_scopes.insert("email_verified".to_string());
}
};
let consent_req = ConsentToken {
client_id: auth_req.client_id.clone(),
ident_id: ident.get_event_origin_id(),
session_id,
state: auth_req.state.clone(),
code_challenge,
redirect_uri: auth_req.redirect_uri.clone(),
scopes: granted_scopes.iter().cloned().collect(),
nonce: auth_req.nonce.clone(),
};
let consent_data = serde_json::to_vec(&consent_req).map_err(|e| {
admin_error!(err = ?e, "Unable to encode consent data");
Oauth2Error::ServerError(OperationError::SerdeJsonError)
})?;
let consent_token = self
.oauth2rs
.inner
.fernet
.encrypt_at_time(&consent_data, ct.as_secs());
Ok(AuthoriseResponse::ConsentRequested {
client_name: o2rs.displayname.clone(),
scopes: granted_scopes.into_iter().collect(),
pii_scopes,
consent_token,
})
}
}
#[instrument(level = "debug", skip_all)]
pub fn check_oauth2_authorise_reject(
&self,
ident: &Identity,
consent_token: &str,
ct: Duration,
) -> Result<Url, OperationError> {
let consent_req: ConsentToken = self
.oauth2rs
.inner
.fernet
.decrypt_at_time(consent_token, Some(300), ct.as_secs())
.map_err(|_| {
admin_error!("Failed to decrypt consent request");
OperationError::CryptographyError
})
.and_then(|data| {
serde_json::from_slice(&data).map_err(|e| {
admin_error!(err = ?e, "Failed to deserialise consent request");
OperationError::SerdeJsonError
})
})?;
if consent_req.ident_id != ident.get_event_origin_id() {
security_info!("consent request ident id does not match the identity of our UAT.");
return Err(OperationError::InvalidSessionState);
}
if consent_req.session_id != ident.get_session_id() {
security_info!("consent request sessien id does not match the session id of our UAT.");
return Err(OperationError::InvalidSessionState);
}
let _o2rs = self
.oauth2rs
.inner
.rs_set
.get(&consent_req.client_id)
.ok_or_else(|| {
admin_error!("Invalid consent request OAuth2 client_id");
OperationError::InvalidRequestState
})?;
Ok(consent_req.redirect_uri)
}
#[instrument(level = "debug", skip_all)]
pub fn check_oauth2_token_introspect(
&mut self,
client_auth_info: &ClientAuthInfo,
intr_req: &AccessTokenIntrospectRequest,
ct: Duration,
) -> Result<AccessTokenIntrospectResponse, Oauth2Error> {
let Some(client_authz) = client_auth_info.basic_authz.as_ref() else {
admin_warn!("OAuth2 client_id not provided by basic authz");
return Err(Oauth2Error::AuthenticationRequired);
};
let (client_id, secret) = parse_basic_authz(client_authz.as_str())?;
let o2rs = self.oauth2rs.inner.rs_set.get(&client_id).ok_or_else(|| {
admin_warn!("Invalid OAuth2 client_id");
Oauth2Error::AuthenticationRequired
})?;
match &o2rs.type_ {
OauthRSType::Basic { authz_secret, .. } => {
if authz_secret != &secret {
security_info!("Invalid OAuth2 client_id secret");
return Err(Oauth2Error::AuthenticationRequired);
}
}
OauthRSType::Public { .. } => {}
};
let prefer_short_username = o2rs.prefer_short_username;
if let Ok(jwsc) = JwsCompact::from_str(&intr_req.token) {
let access_token = match &o2rs.jws_signer {
Oauth2JwsSigner::ES256 { signer } => signer
.get_verifier()
.and_then(|verifier| verifier.verify(&jwsc)),
Oauth2JwsSigner::RS256 { signer } => signer
.get_verifier()
.and_then(|verifier| verifier.verify(&jwsc)),
}
.map_err(|err| {
admin_error!(?err, "Unable to verify access token");
Oauth2Error::InvalidRequest
})
.and_then(|jws| {
jws.from_json().map_err(|err| {
admin_error!(?err, "Unable to deserialise access token");
Oauth2Error::InvalidRequest
})
})?;
let OAuth2RFC9068Token::<_> {
iss: _,
sub,
aud: _,
exp,
nbf,
iat,
jti: _,
client_id: _,
extensions:
OAuth2RFC9068TokenExtensions {
auth_time: _,
acr: _,
amr: _,
scope: scopes,
nonce: _,
session_id,
parent_session_id,
},
} = access_token;
if exp <= ct.as_secs() as i64 {
security_info!(?sub, "access token has expired, returning inactive");
return Ok(AccessTokenIntrospectResponse::inactive());
}
let valid = self
.check_oauth2_account_uuid_valid(sub, session_id, parent_session_id, iat, ct)
.map_err(|_| admin_error!("Account is not valid"));
let Ok(Some(entry)) = valid else {
security_info!(
?sub,
"access token account is not valid, returning inactive"
);
return Ok(AccessTokenIntrospectResponse::inactive());
};
let account = match Account::try_from_entry_ro(&entry, &mut self.qs_read) {
Ok(account) => account,
Err(err) => return Err(Oauth2Error::ServerError(err)),
};
let scope = scopes.clone();
let preferred_username = if prefer_short_username {
Some(account.name.clone())
} else {
Some(account.spn.clone())
};
let token_type = Some(AccessTokenType::Bearer);
Ok(AccessTokenIntrospectResponse {
active: true,
scope,
client_id: Some(client_id.clone()),
username: preferred_username,
token_type,
iat: Some(iat),
exp: Some(exp),
nbf: Some(nbf),
sub: Some(sub.to_string()),
aud: Some(client_id),
iss: None,
jti: None,
})
} else {
let token: Oauth2TokenType = o2rs
.token_fernet
.decrypt(&intr_req.token)
.map_err(|_| {
admin_error!("Failed to decrypt token introspection request");
Oauth2Error::InvalidRequest
})
.and_then(|data| {
serde_json::from_slice(&data).map_err(|e| {
admin_error!("Failed to deserialise token - {:?}", e);
Oauth2Error::InvalidRequest
})
})?;
match token {
Oauth2TokenType::ClientAccess {
scopes,
session_id,
uuid,
exp,
iat,
nbf,
} => {
if exp <= ct.as_secs() as i64 {
security_info!(?uuid, "access token has expired, returning inactive");
return Ok(AccessTokenIntrospectResponse::inactive());
}
let valid = self
.check_oauth2_account_uuid_valid(uuid, session_id, None, iat, ct)
.map_err(|_| admin_error!("Account is not valid"));
let Ok(Some(entry)) = valid else {
security_info!(
?uuid,
"access token account is not valid, returning inactive"
);
return Ok(AccessTokenIntrospectResponse::inactive());
};
let scope = scopes.clone();
let token_type = Some(AccessTokenType::Bearer);
let username = if prefer_short_username {
entry
.get_ava_single_iname(Attribute::Name)
.map(|s| s.to_string())
} else {
entry.get_ava_single_proto_string(Attribute::Spn)
};
Ok(AccessTokenIntrospectResponse {
active: true,
scope,
client_id: Some(client_id.clone()),
username,
token_type,
iat: Some(iat),
exp: Some(exp),
nbf: Some(nbf),
sub: Some(uuid.to_string()),
aud: Some(client_id),
iss: None,
jti: None,
})
}
Oauth2TokenType::Refresh { .. } => Ok(AccessTokenIntrospectResponse::inactive()),
}
}
}
#[instrument(level = "debug", skip_all)]
pub fn oauth2_openid_userinfo(
&mut self,
client_id: &str,
token: JwsCompact,
ct: Duration,
) -> Result<OidcToken, Oauth2Error> {
let o2rs: &Oauth2RS = unsafe {
let s = self.oauth2rs.inner.rs_set.get(client_id).ok_or_else(|| {
admin_warn!(
"Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)"
);
Oauth2Error::InvalidClientId
})?;
&*(s as *const _)
};
let access_token = match &o2rs.jws_signer {
Oauth2JwsSigner::ES256 { signer } => signer
.get_verifier()
.and_then(|verifier| verifier.verify(&token)),
Oauth2JwsSigner::RS256 { signer } => signer
.get_verifier()
.and_then(|verifier| verifier.verify(&token)),
}
.map_err(|err| {
admin_error!(?err, "Unable to verify access token");
Oauth2Error::InvalidRequest
})
.and_then(|jws| {
jws.from_json().map_err(|err| {
admin_error!(?err, "Unable to deserialise access token");
Oauth2Error::InvalidRequest
})
})?;
let OAuth2RFC9068Token::<_> {
iss: _,
sub,
aud: _,
exp,
nbf,
iat,
jti: _,
client_id: _,
extensions:
OAuth2RFC9068TokenExtensions {
auth_time: _,
acr: _,
amr: _,
scope: scopes,
nonce,
session_id,
parent_session_id,
},
} = access_token;
if exp <= ct.as_secs() as i64 {
security_info!(?sub, "access token has expired, returning inactive");
return Err(Oauth2Error::InvalidToken);
}
let valid = self
.check_oauth2_account_uuid_valid(sub, session_id, parent_session_id, iat, ct)
.map_err(|_| admin_error!("Account is not valid"));
let Ok(Some(entry)) = valid else {
security_info!(
?sub,
"access token has account not valid, returning inactive"
);
return Err(Oauth2Error::InvalidToken);
};
let account = match Account::try_from_entry_ro(&entry, &mut self.qs_read) {
Ok(account) => account,
Err(err) => return Err(Oauth2Error::ServerError(err)),
};
let amr = None;
let iss = o2rs.iss.clone();
let s_claims = s_claims_for_account(o2rs, &account, &scopes);
let extra_claims = extra_claims_for_account(&account, &o2rs.claim_map, &scopes);
Ok(OidcToken {
iss,
sub: OidcSubject::U(sub),
aud: client_id.to_string(),
iat,
nbf: Some(nbf),
exp,
auth_time: None,
nonce,
at_hash: None,
acr: None,
amr,
azp: Some(client_id.to_string()),
jti: None,
s_claims,
claims: extra_claims,
})
}
#[instrument(level = "debug", skip_all)]
pub fn oauth2_rfc8414_metadata(
&self,
client_id: &str,
) -> Result<Oauth2Rfc8414MetadataResponse, OperationError> {
let o2rs = self.oauth2rs.inner.rs_set.get(client_id).ok_or_else(|| {
admin_warn!(
"Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)"
);
OperationError::NoMatchingEntries
})?;
let issuer = o2rs.iss.clone();
let authorization_endpoint = o2rs.authorization_endpoint.clone();
let token_endpoint = o2rs.token_endpoint.clone();
let revocation_endpoint = Some(o2rs.revocation_endpoint.clone());
let introspection_endpoint = Some(o2rs.introspection_endpoint.clone());
let jwks_uri = Some(o2rs.jwks_uri.clone());
let scopes_supported = Some(o2rs.scopes_supported.iter().cloned().collect());
let response_types_supported = vec![ResponseType::Code];
let response_modes_supported = vec![ResponseMode::Query];
let grant_types_supported = vec![GrantType::AuthorisationCode];
let token_endpoint_auth_methods_supported = vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost,
];
let revocation_endpoint_auth_methods_supported = vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost,
];
let introspection_endpoint_auth_methods_supported = vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost,
];
let service_documentation = Some(URL_SERVICE_DOCUMENTATION.clone());
let code_challenge_methods_supported = if o2rs.require_pkce() {
vec![PkceAlg::S256]
} else {
Vec::with_capacity(0)
};
Ok(Oauth2Rfc8414MetadataResponse {
issuer,
authorization_endpoint,
token_endpoint,
jwks_uri,
registration_endpoint: None,
scopes_supported,
response_types_supported,
response_modes_supported,
grant_types_supported,
token_endpoint_auth_methods_supported,
token_endpoint_auth_signing_alg_values_supported: None,
service_documentation,
ui_locales_supported: None,
op_policy_uri: None,
op_tos_uri: None,
revocation_endpoint,
revocation_endpoint_auth_methods_supported,
introspection_endpoint,
introspection_endpoint_auth_methods_supported,
introspection_endpoint_auth_signing_alg_values_supported: None,
code_challenge_methods_supported,
})
}
#[instrument(level = "debug", skip_all)]
pub fn oauth2_openid_discovery(
&self,
client_id: &str,
) -> Result<OidcDiscoveryResponse, OperationError> {
let o2rs = self.oauth2rs.inner.rs_set.get(client_id).ok_or_else(|| {
admin_warn!(
"Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)"
);
OperationError::NoMatchingEntries
})?;
let issuer = o2rs.iss.clone();
let authorization_endpoint = o2rs.authorization_endpoint.clone();
let token_endpoint = o2rs.token_endpoint.clone();
let userinfo_endpoint = Some(o2rs.userinfo_endpoint.clone());
let jwks_uri = o2rs.jwks_uri.clone();
let scopes_supported = Some(o2rs.scopes_supported.iter().cloned().collect());
let response_types_supported = vec![ResponseType::Code];
let response_modes_supported = vec![ResponseMode::Query];
let grant_types_supported = vec![GrantType::AuthorisationCode];
let subject_types_supported = vec![SubjectType::Public];
let id_token_signing_alg_values_supported = match &o2rs.jws_signer {
Oauth2JwsSigner::ES256 { .. } => vec![IdTokenSignAlg::ES256],
Oauth2JwsSigner::RS256 { .. } => vec![IdTokenSignAlg::RS256],
};
let userinfo_signing_alg_values_supported = None;
let token_endpoint_auth_methods_supported = vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost,
];
let display_values_supported = Some(vec![DisplayValue::Page]);
let claim_types_supported = vec![ClaimType::Normal];
let claims_supported = None;
let service_documentation = Some(URL_SERVICE_DOCUMENTATION.clone());
let code_challenge_methods_supported = if o2rs.require_pkce() {
vec![PkceAlg::S256]
} else {
Vec::with_capacity(0)
};
let revocation_endpoint = Some(o2rs.revocation_endpoint.clone());
let revocation_endpoint_auth_methods_supported = vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost,
];
let introspection_endpoint = Some(o2rs.introspection_endpoint.clone());
let introspection_endpoint_auth_methods_supported = vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost,
];
Ok(OidcDiscoveryResponse {
issuer,
authorization_endpoint,
token_endpoint,
userinfo_endpoint,
jwks_uri,
registration_endpoint: None,
scopes_supported,
response_types_supported,
response_modes_supported,
grant_types_supported,
acr_values_supported: None,
subject_types_supported,
id_token_signing_alg_values_supported,
id_token_encryption_alg_values_supported: None,
id_token_encryption_enc_values_supported: None,
userinfo_signing_alg_values_supported,
userinfo_encryption_alg_values_supported: None,
userinfo_encryption_enc_values_supported: None,
request_object_signing_alg_values_supported: None,
request_object_encryption_alg_values_supported: None,
request_object_encryption_enc_values_supported: None,
token_endpoint_auth_methods_supported,
token_endpoint_auth_signing_alg_values_supported: None,
display_values_supported,
claim_types_supported,
claims_supported,
service_documentation,
claims_locales_supported: None,
ui_locales_supported: None,
claims_parameter_supported: false,
request_parameter_supported: false,
request_uri_parameter_supported: false,
require_request_uri_registration: false,
op_policy_uri: None,
op_tos_uri: None,
code_challenge_methods_supported,
revocation_endpoint,
revocation_endpoint_auth_methods_supported,
introspection_endpoint,
introspection_endpoint_auth_methods_supported,
introspection_endpoint_auth_signing_alg_values_supported: None,
device_authorization_endpoint: o2rs.device_authorization_endpoint.clone(),
})
}
#[instrument(level = "debug", skip_all)]
pub fn oauth2_openid_publickey(&self, client_id: &str) -> Result<JwkKeySet, OperationError> {
let o2rs = self.oauth2rs.inner.rs_set.get(client_id).ok_or_else(|| {
admin_warn!(
"Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)"
);
OperationError::NoMatchingEntries
})?;
match &o2rs.jws_signer {
Oauth2JwsSigner::ES256 { signer } => signer.public_key_as_jwk(),
Oauth2JwsSigner::RS256 { signer } => signer.public_key_as_jwk(),
}
.map_err(|e| {
admin_error!("Unable to retrieve public key for {} - {:?}", o2rs.name, e);
OperationError::InvalidState
})
.map(|jwk| JwkKeySet { keys: vec![jwk] })
}
}
fn parse_basic_authz(client_authz: &str) -> Result<(String, String), Oauth2Error> {
let authz = general_purpose::STANDARD
.decode(client_authz)
.map_err(|_| {
admin_error!("Basic authz invalid base64");
Oauth2Error::AuthenticationRequired
})
.and_then(|data| {
String::from_utf8(data).map_err(|_| {
admin_error!("Basic authz invalid utf8");
Oauth2Error::AuthenticationRequired
})
})?;
let mut split_iter = authz.split(':');
let client_id = split_iter.next().ok_or_else(|| {
admin_error!("Basic authz invalid format (corrupt input?)");
Oauth2Error::AuthenticationRequired
})?;
let secret = split_iter.next().ok_or_else(|| {
admin_error!("Basic authz invalid format (missing ':' separator?)");
Oauth2Error::AuthenticationRequired
})?;
Ok((client_id.to_string(), secret.to_string()))
}
fn s_claims_for_account(
o2rs: &Oauth2RS,
account: &Account,
scopes: &BTreeSet<String>,
) -> OidcClaims {
let preferred_username = if o2rs.prefer_short_username {
Some(account.name.clone())
} else {
Some(account.spn.clone())
};
let (email, email_verified) = if scopes.contains(OAUTH2_SCOPE_EMAIL) {
if let Some(mp) = &account.mail_primary {
(Some(mp.clone()), Some(true))
} else {
(None, None)
}
} else {
(None, None)
};
OidcClaims {
name: Some(account.displayname.clone()),
scopes: scopes.iter().cloned().collect(),
preferred_username,
email,
email_verified,
..Default::default()
}
}
fn extra_claims_for_account(
account: &Account,
claim_map: &BTreeMap<Uuid, Vec<(String, ClaimValue)>>,
scopes: &BTreeSet<String>,
) -> BTreeMap<String, serde_json::Value> {
let mut extra_claims = BTreeMap::new();
let mut account_claims: BTreeMap<&str, ClaimValue> = BTreeMap::new();
for group_uuid in account.groups.iter().map(|g| g.uuid()) {
if let Some(claim) = claim_map.get(group_uuid) {
for (claim_name, claim_value) in claim.iter() {
match account_claims.entry(claim_name.as_str()) {
BTreeEntry::Vacant(e) => {
e.insert(claim_value.clone());
}
BTreeEntry::Occupied(mut e) => {
let mut_claim_value = e.get_mut();
mut_claim_value.merge(claim_value);
}
}
}
}
}
for (claim_name, claim_value) in account_claims {
extra_claims.insert(claim_name.to_string(), claim_value.to_json_value());
}
if scopes.contains("groups") {
extra_claims.insert(
"groups".to_string(),
account
.groups
.iter()
.flat_map(|x| {
let proto_group = x.to_proto();
[proto_group.spn, proto_group.uuid]
})
.collect(),
);
}
trace!(?extra_claims);
extra_claims
}
fn validate_scopes(req_scopes: &BTreeSet<String>) -> Result<(), Oauth2Error> {
let failed_scopes = req_scopes
.iter()
.filter(|&s| !OAUTHSCOPE_RE.is_match(s))
.cloned()
.collect::<Vec<String>>();
if !failed_scopes.is_empty() {
let requested_scopes_string = req_scopes
.iter()
.cloned()
.collect::<Vec<String>>()
.join(",");
admin_error!(
"Invalid OAuth2 request - requested scopes ({}) but ({}) failed to pass validation rules - all must match the regex {}",
requested_scopes_string,
failed_scopes.join(","),
OAUTHSCOPE_RE.as_str()
);
return Err(Oauth2Error::InvalidScope);
}
Ok(())
}
#[inline]
#[cfg(any(feature = "dev-oauth2-device-flow", test))]
#[allow(dead_code)]
fn gen_device_code() -> Result<[u8; 16], Oauth2Error> {
let mut rng = rand::thread_rng();
let mut result = [0u8; 16];
use rand::Rng;
if let Err(err) = rng.try_fill(&mut result) {
error!("Failed to generate device code! {:?}", err);
return Err(Oauth2Error::ServerError(OperationError::Backend));
}
Ok(result)
}
#[inline]
#[cfg(any(feature = "dev-oauth2-device-flow", test))]
#[allow(dead_code)]
fn gen_user_code() -> (String, u32) {
use rand::Rng;
let mut rng = rand::thread_rng();
let num: u32 = rng.gen_range(0..=999999999);
let result = format!("{:09}", num);
(
format!("{}-{}-{}", &result[0..3], &result[3..6], &result[6..9]),
num,
)
}
#[allow(dead_code)]
fn parse_user_code(val: &str) -> Result<u32, Oauth2Error> {
let mut val = val.to_string();
val.retain(|c| c.is_ascii_digit());
val.parse().map_err(|err| {
debug!("Failed to parse value={} as u32: {:?}", val, err);
Oauth2Error::InvalidRequest
})
}
fn host_is_local(host: &Host<&str>) -> bool {
match host {
Host::Ipv4(ip) => ip.is_loopback(),
Host::Ipv6(ip) => ip.is_loopback(),
Host::Domain(domain) => *domain == "localhost",
}
}
fn check_is_loopback(redirect_uri: &Url) -> bool {
redirect_uri.host().map_or(false, |host| {
host_is_local(&host)
})
}
#[cfg(test)]
mod tests {
use base64::{engine::general_purpose, Engine as _};
use std::collections::BTreeSet;
use std::convert::TryFrom;
use std::str::FromStr;
use std::time::Duration;
use uri::{OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT};
use compact_jwt::{
compact::JwkUse, crypto::JwsRs256Verifier, dangernoverify::JwsDangerReleaseWithoutVerify,
JwaAlg, Jwk, JwsCompact, JwsEs256Verifier, JwsVerifier, OidcSubject, OidcUnverified,
};
use kanidm_proto::constants::*;
use kanidm_proto::internal::UserAuthToken;
use kanidm_proto::oauth2::*;
use openssl::sha;
use crate::idm::accountpolicy::ResolvedAccountPolicy;
use crate::idm::oauth2::{host_is_local, AuthoriseResponse, Oauth2Error, OauthRSType};
use crate::idm::server::{IdmServer, IdmServerTransaction};
use crate::prelude::*;
use crate::value::{AuthType, OauthClaimMapJoin, SessionState};
use crate::credential::Credential;
use kanidm_lib_crypto::CryptoPolicy;
use super::Oauth2TokenType;
const TEST_CURRENT_TIME: u64 = 6000;
const UAT_EXPIRE: u64 = 5;
const TOKEN_EXPIRE: u64 = 900;
const UUID_TESTGROUP: Uuid = uuid!("a3028223-bf20-47d5-8b65-967b5d2bb3eb");
macro_rules! create_code_verifier {
($key:expr) => {{
let code_verifier = $key.to_string();
let mut hasher = sha::Sha256::new();
hasher.update(code_verifier.as_bytes());
let code_challenge: Vec<u8> = hasher.finish().iter().copied().collect();
(Some(code_verifier), code_challenge)
}};
}
macro_rules! good_authorisation_request {
(
$idms_prox_read:expr,
$ident:expr,
$ct:expr,
$code_challenge:expr,
$scope:expr
) => {{
let scope: BTreeSet<String> = $scope.split(" ").map(|s| s.to_string()).collect();
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: Some(PkceRequest {
code_challenge: $code_challenge.into(),
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope,
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
$idms_prox_read
.check_oauth2_authorisation(Some($ident), &auth_req, $ct)
.expect("OAuth2 authorisation failed")
}};
}
async fn setup_oauth2_resource_server_basic(
idms: &IdmServer,
ct: Duration,
enable_pkce: bool,
enable_legacy_crypto: bool,
prefer_short_username: bool,
) -> (String, UserAuthToken, Identity, Uuid) {
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let rs_uuid = Uuid::new_v4();
let entry_group: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(Attribute::Uuid, Value::Uuid(UUID_TESTGROUP)),
(Attribute::Member, Value::Refer(UUID_TESTPERSON_1),)
);
let entry_rs: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(
Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value()
),
(
Attribute::Class,
EntryClass::OAuth2ResourceServerBasic.to_value()
),
(Attribute::Uuid, Value::Uuid(rs_uuid)),
(Attribute::Name, Value::new_iname("test_resource_server")),
(
Attribute::DisplayName,
Value::new_utf8s("test_resource_server")
),
(
Attribute::OAuth2RsOriginLanding,
Value::new_url_s("https://demo.example.com").unwrap()
),
(
Attribute::OAuth2RsOrigin,
Value::new_url_s("https://demo.example.com/oauth2/result").unwrap()
),
(
Attribute::OAuth2RsOrigin,
Value::new_url_s("https://portal.example.com").unwrap()
),
(
Attribute::OAuth2RsOrigin,
Value::new_url_s("app://cheese").unwrap()
),
(
Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap(
UUID_TESTGROUP,
btreeset![OAUTH2_SCOPE_GROUPS.to_string()]
)
.expect("invalid oauthscope")
),
(
Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap(
UUID_IDM_ALL_ACCOUNTS,
btreeset![OAUTH2_SCOPE_OPENID.to_string()]
)
.expect("invalid oauthscope")
),
(
Attribute::OAuth2RsSupScopeMap,
Value::new_oauthscopemap(
UUID_IDM_ALL_ACCOUNTS,
btreeset!["supplement".to_string()]
)
.expect("invalid oauthscope")
),
(
Attribute::OAuth2AllowInsecureClientDisablePkce,
Value::new_bool(!enable_pkce)
),
(
Attribute::OAuth2JwtLegacyCryptoEnable,
Value::new_bool(enable_legacy_crypto)
),
(
Attribute::OAuth2PreferShortUsername,
Value::new_bool(prefer_short_username)
)
);
let ce = CreateEvent::new_internal(vec![entry_rs, entry_group, E_TESTPERSON_1.clone()]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
let entry = idms_prox_write
.qs_write
.internal_search_uuid(rs_uuid)
.expect("Failed to retrieve OAuth2 resource entry ");
let secret = entry
.get_ava_single_secret(Attribute::OAuth2RsBasicSecret)
.map(str::to_string)
.expect("No oauth2_rs_basic_secret found");
let session_id = uuid::Uuid::new_v4();
let account = idms_prox_write
.target_to_account(UUID_TESTPERSON_1)
.expect("account must exist");
let uat = account
.to_userauthtoken(
session_id,
SessionScope::ReadWrite,
ct,
&ResolvedAccountPolicy::test_policy(),
)
.expect("Unable to create uat");
let state = uat
.expiry
.map(SessionState::ExpiresAt)
.unwrap_or(SessionState::NeverExpires);
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, "test_password").unwrap();
let cred_id = cred.uuid;
let session = Value::Session(
session_id,
crate::value::Session {
label: "label".to_string(),
state,
issued_at: time::OffsetDateTime::UNIX_EPOCH + ct,
issued_by: IdentityId::Internal,
cred_id,
scope: SessionScope::ReadWrite,
type_: AuthType::Passkey,
},
);
let modlist = ModifyList::new_list(vec![
Modify::Present(Attribute::UserAuthTokenSession, session),
Modify::Present(
Attribute::PrimaryCredential,
Value::Cred("primary".to_string(), cred),
),
]);
idms_prox_write
.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
&modlist,
)
.expect("Failed to modify user");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct, Source::Internal)
.expect("Unable to process uat");
idms_prox_write.commit().expect("failed to commit");
(secret, uat, ident, rs_uuid)
}
async fn setup_oauth2_resource_server_public(
idms: &IdmServer,
ct: Duration,
) -> (UserAuthToken, Identity, Uuid) {
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let rs_uuid = Uuid::new_v4();
let entry_group: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(Attribute::Uuid, Value::Uuid(UUID_TESTGROUP)),
(Attribute::Member, Value::Refer(UUID_TESTPERSON_1),)
);
let entry_rs: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(
Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value()
),
(
Attribute::Class,
EntryClass::OAuth2ResourceServerPublic.to_value()
),
(Attribute::Uuid, Value::Uuid(rs_uuid)),
(Attribute::Name, Value::new_iname("test_resource_server")),
(
Attribute::DisplayName,
Value::new_utf8s("test_resource_server")
),
(
Attribute::OAuth2RsOriginLanding,
Value::new_url_s("https://demo.example.com").unwrap()
),
(
Attribute::OAuth2RsOrigin,
Value::new_url_s("https://demo.example.com/oauth2/result").unwrap()
),
(
Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap(UUID_TESTGROUP, btreeset!["groups".to_string()])
.expect("invalid oauthscope")
),
(
Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap(
UUID_IDM_ALL_ACCOUNTS,
btreeset![OAUTH2_SCOPE_OPENID.to_string()]
)
.expect("invalid oauthscope")
),
(
Attribute::OAuth2RsSupScopeMap,
Value::new_oauthscopemap(
UUID_IDM_ALL_ACCOUNTS,
btreeset!["supplement".to_string()]
)
.expect("invalid oauthscope")
)
);
let ce = CreateEvent::new_internal(vec![entry_rs, entry_group, E_TESTPERSON_1.clone()]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
let session_id = uuid::Uuid::new_v4();
let account = idms_prox_write
.target_to_account(UUID_TESTPERSON_1)
.expect("account must exist");
let uat = account
.to_userauthtoken(
session_id,
SessionScope::ReadWrite,
ct,
&ResolvedAccountPolicy::test_policy(),
)
.expect("Unable to create uat");
let state = uat
.expiry
.map(SessionState::ExpiresAt)
.unwrap_or(SessionState::NeverExpires);
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, "test_password").unwrap();
let cred_id = cred.uuid;
let session = Value::Session(
session_id,
crate::value::Session {
label: "label".to_string(),
state,
issued_at: time::OffsetDateTime::UNIX_EPOCH + ct,
issued_by: IdentityId::Internal,
cred_id,
scope: SessionScope::ReadWrite,
type_: AuthType::Passkey,
},
);
let modlist = ModifyList::new_list(vec![
Modify::Present(Attribute::UserAuthTokenSession, session),
Modify::Present(
Attribute::PrimaryCredential,
Value::Cred("primary".to_string(), cred),
),
]);
idms_prox_write
.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
&modlist,
)
.expect("Failed to modify user");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct, Source::Internal)
.expect("Unable to process uat");
idms_prox_write.commit().expect("failed to commit");
(uat, ident, rs_uuid)
}
async fn setup_idm_admin(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) {
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let account = idms_prox_write
.target_to_account(UUID_IDM_ADMIN)
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat = account
.to_userauthtoken(
session_id,
SessionScope::ReadWrite,
ct,
&ResolvedAccountPolicy::test_policy(),
)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct, Source::Internal)
.expect("Unable to process uat");
idms_prox_write.commit().expect("failed to commit");
(uat, ident)
}
#[idm_test]
async fn test_idm_oauth2_basic_function(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
assert_eq!(permit_success.state, "123");
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
},
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret),
};
let token_response = idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
assert_eq!(token_response.token_type, AccessTokenType::Bearer);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_public_function(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_uat, ident, _) = setup_oauth2_resource_server_public(idms, ct).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
assert_eq!(permit_success.state, "123");
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
},
client_id: Some("test_resource_server".to_string()),
client_secret: None,
};
let token_response = idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
assert_eq!(token_response.token_type, AccessTokenType::Bearer);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_invalid_authorisation_requests(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let (_anon_uat, anon_ident) = setup_idm_admin(idms, ct).await;
let (_idm_admin_uat, idm_admin_ident) = setup_idm_admin(idms, ct).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let pkce_request = Some(PkceRequest {
code_challenge,
code_challenge_method: CodeChallengeMethod::S256,
});
let auth_req = AuthorisationRequest {
response_type: "NOTCODE".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.unwrap_err()
== Oauth2Error::UnsupportedResponseType
);
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: None,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.unwrap_err()
== Oauth2Error::InvalidRequest
);
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "NOT A REAL RESOURCE SERVER".to_string(),
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.unwrap_err()
== Oauth2Error::InvalidClientId
);
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.unwrap_err()
== Oauth2Error::InvalidOrigin
);
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/wrong_place").unwrap(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.unwrap_err()
== Oauth2Error::InvalidOrigin
);
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
let req = idms_prox_read
.check_oauth2_authorisation(None, &auth_req, ct)
.unwrap();
assert!(matches!(
req,
AuthoriseResponse::AuthenticationRequired { .. }
));
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset!["invalid_scope".to_string(), "read".to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.unwrap_err()
== Oauth2Error::AccessDenied
);
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset!["openid".to_string(), "read".to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(Some(&idm_admin_ident), &auth_req, ct)
.unwrap_err()
== Oauth2Error::AccessDenied
);
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset!["openid".to_string(), "read".to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(Some(&anon_ident), &auth_req, ct)
.unwrap_err()
== Oauth2Error::AccessDenied
);
}
#[idm_test]
async fn test_idm_oauth2_invalid_authorisation_permit_requests(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let mut uat_wrong_session_id = uat.clone();
uat_wrong_session_id.session_id = uuid::Uuid::new_v4();
let ident_wrong_session_id = idms_prox_write
.process_uat_to_identity(&uat_wrong_session_id, ct, Source::Internal)
.expect("Unable to process uat");
let account = idms_prox_write
.target_to_account(UUID_IDM_ADMIN)
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat2 = account
.to_userauthtoken(
session_id,
SessionScope::ReadWrite,
ct,
&ResolvedAccountPolicy::test_policy(),
)
.expect("Unable to create uat");
let ident2 = idms_prox_write
.process_uat_to_identity(&uat2, ct, Source::Internal)
.expect("Unable to process uat");
assert!(idms_prox_write.commit().is_ok());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
assert!(
idms_prox_write
.check_oauth2_authorise_permit(
&ident,
&consent_token,
ct + Duration::from_secs(TOKEN_EXPIRE),
)
.unwrap_err()
== OperationError::CryptographyError
);
assert!(
idms_prox_write
.check_oauth2_authorise_permit(&ident2, &consent_token, ct,)
.unwrap_err()
== OperationError::InvalidSessionState
);
assert!(
idms_prox_write
.check_oauth2_authorise_permit(&ident_wrong_session_id, &consent_token, ct,)
.unwrap_err()
== OperationError::InvalidSessionState
);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_invalid_token_exchange_requests(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, mut uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
uat.expiry = Some(
time::OffsetDateTime::UNIX_EPOCH
+ Duration::from_secs(TEST_CURRENT_TIME + UAT_EXPIRE - 1),
);
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier: code_verifier.clone(),
}
.into();
let client_authz = ClientAuthInfo::from("not base64");
assert!(
idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err()
== Oauth2Error::AuthenticationRequired
);
let client_authz =
general_purpose::STANDARD.encode(format!("test_resource_server {secret}"));
let client_authz = ClientAuthInfo::from(client_authz.as_str());
assert!(
idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err()
== Oauth2Error::AuthenticationRequired
);
let client_authz = ClientAuthInfo::encode_basic("NOT A REAL SERVER", secret.as_str());
assert!(
idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err()
== Oauth2Error::AuthenticationRequired
);
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", "12345");
assert!(
idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err()
== Oauth2Error::AuthenticationRequired
);
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
assert!(
idms_prox_write
.check_oauth2_token_exchange(
&client_authz,
&token_req,
ct + Duration::from_secs(TOKEN_EXPIRE)
)
.unwrap_err()
== Oauth2Error::InvalidRequest
);
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code.clone(),
redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(),
code_verifier,
}
.into();
assert!(
idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err()
== Oauth2Error::InvalidOrigin
);
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier: Some("12345".to_string()),
}
.into();
assert!(
idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err()
== Oauth2Error::InvalidRequest
);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_supplemental_origin_redirect(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: Some(PkceRequest {
code_challenge: code_challenge.clone(),
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("https://portal.example.com").unwrap(),
scope: btreeset![OAUTH2_SCOPE_GROUPS.to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
let consent_request = idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.expect("OAuth2 authorisation failed");
trace!(?consent_request);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
assert_eq!(permit_success.state, "123");
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://portal.example.com").unwrap(),
code_verifier: code_verifier.clone(),
},
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret.clone()),
};
let token_response = idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
assert_eq!(token_response.token_type, AccessTokenType::Bearer);
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let ident = idms_prox_read
.process_uat_to_identity(&uat, ct, Source::Internal)
.expect("Unable to process uat");
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: Some(PkceRequest {
code_challenge,
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("app://cheese").unwrap(),
scope: btreeset![OAUTH2_SCOPE_GROUPS.to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
let consent_request = idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.expect("OAuth2 authorisation failed");
trace!(?consent_request);
let AuthoriseResponse::Permitted(permit_success) = consent_request else {
unreachable!();
};
assert_eq!(permit_success.state, "123");
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("app://cheese").unwrap(),
code_verifier,
},
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret),
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let token_response = idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
assert_eq!(token_response.token_type, AccessTokenType::Bearer);
}
#[idm_test]
async fn test_idm_oauth2_token_introspect(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
}
.into();
let oauth2_token = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let intr_request = AccessTokenIntrospectRequest {
token: oauth2_token.access_token,
token_type_hint: None,
};
let intr_response = idms_prox_read
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
.expect("Failed to inspect token");
eprintln!("👉 {intr_response:?}");
assert!(intr_response.active);
assert_eq!(
intr_response.scope,
btreeset!["openid".to_string(), "supplement".to_string()]
);
assert_eq!(
intr_response.client_id.as_deref(),
Some("test_resource_server")
);
assert_eq!(
intr_response.username.as_deref(),
Some("testperson1@example.com")
);
assert_eq!(intr_response.token_type, Some(AccessTokenType::Bearer));
assert_eq!(intr_response.iat, Some(ct.as_secs() as i64));
assert_eq!(intr_response.nbf, Some(ct.as_secs() as i64));
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_CURRENT_TIME - 1));
let me_inv_m = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
ModifyList::new_list(vec![Modify::Present(Attribute::AccountExpire, v_expire)]),
);
assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let intr_response = idms_prox_read
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
.expect("Failed to inspect token");
assert!(!intr_response.active);
}
#[idm_test]
async fn test_idm_oauth2_token_revoke(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
}
.into();
let oauth2_token = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let intr_request = AccessTokenIntrospectRequest {
token: oauth2_token.access_token.clone(),
token_type_hint: None,
};
let intr_response = idms_prox_read
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
.expect("Failed to inspect token");
eprintln!("👉 {intr_response:?}");
assert!(intr_response.active);
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let bad_client_authz = ClientAuthInfo::encode_basic("test_resource_server", "12345");
let revoke_request = TokenRevokeRequest {
token: oauth2_token.access_token.clone(),
token_type_hint: None,
};
let e = idms_prox_write
.oauth2_token_revoke(&bad_client_authz, &revoke_request, ct)
.unwrap_err();
assert!(matches!(e, Oauth2Error::AuthenticationRequired));
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let revoke_request = TokenRevokeRequest {
token: "this is an invalid token, nothing will happen!".to_string(),
token_type_hint: None,
};
let e = idms_prox_write
.oauth2_token_revoke(&client_authz, &revoke_request, ct)
.unwrap_err();
assert!(matches!(e, Oauth2Error::InvalidRequest));
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let intr_response = idms_prox_read
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
.expect("Failed to inspect token");
assert!(intr_response.active);
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let revoke_request = TokenRevokeRequest {
token: oauth2_token.access_token.clone(),
token_type_hint: None,
};
assert!(idms_prox_write
.oauth2_token_revoke(&client_authz, &revoke_request, ct,)
.is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let intr_response = idms_prox_read
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
.expect("Failed to inspect token");
assert!(!intr_response.active);
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let filt = filter!(f_eq(
Attribute::Uuid,
PartialValue::Uuid(ident.get_uuid().unwrap())
));
let mut work_set = idms_prox_write
.qs_write
.internal_search_writeable(&filt)
.expect("Failed to perform internal search writeable");
for (_, entry) in work_set.iter_mut() {
let _ = entry.force_trim_ava(Attribute::OAuth2Session);
}
assert!(idms_prox_write
.qs_write
.internal_apply_writable(work_set)
.is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let intr_response = idms_prox_read
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
.expect("Failed to inspect token");
assert!(intr_response.active);
let ct = ct + AUTH_TOKEN_GRACE_WINDOW;
let intr_response = idms_prox_read
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
.expect("Failed to inspect token");
assert!(!intr_response.active);
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let revoke_request = TokenRevokeRequest {
token: oauth2_token.access_token,
token_type_hint: None,
};
assert!(idms_prox_write
.oauth2_token_revoke(&client_authz, &revoke_request, ct,)
.is_ok());
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_session_cleanup_post_rs_delete(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
}
.into();
let oauth2_token = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
let access_token =
JwsCompact::from_str(&oauth2_token.access_token).expect("Invalid Access Token");
let jws_verifier = JwsDangerReleaseWithoutVerify::default();
let reflected_token = jws_verifier
.verify(&access_token)
.unwrap()
.from_json::<OAuth2RFC9068Token<OAuth2RFC9068TokenExtensions>>()
.expect("Failed to access internals of the refresh token");
let session_id = reflected_token.extensions.session_id;
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let entry = idms_prox_write
.qs_write
.internal_search_uuid(UUID_TESTPERSON_1)
.expect("failed");
let valid = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
.map(|map| map.get(&session_id).is_some())
.unwrap_or(false);
assert!(valid);
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("test_resource_server")
)));
assert!(idms_prox_write.qs_write.delete(&de).is_ok());
let entry = idms_prox_write
.qs_write
.internal_search_uuid(UUID_TESTPERSON_1)
.expect("failed");
let revoked = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
.and_then(|sessions| sessions.get(&session_id))
.map(|session| matches!(session.state, SessionState::RevokedAt(_)))
.unwrap_or(false);
assert!(revoked);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_authorisation_reject(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let ident2 = {
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let account = idms_prox_write
.target_to_account(UUID_IDM_ADMIN)
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat2 = account
.to_userauthtoken(
session_id,
SessionScope::ReadWrite,
ct,
&ResolvedAccountPolicy::test_policy(),
)
.expect("Unable to create uat");
idms_prox_write
.process_uat_to_identity(&uat2, ct, Source::Internal)
.expect("Unable to process uat")
};
let idms_prox_read = idms.proxy_read().await.unwrap();
let redirect_uri = Url::parse("https://demo.example.com/oauth2/result").unwrap();
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
let reject_success = idms_prox_read
.check_oauth2_authorise_reject(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 reject");
assert_eq!(reject_success, redirect_uri);
let past_ct = Duration::from_secs(TEST_CURRENT_TIME + 301);
assert!(
idms_prox_read
.check_oauth2_authorise_reject(&ident, &consent_token, past_ct)
.unwrap_err()
== OperationError::CryptographyError
);
assert!(
idms_prox_read
.check_oauth2_authorise_reject(&ident, "not a token", ct)
.unwrap_err()
== OperationError::CryptographyError
);
assert!(
idms_prox_read
.check_oauth2_authorise_reject(&ident2, &consent_token, ct)
.unwrap_err()
== OperationError::InvalidSessionState
);
}
#[idm_test]
async fn test_idm_oauth2_rfc8414_metadata(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, _uat, _ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
assert!(
idms_prox_read
.oauth2_rfc8414_metadata("nosuchclient")
.unwrap_err()
== OperationError::NoMatchingEntries
);
let discovery = idms_prox_read
.oauth2_rfc8414_metadata("test_resource_server")
.expect("Failed to get discovery");
assert!(
discovery.issuer
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
.unwrap()
);
assert!(
discovery.authorization_endpoint
== Url::parse("https://idm.example.com/ui/oauth2").unwrap()
);
assert!(
discovery.token_endpoint
== Url::parse(&format!(
"https://idm.example.com{}",
uri::OAUTH2_TOKEN_ENDPOINT
))
.unwrap()
);
assert!(
discovery.jwks_uri
== Some(
Url::parse(
"https://idm.example.com/oauth2/openid/test_resource_server/public_key.jwk"
)
.unwrap()
)
);
assert!(discovery.registration_endpoint.is_none());
assert!(
discovery.scopes_supported
== Some(vec![
"groups".to_string(),
OAUTH2_SCOPE_OPENID.to_string(),
"supplement".to_string(),
])
);
assert_eq!(discovery.response_types_supported, vec![ResponseType::Code]);
assert_eq!(
discovery.response_modes_supported,
vec![ResponseMode::Query]
);
assert_eq!(
discovery.grant_types_supported,
vec![GrantType::AuthorisationCode]
);
assert!(
discovery.token_endpoint_auth_methods_supported
== vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost
]
);
assert!(discovery.service_documentation.is_some());
assert!(discovery.ui_locales_supported.is_none());
assert!(discovery.op_policy_uri.is_none());
assert!(discovery.op_tos_uri.is_none());
assert!(
discovery.revocation_endpoint
== Some(
Url::parse(&format!(
"https://idm.example.com{}",
OAUTH2_TOKEN_REVOKE_ENDPOINT
))
.unwrap()
)
);
assert!(
discovery.revocation_endpoint_auth_methods_supported
== vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost
]
);
assert!(
discovery.introspection_endpoint
== Some(
Url::parse(&format!(
"https://idm.example.com{}",
kanidm_proto::constants::uri::OAUTH2_TOKEN_INTROSPECT_ENDPOINT
))
.unwrap()
)
);
assert!(
discovery.introspection_endpoint_auth_methods_supported
== vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost
]
);
assert!(discovery
.introspection_endpoint_auth_signing_alg_values_supported
.is_none());
assert_eq!(
discovery.code_challenge_methods_supported,
vec![PkceAlg::S256]
)
}
#[idm_test]
async fn test_idm_oauth2_openid_discovery(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, _uat, _ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
assert!(
idms_prox_read
.oauth2_openid_discovery("nosuchclient")
.unwrap_err()
== OperationError::NoMatchingEntries
);
assert!(
idms_prox_read
.oauth2_openid_publickey("nosuchclient")
.unwrap_err()
== OperationError::NoMatchingEntries
);
let discovery = idms_prox_read
.oauth2_openid_discovery("test_resource_server")
.expect("Failed to get discovery");
let mut jwkset = idms_prox_read
.oauth2_openid_publickey("test_resource_server")
.expect("Failed to get public key");
let jwk = jwkset.keys.pop().expect("no such jwk");
match jwk {
Jwk::EC { alg, use_, kid, .. } => {
match (
alg.unwrap(),
&discovery.id_token_signing_alg_values_supported[0],
) {
(JwaAlg::ES256, IdTokenSignAlg::ES256) => {}
_ => panic!(),
};
assert_eq!(use_.unwrap(), JwkUse::Sig);
assert!(kid.is_some())
}
_ => panic!(),
};
assert!(
discovery.issuer
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
.unwrap()
);
assert!(
discovery.authorization_endpoint
== Url::parse("https://idm.example.com/ui/oauth2").unwrap()
);
assert!(
discovery.token_endpoint == Url::parse("https://idm.example.com/oauth2/token").unwrap()
);
assert!(
discovery.userinfo_endpoint
== Some(
Url::parse(
"https://idm.example.com/oauth2/openid/test_resource_server/userinfo"
)
.unwrap()
)
);
assert!(
discovery.jwks_uri
== Url::parse(
"https://idm.example.com/oauth2/openid/test_resource_server/public_key.jwk"
)
.unwrap()
);
assert!(
discovery.scopes_supported
== Some(vec![
"groups".to_string(),
OAUTH2_SCOPE_OPENID.to_string(),
"supplement".to_string(),
])
);
assert_eq!(discovery.response_types_supported, vec![ResponseType::Code]);
assert_eq!(
discovery.response_modes_supported,
vec![ResponseMode::Query]
);
assert_eq!(
discovery.grant_types_supported,
vec![GrantType::AuthorisationCode]
);
assert_eq!(discovery.subject_types_supported, vec![SubjectType::Public]);
assert_eq!(
discovery.id_token_signing_alg_values_supported,
vec![IdTokenSignAlg::ES256]
);
assert!(discovery.userinfo_signing_alg_values_supported.is_none());
assert!(
discovery.token_endpoint_auth_methods_supported
== vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost
]
);
assert_eq!(
discovery.display_values_supported,
Some(vec![DisplayValue::Page])
);
assert_eq!(discovery.claim_types_supported, vec![ClaimType::Normal]);
assert!(discovery.claims_supported.is_none());
assert!(discovery.service_documentation.is_some());
assert!(discovery.registration_endpoint.is_none());
assert!(discovery.acr_values_supported.is_none());
assert!(discovery.id_token_encryption_alg_values_supported.is_none());
assert!(discovery.id_token_encryption_enc_values_supported.is_none());
assert!(discovery.userinfo_encryption_alg_values_supported.is_none());
assert!(discovery.userinfo_encryption_enc_values_supported.is_none());
assert!(discovery
.request_object_signing_alg_values_supported
.is_none());
assert!(discovery
.request_object_encryption_alg_values_supported
.is_none());
assert!(discovery
.request_object_encryption_enc_values_supported
.is_none());
assert!(discovery
.token_endpoint_auth_signing_alg_values_supported
.is_none());
assert!(discovery.claims_locales_supported.is_none());
assert!(discovery.ui_locales_supported.is_none());
assert!(discovery.op_policy_uri.is_none());
assert!(discovery.op_tos_uri.is_none());
assert!(!discovery.claims_parameter_supported);
assert!(!discovery.request_uri_parameter_supported);
assert!(!discovery.require_request_uri_registration);
assert!(!discovery.request_parameter_supported);
assert_eq!(
discovery.code_challenge_methods_supported,
vec![PkceAlg::S256]
);
assert!(
discovery.revocation_endpoint
== Some(
Url::parse(&format!(
"https://idm.example.com{}",
OAUTH2_TOKEN_REVOKE_ENDPOINT
))
.unwrap()
)
);
assert!(
discovery.revocation_endpoint_auth_methods_supported
== vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost
]
);
assert!(
discovery.introspection_endpoint
== Some(
Url::parse(&format!(
"https://idm.example.com{}",
OAUTH2_TOKEN_INTROSPECT_ENDPOINT
))
.unwrap()
)
);
assert!(
discovery.introspection_endpoint_auth_methods_supported
== vec![
TokenEndpointAuthMethod::ClientSecretBasic,
TokenEndpointAuthMethod::ClientSecretPost
]
);
assert!(discovery
.introspection_endpoint_auth_signing_alg_values_supported
.is_none());
}
#[idm_test]
async fn test_idm_oauth2_openid_extensions(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
}
.into();
let token_response = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
assert_eq!(token_response.token_type, AccessTokenType::Bearer);
let id_token = token_response.id_token.expect("No id_token in response!");
let access_token =
JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
let refresh_token = token_response
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let mut jwkset = idms_prox_read
.oauth2_openid_publickey("test_resource_server")
.expect("Failed to get public key");
let public_jwk = jwkset.keys.pop().expect("no such jwk");
let jws_validator =
JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
let oidc_unverified =
OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
let iat = ct.as_secs() as i64;
let oidc = jws_validator
.verify(&oidc_unverified)
.unwrap()
.verify_exp(iat)
.expect("Failed to verify oidc");
assert!(
oidc.iss
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
.unwrap()
);
assert_eq!(oidc.sub, OidcSubject::U(UUID_TESTPERSON_1));
assert_eq!(oidc.aud, "test_resource_server");
assert_eq!(oidc.iat, iat);
assert_eq!(oidc.nbf, Some(iat));
assert_eq!(oidc.exp, iat + (OAUTH2_ACCESS_TOKEN_EXPIRY as i64));
assert!(oidc.auth_time.is_none());
assert_eq!(oidc.nonce, Some("abcdef".to_string()));
assert!(oidc.at_hash.is_none());
assert!(oidc.acr.is_none());
assert!(oidc.amr.is_none());
assert_eq!(oidc.azp, Some("test_resource_server".to_string()));
assert!(oidc.jti.is_none());
assert_eq!(oidc.s_claims.name, Some("Test Person 1".to_string()));
assert_eq!(
oidc.s_claims.preferred_username,
Some("testperson1@example.com".to_string())
);
assert!(
oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
);
assert!(oidc.claims.is_empty());
let userinfo = idms_prox_read
.oauth2_openid_userinfo("test_resource_server", access_token, ct)
.expect("failed to get userinfo");
assert_eq!(oidc.iss, userinfo.iss);
assert_eq!(oidc.sub, userinfo.sub);
assert_eq!(oidc.aud, userinfo.aud);
assert_eq!(oidc.iat, userinfo.iat);
assert_eq!(oidc.nbf, userinfo.nbf);
assert_eq!(oidc.exp, userinfo.exp);
assert!(userinfo.auth_time.is_none());
assert_eq!(userinfo.nonce, Some("abcdef".to_string()));
assert!(userinfo.at_hash.is_none());
assert!(userinfo.acr.is_none());
assert_eq!(oidc.amr, userinfo.amr);
assert_eq!(oidc.azp, userinfo.azp);
assert!(userinfo.jti.is_none());
assert_eq!(oidc.s_claims, userinfo.s_claims);
assert!(userinfo.claims.is_empty());
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let token_response = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
let access_token =
JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let userinfo = idms_prox_read
.oauth2_openid_userinfo("test_resource_server", access_token, ct)
.expect("failed to get userinfo");
assert_eq!(oidc.iss, userinfo.iss);
assert_eq!(oidc.sub, userinfo.sub);
assert_eq!(oidc.aud, userinfo.aud);
assert_eq!(oidc.iat, userinfo.iat);
assert_eq!(oidc.nbf, userinfo.nbf);
assert_eq!(oidc.exp, userinfo.exp);
assert!(userinfo.auth_time.is_none());
assert_eq!(userinfo.nonce, Some("abcdef".to_string()));
assert!(userinfo.at_hash.is_none());
assert!(userinfo.acr.is_none());
assert_eq!(oidc.amr, userinfo.amr);
assert_eq!(oidc.azp, userinfo.azp);
assert!(userinfo.jti.is_none());
assert_eq!(oidc.s_claims, userinfo.s_claims);
assert!(userinfo.claims.is_empty());
}
#[idm_test]
async fn test_idm_oauth2_openid_short_username(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, true).await;
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
}
.into();
let token_response = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
let id_token = token_response.id_token.expect("No id_token in response!");
let access_token =
JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let mut jwkset = idms_prox_read
.oauth2_openid_publickey("test_resource_server")
.expect("Failed to get public key");
let public_jwk = jwkset.keys.pop().expect("no such jwk");
let jws_validator =
JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
let oidc_unverified =
OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
let iat = ct.as_secs() as i64;
let oidc = jws_validator
.verify(&oidc_unverified)
.unwrap()
.verify_exp(iat)
.expect("Failed to verify oidc");
assert_eq!(
oidc.s_claims.preferred_username,
Some("testperson1".to_string())
);
let userinfo = idms_prox_read
.oauth2_openid_userinfo("test_resource_server", access_token, ct)
.expect("failed to get userinfo");
assert_eq!(oidc.s_claims, userinfo.s_claims);
}
#[idm_test]
async fn test_idm_oauth2_openid_group_claims(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, true).await;
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
"openid groups".to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
}
.into();
let token_response = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
let id_token = token_response.id_token.expect("No id_token in response!");
let access_token =
JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let mut jwkset = idms_prox_read
.oauth2_openid_publickey("test_resource_server")
.expect("Failed to get public key");
let public_jwk = jwkset.keys.pop().expect("no such jwk");
let jws_validator =
JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
let oidc_unverified =
OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
let iat = ct.as_secs() as i64;
let oidc = jws_validator
.verify(&oidc_unverified)
.unwrap()
.verify_exp(iat)
.expect("Failed to verify oidc");
assert!(oidc.claims.contains_key("groups"));
assert!(oidc
.claims
.get("groups")
.expect("unable to find key")
.as_array()
.unwrap()
.contains(&serde_json::json!(STR_UUID_IDM_ALL_ACCOUNTS)));
let userinfo = idms_prox_read
.oauth2_openid_userinfo("test_resource_server", access_token, ct)
.expect("failed to get userinfo");
assert_eq!(oidc.claims.get("groups"), userinfo.claims.get("groups"));
}
#[idm_test]
async fn test_idm_oauth2_insecure_pkce(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, false, false, false).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let _consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: None,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset![OAUTH2_SCOPE_GROUPS.to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.expect("Oauth2 authorisation failed");
}
#[idm_test]
async fn test_idm_oauth2_openid_legacy_crypto(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, false, true, false).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
let discovery = idms_prox_read
.oauth2_openid_discovery("test_resource_server")
.expect("Failed to get discovery");
let mut jwkset = idms_prox_read
.oauth2_openid_publickey("test_resource_server")
.expect("Failed to get public key");
let jwk = jwkset.keys.pop().expect("no such jwk");
let public_jwk = jwk.clone();
match jwk {
Jwk::RSA { alg, use_, kid, .. } => {
match (
alg.unwrap(),
&discovery.id_token_signing_alg_values_supported[0],
) {
(JwaAlg::RS256, IdTokenSignAlg::RS256) => {}
_ => panic!(),
};
assert_eq!(use_.unwrap(), JwkUse::Sig);
assert!(kid.is_some());
}
_ => panic!(),
};
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
},
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret),
};
let token_response = idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
assert_eq!(token_response.token_type, AccessTokenType::Bearer);
let id_token = token_response.id_token.expect("No id_token in response!");
let jws_validator =
JwsRs256Verifier::try_from(&public_jwk).expect("failed to build validator");
let oidc_unverified =
OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
let iat = ct.as_secs() as i64;
let oidc = jws_validator
.verify(&oidc_unverified)
.unwrap()
.verify_exp(iat)
.expect("Failed to verify oidc");
assert_eq!(oidc.sub, OidcSubject::U(UUID_TESTPERSON_1));
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_consent_granted_and_changed_workflow(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let _permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let ident = idms_prox_read
.process_uat_to_identity(&uat, ct, Source::Internal)
.expect("Unable to process uat");
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::Permitted(_permit_success) = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let me_extend_scopes = ModifyEvent::new_internal_invalid(
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("test_resource_server")
)),
ModifyList::new_list(vec![Modify::Present(
Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap(
UUID_IDM_ALL_ACCOUNTS,
btreeset![
OAUTH2_SCOPE_EMAIL.to_string(),
OAUTH2_SCOPE_OPENID.to_string()
],
)
.expect("invalid oauthscope"),
)]),
);
assert!(idms_prox_write.qs_write.modify(&me_extend_scopes).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let ident = idms_prox_read
.process_uat_to_identity(&uat, ct, Source::Internal)
.expect("Unable to process uat");
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: Some(PkceRequest {
code_challenge,
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset!["openid".to_string(), "email".to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
let consent_request = idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.expect("Oauth2 authorisation failed");
let AuthoriseResponse::ConsentRequested { .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let me_extend_scopes = ModifyEvent::new_internal_invalid(
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("test_resource_server")
)),
ModifyList::new_list(vec![Modify::Present(
Attribute::OAuth2RsSupScopeMap,
Value::new_oauthscopemap(UUID_IDM_ALL_ACCOUNTS, btreeset!["newscope".to_string()])
.expect("invalid oauthscope"),
)]),
);
assert!(idms_prox_write.qs_write.modify(&me_extend_scopes).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let ident = idms_prox_read
.process_uat_to_identity(&uat, ct, Source::Internal)
.expect("Unable to process uat");
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: Some(PkceRequest {
code_challenge,
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset!["openid".to_string(), "email".to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
let consent_request = idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.expect("Oauth2 authorisation failed");
let _consent_token = if let AuthoriseResponse::ConsentRequested {
consent_token,
scopes,
..
} = consent_request
{
assert!(scopes.contains("newscope"));
consent_token
} else {
unreachable!();
};
}
#[idm_test]
async fn test_idm_oauth2_consent_granted_refint_cleanup_on_delete(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, o2rs_uuid) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
assert!(ident.get_oauth2_consent_scopes(o2rs_uuid).is_none());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let _permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct, Source::Internal)
.expect("Unable to process uat");
assert!(
ident.get_oauth2_consent_scopes(o2rs_uuid)
== Some(&btreeset![
OAUTH2_SCOPE_OPENID.to_string(),
"supplement".to_string()
])
);
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("test_resource_server")
)));
assert!(idms_prox_write.qs_write.delete(&de).is_ok());
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct, Source::Internal)
.expect("Unable to process uat");
dbg!(&o2rs_uuid);
dbg!(&ident);
let consent_scopes = ident.get_oauth2_consent_scopes(o2rs_uuid);
dbg!(consent_scopes);
assert!(consent_scopes.is_none());
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_1076_pkce_downgrade(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, false, false, false).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, _code_challenge) = create_code_verifier!("Whar Garble");
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: None,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
let consent_request = idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.expect("Failed to perform OAuth2 authorisation request.");
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
},
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret),
};
assert!(matches!(
idms_prox_write.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct),
Err(Oauth2Error::InvalidRequest)
));
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_redir_http_downgrade(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, false, false, false).await;
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: Some(PkceRequest {
code_challenge: code_challenge.clone(),
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("http://demo.example.com/oauth2/result").unwrap(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
assert!(
idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.unwrap_err()
== Oauth2Error::InvalidOrigin
);
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("http://demo.example.com/oauth2/result").unwrap(),
code_verifier,
},
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret),
};
assert!(matches!(
idms_prox_write.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct),
Err(Oauth2Error::InvalidOrigin)
));
assert!(idms_prox_write.commit().is_ok());
}
async fn setup_refresh_token(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
ct: Duration,
) -> (AccessTokenResponse, ClientAuthInfo) {
let (secret, _uat, ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
}
.into();
let access_token_response_1 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
assert!(idms_prox_write.commit().is_ok());
trace!(?access_token_response_1);
(access_token_response_1, client_authz)
}
#[idm_test]
async fn test_idm_oauth2_refresh_token_basic(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (access_token_response_1, client_authz) =
setup_refresh_token(idms, idms_delayed, ct).await;
let ct = Duration::from_secs(TEST_CURRENT_TIME + 10);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_1
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let access_token_response_2 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
assert!(idms_prox_write.commit().is_ok());
trace!(?access_token_response_2);
assert!(access_token_response_1.access_token != access_token_response_2.access_token);
assert!(access_token_response_1.refresh_token != access_token_response_2.refresh_token);
assert!(access_token_response_1.id_token != access_token_response_2.id_token);
let ct =
Duration::from_secs(TEST_CURRENT_TIME + 20 + access_token_response_2.expires_in as u64);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_2
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let reflected_token = idms_prox_write
.reflect_oauth2_token(&client_authz, &refresh_token)
.expect("Failed to access internals of the refresh token");
let refresh_exp = match reflected_token {
Oauth2TokenType::Refresh { exp, .. } => exp,
Oauth2TokenType::ClientAccess { .. } => unreachable!(),
};
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let access_token_response_3 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
let entry = idms_prox_write
.qs_write
.internal_search_uuid(UUID_TESTPERSON_1)
.expect("failed");
let session = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
.and_then(|sessions| sessions.first_key_value())
.unwrap();
trace!(?session);
assert_eq!(
SessionState::ExpiresAt(
time::OffsetDateTime::UNIX_EPOCH
+ ct
+ Duration::from_secs(OAUTH_REFRESH_TOKEN_EXPIRY)
),
session.1.state
);
assert!(idms_prox_write.commit().is_ok());
trace!(?access_token_response_3);
assert!(access_token_response_3.access_token != access_token_response_2.access_token);
assert!(access_token_response_3.refresh_token != access_token_response_2.refresh_token);
assert!(access_token_response_3.id_token != access_token_response_2.id_token);
let ct = Duration::from_secs(
TEST_CURRENT_TIME + refresh_exp as u64 + access_token_response_3.expires_in as u64,
);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_3
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let access_token_response_4 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err();
assert_eq!(access_token_response_4, Oauth2Error::InvalidGrant);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_refresh_token_oauth2_session_expired(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (access_token_response_1, client_authz) =
setup_refresh_token(idms, idms_delayed, ct).await;
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let revoke_request = TokenRevokeRequest {
token: access_token_response_1.access_token.clone(),
token_type_hint: None,
};
assert!(idms_prox_write
.oauth2_token_revoke(&client_authz, &revoke_request, ct,)
.is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_1
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let access_token_response_2 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err();
assert_eq!(access_token_response_2, Oauth2Error::InvalidGrant);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_refresh_token_invalid_client_authz(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (access_token_response_1, _client_authz) =
setup_refresh_token(idms, idms_delayed, ct).await;
let bad_client_authz = ClientAuthInfo::encode_basic("test_resource_server", "12345");
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_1
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let access_token_response_2 = idms_prox_write
.check_oauth2_token_exchange(&bad_client_authz, &token_req, ct)
.unwrap_err();
assert_eq!(access_token_response_2, Oauth2Error::AuthenticationRequired);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_refresh_token_inconsistent_scopes(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (access_token_response_1, client_authz) =
setup_refresh_token(idms, idms_delayed, ct).await;
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_1
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: Some(btreeset!["invalid_scope".to_string()]),
}
.into();
let access_token_response_2 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err();
assert_eq!(access_token_response_2, Oauth2Error::InvalidScope);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_refresh_token_reuse_invalidates_session(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (access_token_response_1, client_authz) =
setup_refresh_token(idms, idms_delayed, ct).await;
let ct = Duration::from_secs(TEST_CURRENT_TIME + 1);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_1
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let _access_token_response_2 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
assert!(idms_prox_write.commit().is_ok());
let ct = Duration::from_secs(TEST_CURRENT_TIME + 2);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_1
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let access_token_response_3 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err();
assert_eq!(access_token_response_3, Oauth2Error::InvalidGrant);
let entry = idms_prox_write
.qs_write
.internal_search_uuid(UUID_TESTPERSON_1)
.expect("failed");
let valid = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
.and_then(|sessions| sessions.first_key_value())
.map(|(_, session)| !matches!(session.state, SessionState::RevokedAt(_)))
.unwrap();
assert!(!valid);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_refresh_token_divergence(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (access_token_response_1, client_authz) =
setup_refresh_token(idms, idms_delayed, ct).await;
let ct = Duration::from_secs(TEST_CURRENT_TIME + 1);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_1
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let access_token_response_2 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
drop(idms_prox_write);
let ct = Duration::from_secs(TEST_CURRENT_TIME + 2);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_2
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let _access_token_response_3 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_refresh_token_scope_constraints(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (access_token_response_1, client_authz) =
setup_refresh_token(idms, idms_delayed, ct).await;
let ct = Duration::from_secs(TEST_CURRENT_TIME + 10);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let refresh_token = access_token_response_1
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let jws_verifier = JwsDangerReleaseWithoutVerify::default();
let access_token_unverified = JwsCompact::from_str(&access_token_response_1.access_token)
.expect("Invalid Access Token");
let reflected_token = jws_verifier
.verify(&access_token_unverified)
.unwrap()
.from_json::<OAuth2RFC9068Token<OAuth2RFC9068TokenExtensions>>()
.expect("Failed to access internals of the refresh token");
trace!(?reflected_token);
let initial_scopes = reflected_token.extensions.scope;
trace!(?initial_scopes);
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let access_token_response_2 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
let access_token_unverified = JwsCompact::from_str(&access_token_response_2.access_token)
.expect("Invalid Access Token");
let reflected_token = jws_verifier
.verify(&access_token_unverified)
.unwrap()
.from_json::<OAuth2RFC9068Token<OAuth2RFC9068TokenExtensions>>()
.expect("Failed to access internals of the refresh token");
assert_eq!(initial_scopes, reflected_token.extensions.scope);
let refresh_token = access_token_response_2
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: Some(["openid".to_string()].into()),
}
.into();
let access_token_response_3 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
let access_token_unverified = JwsCompact::from_str(&access_token_response_3.access_token)
.expect("Invalid Access Token");
let reflected_token = jws_verifier
.verify(&access_token_unverified)
.unwrap()
.from_json::<OAuth2RFC9068Token<OAuth2RFC9068TokenExtensions>>()
.expect("Failed to access internals of the refresh token");
assert_ne!(initial_scopes, reflected_token.extensions.scope);
let constrained_scopes = reflected_token.extensions.scope;
let refresh_token = access_token_response_3
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: None,
}
.into();
let access_token_response_4 = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Unable to exchange for OAuth2 token");
let access_token_unverified = JwsCompact::from_str(&access_token_response_4.access_token)
.expect("Invalid Access Token");
let reflected_token = jws_verifier
.verify(&access_token_unverified)
.unwrap()
.from_json::<OAuth2RFC9068Token<OAuth2RFC9068TokenExtensions>>()
.expect("Failed to access internals of the refresh token");
assert_ne!(initial_scopes, reflected_token.extensions.scope);
assert_eq!(constrained_scopes, reflected_token.extensions.scope);
let refresh_token = access_token_response_4
.refresh_token
.as_ref()
.expect("no refresh token was issued")
.clone();
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
refresh_token,
scope: Some(initial_scopes),
}
.into();
let access_token_response_5_err = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.unwrap_err();
assert_eq!(access_token_response_5_err, Oauth2Error::InvalidScope);
assert!(idms_prox_write.commit().is_ok());
}
#[test]
fn compliant_serialization_test() {
let token_req: Result<AccessTokenRequest, serde_json::Error> = serde_json::from_str(
r#"
{
"grant_type": "refresh_token",
"refresh_token": "some_dumb_refresh_token",
"scope": "invalid_scope vasd asd"
}
"#,
);
assert!(token_req.is_ok());
}
#[idm_test]
async fn test_idm_oauth2_custom_claims(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, ident, oauth2_rs_uuid) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let modlist = ModifyList::new_list(vec![
Modify::Present(
Attribute::OAuth2RsClaimMap,
Value::OauthClaimMap(
"custom_a".to_string(),
OauthClaimMapJoin::CommaSeparatedValue,
),
),
Modify::Present(
Attribute::OAuth2RsClaimMap,
Value::OauthClaimValue(
"custom_a".to_string(),
UUID_TESTGROUP,
btreeset!["value_a".to_string()],
),
),
Modify::Present(
Attribute::OAuth2RsClaimMap,
Value::OauthClaimValue(
"custom_a".to_string(),
UUID_IDM_ALL_ACCOUNTS,
btreeset!["value_b".to_string()],
),
),
Modify::Present(
Attribute::OAuth2RsClaimMap,
Value::OauthClaimMap(
"custom_b".to_string(),
OauthClaimMapJoin::SpaceSeparatedValue,
),
),
Modify::Present(
Attribute::OAuth2RsClaimMap,
Value::OauthClaimValue(
"custom_b".to_string(),
UUID_TESTGROUP,
btreeset!["value_a".to_string()],
),
),
Modify::Present(
Attribute::OAuth2RsClaimMap,
Value::OauthClaimValue(
"custom_b".to_string(),
UUID_IDM_ALL_ACCOUNTS,
btreeset!["value_b".to_string()],
),
),
Modify::Present(
Attribute::OAuth2RsClaimMap,
Value::OauthClaimValue(
"custom_b".to_string(),
UUID_IDM_ADMINS,
btreeset!["value_c".to_string()],
),
),
]);
assert!(idms_prox_write
.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(oauth2_rs_uuid))),
&modlist,
)
.is_ok());
assert!(idms_prox_write.commit().is_ok());
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let consent_request = good_authorisation_request!(
idms_prox_read,
&ident,
ct,
code_challenge,
OAUTH2_SCOPE_OPENID.to_string()
);
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
code_verifier,
}
.into();
let token_response = idms_prox_write
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
assert_eq!(token_response.token_type, AccessTokenType::Bearer);
let id_token = token_response.id_token.expect("No id_token in response!");
let access_token =
JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let mut jwkset = idms_prox_read
.oauth2_openid_publickey("test_resource_server")
.expect("Failed to get public key");
let public_jwk = jwkset.keys.pop().expect("no such jwk");
let jws_validator =
JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
let oidc_unverified =
OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
let iat = ct.as_secs() as i64;
let oidc = jws_validator
.verify(&oidc_unverified)
.unwrap()
.verify_exp(iat)
.expect("Failed to verify oidc");
assert!(
oidc.iss
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
.unwrap()
);
assert_eq!(oidc.sub, OidcSubject::U(UUID_TESTPERSON_1));
assert_eq!(oidc.aud, "test_resource_server");
assert_eq!(oidc.iat, iat);
assert_eq!(oidc.nbf, Some(iat));
assert_eq!(oidc.exp, iat + (OAUTH2_ACCESS_TOKEN_EXPIRY as i64));
assert!(oidc.auth_time.is_none());
assert_eq!(oidc.nonce, Some("abcdef".to_string()));
assert!(oidc.at_hash.is_none());
assert!(oidc.acr.is_none());
assert!(oidc.amr.is_none());
assert_eq!(oidc.azp, Some("test_resource_server".to_string()));
assert!(oidc.jti.is_none());
assert_eq!(oidc.s_claims.name, Some("Test Person 1".to_string()));
assert_eq!(
oidc.s_claims.preferred_username,
Some("testperson1@example.com".to_string())
);
assert!(
oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
);
assert_eq!(
oidc.claims.get("custom_a").and_then(|v| v.as_str()),
Some("value_a,value_b")
);
assert_eq!(
oidc.claims.get("custom_b").and_then(|v| v.as_str()),
Some("value_a value_b")
);
let userinfo = idms_prox_read
.oauth2_openid_userinfo("test_resource_server", access_token, ct)
.expect("failed to get userinfo");
assert_eq!(oidc.iss, userinfo.iss);
assert_eq!(oidc.sub, userinfo.sub);
assert_eq!(oidc.aud, userinfo.aud);
assert_eq!(oidc.iat, userinfo.iat);
assert_eq!(oidc.nbf, userinfo.nbf);
assert_eq!(oidc.exp, userinfo.exp);
assert!(userinfo.auth_time.is_none());
assert_eq!(userinfo.nonce, Some("abcdef".to_string()));
assert!(userinfo.at_hash.is_none());
assert!(userinfo.acr.is_none());
assert_eq!(oidc.amr, userinfo.amr);
assert_eq!(oidc.azp, userinfo.azp);
assert!(userinfo.jti.is_none());
assert_eq!(oidc.s_claims, userinfo.s_claims);
assert_eq!(oidc.claims, userinfo.claims);
let intr_request = AccessTokenIntrospectRequest {
token: token_response.access_token.clone(),
token_type_hint: None,
};
let intr_response = idms_prox_read
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
.expect("Failed to inspect token");
eprintln!("👉 {intr_response:?}");
assert!(intr_response.active);
assert_eq!(
intr_response.scope,
btreeset!["openid".to_string(), "supplement".to_string()]
);
assert_eq!(
intr_response.client_id.as_deref(),
Some("test_resource_server")
);
assert_eq!(
intr_response.username.as_deref(),
Some("testperson1@example.com")
);
assert_eq!(intr_response.token_type, Some(AccessTokenType::Bearer));
assert_eq!(intr_response.iat, Some(ct.as_secs() as i64));
assert_eq!(intr_response.nbf, Some(ct.as_secs() as i64));
drop(idms_prox_read);
}
#[idm_test]
async fn test_idm_oauth2_public_allow_localhost_redirect(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_uat, ident, oauth2_rs_uuid) = setup_oauth2_resource_server_public(idms, ct).await;
let mut idms_prox_write: crate::idm::server::IdmServerProxyWriteTransaction<'_> =
idms.proxy_write(ct).await.unwrap();
let redirect_uri = Url::parse("http://localhost:8765/oauth2/result")
.expect("Failed to parse redirect URL");
let modlist = ModifyList::new_list(vec![
Modify::Present(Attribute::OAuth2AllowLocalhostRedirect, Value::Bool(true)),
Modify::Present(Attribute::OAuth2RsOrigin, Value::Url(redirect_uri.clone())),
]);
assert!(idms_prox_write
.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(oauth2_rs_uuid))),
&modlist,
)
.is_ok());
assert!(idms_prox_write.commit().is_ok());
let idms_prox_read = idms.proxy_read().await.unwrap();
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
state: "123".to_string(),
pkce_request: Some(PkceRequest {
code_challenge,
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("http://localhost:8765/oauth2/result").unwrap(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
unknown_keys: Default::default(),
};
let consent_request = idms_prox_read
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
.expect("OAuth2 authorisation failed");
let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
unreachable!();
};
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let permit_success = idms_prox_write
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
.expect("Failed to perform OAuth2 permit");
assert_eq!(permit_success.state, "123");
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::AuthorizationCode {
code: permit_success.code,
redirect_uri,
code_verifier,
},
client_id: Some("test_resource_server".to_string()),
client_secret: None,
};
let token_response = idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
assert_eq!(token_response.token_type, AccessTokenType::Bearer);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test]
async fn test_idm_oauth2_basic_client_credentials_grant_valid(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, _ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::ClientCredentials { scope: None },
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret),
};
let oauth2_token = idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
assert!(idms_prox_write.commit().is_ok());
assert_eq!(oauth2_token.token_type, AccessTokenType::Bearer);
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let intr_request = AccessTokenIntrospectRequest {
token: oauth2_token.access_token.clone(),
token_type_hint: None,
};
let intr_response = idms_prox_read
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
.expect("Failed to inspect token");
eprintln!("👉 {intr_response:?}");
assert!(intr_response.active);
assert_eq!(intr_response.scope, btreeset!["supplement".to_string()]);
assert_eq!(
intr_response.client_id.as_deref(),
Some("test_resource_server")
);
assert_eq!(
intr_response.username.as_deref(),
Some("test_resource_server@example.com")
);
assert_eq!(intr_response.token_type, Some(AccessTokenType::Bearer));
assert_eq!(intr_response.iat, Some(ct.as_secs() as i64));
assert_eq!(intr_response.nbf, Some(ct.as_secs() as i64));
drop(idms_prox_read);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let revoke_request = TokenRevokeRequest {
token: oauth2_token.access_token.clone(),
token_type_hint: None,
};
assert!(idms_prox_write
.oauth2_token_revoke(&client_authz, &revoke_request, ct,)
.is_ok());
assert!(idms_prox_write.commit().is_ok());
let ct = ct + AUTH_TOKEN_GRACE_WINDOW;
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let intr_request = AccessTokenIntrospectRequest {
token: oauth2_token.access_token.clone(),
token_type_hint: None,
};
let intr_response = idms_prox_read
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
.expect("Failed to inspect token");
assert!(!intr_response.active);
drop(idms_prox_read);
}
#[idm_test]
async fn test_idm_oauth2_basic_client_credentials_grant_invalid(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, _uat, _ident, _) =
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::ClientCredentials { scope: None },
client_id: Some("test_resource_server".to_string()),
client_secret: None,
};
assert_eq!(
idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.unwrap_err(),
Oauth2Error::AuthenticationRequired
);
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::ClientCredentials { scope: None },
client_id: Some("test_resource_server".to_string()),
client_secret: Some("wrong password".to_string()),
};
assert_eq!(
idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.unwrap_err(),
Oauth2Error::AuthenticationRequired
);
let scope = Some(btreeset!["💅".to_string()]);
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::ClientCredentials { scope },
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret.clone()),
};
assert_eq!(
idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.unwrap_err(),
Oauth2Error::InvalidScope
);
let scope = Some(btreeset!["invalid_scope".to_string()]);
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::ClientCredentials { scope },
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret.clone()),
};
assert_eq!(
idms_prox_write
.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
.unwrap_err(),
Oauth2Error::AccessDenied
);
assert!(idms_prox_write.commit().is_ok());
}
#[test]
fn test_get_code() {
use super::{gen_device_code, gen_user_code, parse_user_code};
assert!(gen_device_code().is_ok());
let (res_string, res_value) = gen_user_code();
assert!(res_string.split('-').count() == 3);
let res_string_clean = res_string.replace("-", "");
let res_string_as_num = res_string_clean
.parse::<u32>()
.expect("Failed to parse as number");
assert_eq!(res_string_as_num, res_value);
assert_eq!(
parse_user_code(&res_string).expect("Failed to parse code"),
res_value
);
}
#[idm_test]
async fn handle_oauth2_start_device_flow(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = duration_from_epoch_now();
let client_auth_info = ClientAuthInfo::from(Source::Https(
"127.0.0.1"
.parse()
.expect("Failed to parse 127.0.0.1 as an IP!"),
));
let eventid = Uuid::new_v4();
let res = idms
.proxy_write(ct)
.await
.expect("Failed to get idmspwt")
.handle_oauth2_start_device_flow(client_auth_info, "test_rs_id", &None, eventid);
dbg!(&res);
assert!(res.is_err());
}
#[test]
fn test_url_localhost_domain() {
let example_is_not_local = "https://example.com/sdfsdf";
println!("Ensuring that {} is not local", example_is_not_local);
assert!(!host_is_local(
&Url::parse(example_is_not_local)
.expect("Failed to parse example.com as a host?")
.host()
.expect(&format!(
"Couldn't get a host from {}",
example_is_not_local
))
));
let test_urls = [
("http://localhost:8080/oauth2/callback", "/oauth2/callback"),
("https://localhost/foo/bar", "/foo/bar"),
("http://127.0.0.1:12345/foo", "/foo"),
("http://[::1]:12345/foo", "/foo"),
];
for (url, path) in test_urls.into_iter() {
println!("Testing URL: {}", url);
let url = Url::parse(url).expect("One of the test values failed!");
assert!(host_is_local(
&url.host().expect("Didn't parse a host out?")
));
assert_eq!(url.path(), path);
}
}
#[test]
fn test_oauth2_rs_type_allow_localhost_redirect() {
let test_cases = [
(
OauthRSType::Public {
allow_localhost_redirect: true,
},
true,
),
(
OauthRSType::Public {
allow_localhost_redirect: false,
},
false,
),
(
OauthRSType::Basic {
authz_secret: "supersecret".to_string(),
enable_pkce: false,
},
false,
),
];
assert!(test_cases.iter().all(|(rs_type, expected)| {
let actual = rs_type.allow_localhost_redirect();
println!("Testing {:?} -> {}", rs_type, expected);
actual == *expected
}));
}
}