mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-24 04:57:00 +01:00
feat(oauth2): add support for a 'groups' claim (#1272)
This commit is contained in:
parent
4a6d645bc7
commit
f344c68159
|
@ -156,7 +156,7 @@ impl Account {
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub(crate) fn try_from_entry_ro(
|
pub(crate) fn try_from_entry_ro(
|
||||||
value: &Entry<EntrySealed, EntryCommitted>,
|
value: &Entry<EntrySealed, EntryCommitted>,
|
||||||
qs: &mut QueryServerReadTransaction,
|
qs: &QueryServerReadTransaction,
|
||||||
) -> Result<Self, OperationError> {
|
) -> Result<Self, OperationError> {
|
||||||
let groups = Group::try_from_account_entry_ro(value, qs)?;
|
let groups = Group::try_from_account_entry_ro(value, qs)?;
|
||||||
try_from_entry!(value, groups)
|
try_from_entry!(value, groups)
|
||||||
|
|
|
@ -85,7 +85,7 @@ impl Group {
|
||||||
|
|
||||||
pub fn try_from_account_entry_ro(
|
pub fn try_from_account_entry_ro(
|
||||||
value: &Entry<EntrySealed, EntryCommitted>,
|
value: &Entry<EntrySealed, EntryCommitted>,
|
||||||
qs: &mut QueryServerReadTransaction,
|
qs: &QueryServerReadTransaction,
|
||||||
) -> Result<Vec<Self>, OperationError> {
|
) -> Result<Vec<Self>, OperationError> {
|
||||||
try_from_account_e!(value, qs)
|
try_from_account_e!(value, qs)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ use tracing::trace;
|
||||||
use url::{Origin, Url};
|
use url::{Origin, Url};
|
||||||
|
|
||||||
use crate::identity::IdentityId;
|
use crate::identity::IdentityId;
|
||||||
|
use crate::idm::account::Account;
|
||||||
use crate::idm::delayed::{DelayedAction, Oauth2ConsentGrant, Oauth2SessionRecord};
|
use crate::idm::delayed::{DelayedAction, Oauth2ConsentGrant, Oauth2SessionRecord};
|
||||||
use crate::idm::server::{
|
use crate::idm::server::{
|
||||||
IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
|
IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
|
||||||
|
@ -955,6 +956,7 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
|
|
||||||
pub fn check_oauth2_token_exchange(
|
pub fn check_oauth2_token_exchange(
|
||||||
&self,
|
&self,
|
||||||
|
idms: &IdmServerProxyReadTransaction<'_>,
|
||||||
client_authz: Option<&str>,
|
client_authz: Option<&str>,
|
||||||
token_req: &AccessTokenRequest,
|
token_req: &AccessTokenRequest,
|
||||||
ct: Duration,
|
ct: Duration,
|
||||||
|
@ -991,7 +993,7 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
// TODO: add refresh token grant type.
|
// TODO: add refresh token grant type.
|
||||||
// If it's a refresh token grant, are the consent permissions the same?
|
// If it's a refresh token grant, are the consent permissions the same?
|
||||||
if token_req.grant_type == "authorization_code" {
|
if token_req.grant_type == "authorization_code" {
|
||||||
self.check_oauth2_token_exchange_authorization_code(o2rs, token_req, ct, async_tx)
|
self.check_oauth2_token_exchange_authorization_code(idms, o2rs, token_req, ct, async_tx)
|
||||||
} else {
|
} else {
|
||||||
admin_warn!("Invalid oauth2 grant_type (should be 'authorization_code')");
|
admin_warn!("Invalid oauth2 grant_type (should be 'authorization_code')");
|
||||||
Err(Oauth2Error::InvalidRequest)
|
Err(Oauth2Error::InvalidRequest)
|
||||||
|
@ -1000,6 +1002,7 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
|
|
||||||
fn check_oauth2_token_exchange_authorization_code(
|
fn check_oauth2_token_exchange_authorization_code(
|
||||||
&self,
|
&self,
|
||||||
|
idms: &IdmServerProxyReadTransaction<'_>,
|
||||||
o2rs: &Oauth2RS,
|
o2rs: &Oauth2RS,
|
||||||
token_req: &AccessTokenRequest,
|
token_req: &AccessTokenRequest,
|
||||||
ct: Duration,
|
ct: Duration,
|
||||||
|
@ -1094,9 +1097,7 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
Some(code_xchg.scopes.join(" "))
|
Some(code_xchg.scopes.join(" "))
|
||||||
};
|
};
|
||||||
|
|
||||||
let scope_set: BTreeSet<String> = code_xchg.scopes.iter().cloned().collect();
|
let id_token = if code_xchg.scopes.contains(&"openid".to_string()) {
|
||||||
|
|
||||||
let id_token = if scope_set.contains("openid") {
|
|
||||||
// TODO: Scopes map to claims:
|
// TODO: Scopes map to claims:
|
||||||
//
|
//
|
||||||
// * profile - (name, family\_name, given\_name, middle\_name, nickname, preferred\_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated\_at)
|
// * profile - (name, family\_name, given\_name, middle\_name, nickname, preferred\_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated\_at)
|
||||||
|
@ -1109,23 +1110,6 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
// TODO: Can the user consent to which claims are released? Today as we don't support most
|
// TODO: Can the user consent to which claims are released? Today as we don't support most
|
||||||
// of them anyway, no, but in the future, we can stash these to the consent req.
|
// of them anyway, no, but in the future, we can stash these to the consent req.
|
||||||
|
|
||||||
admin_warn!("prefer_short_username: {:?}", o2rs.prefer_short_username);
|
|
||||||
let preferred_username = if o2rs.prefer_short_username {
|
|
||||||
Some(code_xchg.uat.name().to_string())
|
|
||||||
} else {
|
|
||||||
Some(code_xchg.uat.spn.clone())
|
|
||||||
};
|
|
||||||
|
|
||||||
let (email, email_verified) = if scope_set.contains("email") {
|
|
||||||
if let Some(mp) = code_xchg.uat.mail_primary {
|
|
||||||
(Some(mp), Some(true))
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: If max_age was requested in the request, we MUST provide auth_time.
|
// TODO: If max_age was requested in the request, we MUST provide auth_time.
|
||||||
|
|
||||||
// amr == auth method
|
// amr == auth method
|
||||||
|
@ -1136,6 +1120,19 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
|
|
||||||
let iss = o2rs.iss.clone();
|
let iss = o2rs.iss.clone();
|
||||||
|
|
||||||
|
let entry = match idms.qs_read.internal_search_uuid(code_xchg.uat.uuid) {
|
||||||
|
Ok(entry) => entry,
|
||||||
|
Err(err) => return Err(Oauth2Error::ServerError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let account = match Account::try_from_entry_ro(&entry, &idms.qs_read) {
|
||||||
|
Ok(account) => account,
|
||||||
|
Err(err) => return Err(Oauth2Error::ServerError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let s_claims = s_claims_for_account(o2rs, &account, &code_xchg.scopes);
|
||||||
|
let extra_claims = extra_claims_for_account(&account, &code_xchg.scopes);
|
||||||
|
|
||||||
let oidc = OidcToken {
|
let oidc = OidcToken {
|
||||||
iss,
|
iss,
|
||||||
sub: OidcSubject::U(code_xchg.uat.uuid),
|
sub: OidcSubject::U(code_xchg.uat.uuid),
|
||||||
|
@ -1150,17 +1147,8 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
amr,
|
amr,
|
||||||
azp: Some(o2rs.name.clone()),
|
azp: Some(o2rs.name.clone()),
|
||||||
jti: None,
|
jti: None,
|
||||||
s_claims: OidcClaims {
|
s_claims,
|
||||||
// Map from displayname
|
claims: extra_claims,
|
||||||
name: Some(code_xchg.uat.displayname.clone()),
|
|
||||||
// Map from spn
|
|
||||||
scopes: code_xchg.scopes.clone(),
|
|
||||||
email,
|
|
||||||
email_verified,
|
|
||||||
preferred_username,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
claims: Default::default(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!(?oidc);
|
trace!(?oidc);
|
||||||
|
@ -1290,17 +1278,22 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct)
|
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct)
|
||||||
.map_err(|_| admin_error!("Account is not valid"));
|
.map_err(|_| admin_error!("Account is not valid"));
|
||||||
|
|
||||||
let account = match valid {
|
let entry = match valid {
|
||||||
Ok(Some(account)) => account,
|
Ok(Some(entry)) => entry,
|
||||||
_ => {
|
_ => {
|
||||||
security_info!(
|
security_info!(
|
||||||
?uuid,
|
?uuid,
|
||||||
"access token has account not valid, returning inactive"
|
"access token has no account not valid, returning inactive"
|
||||||
);
|
);
|
||||||
return Ok(AccessTokenIntrospectResponse::inactive());
|
return Ok(AccessTokenIntrospectResponse::inactive());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let account = match Account::try_from_entry_no_groups(&entry) {
|
||||||
|
Ok(account) => account,
|
||||||
|
Err(err) => return Err(Oauth2Error::ServerError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
// ==== good to generate response ====
|
// ==== good to generate response ====
|
||||||
|
|
||||||
let scope = if scopes.is_empty() {
|
let scope = if scopes.is_empty() {
|
||||||
|
@ -1382,8 +1375,8 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct)
|
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct)
|
||||||
.map_err(|_| admin_error!("Account is not valid"));
|
.map_err(|_| admin_error!("Account is not valid"));
|
||||||
|
|
||||||
let account = match valid {
|
let entry = match valid {
|
||||||
Ok(Some(account)) => account,
|
Ok(Some(entry)) => entry,
|
||||||
_ => {
|
_ => {
|
||||||
security_info!(
|
security_info!(
|
||||||
?uuid,
|
?uuid,
|
||||||
|
@ -1393,26 +1386,18 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let preferred_username = if o2rs.prefer_short_username {
|
let account = match Account::try_from_entry_ro(&entry, &idms.qs_read) {
|
||||||
Some(account.name)
|
Ok(account) => account,
|
||||||
} else {
|
Err(err) => return Err(Oauth2Error::ServerError(err)),
|
||||||
Some(account.spn)
|
|
||||||
};
|
|
||||||
|
|
||||||
let (email, email_verified) = if scopes.contains(&"email".to_string()) {
|
|
||||||
if let Some(mp) = account.mail_primary {
|
|
||||||
(Some(mp), Some(true))
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let amr = Some(vec![auth_type.to_string()]);
|
let amr = Some(vec![auth_type.to_string()]);
|
||||||
|
|
||||||
let iss = o2rs.iss.clone();
|
let iss = o2rs.iss.clone();
|
||||||
|
|
||||||
|
let s_claims = s_claims_for_account(o2rs, &account, &scopes);
|
||||||
|
let extra_claims = extra_claims_for_account(&account, &scopes);
|
||||||
|
|
||||||
// ==== good to generate response ====
|
// ==== good to generate response ====
|
||||||
|
|
||||||
Ok(OidcToken {
|
Ok(OidcToken {
|
||||||
|
@ -1429,16 +1414,8 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
amr,
|
amr,
|
||||||
azp: Some(client_id.to_string()),
|
azp: Some(client_id.to_string()),
|
||||||
jti: None,
|
jti: None,
|
||||||
s_claims: OidcClaims {
|
s_claims,
|
||||||
// Map from displayname
|
claims: extra_claims,
|
||||||
name: Some(account.displayname.clone()),
|
|
||||||
scopes,
|
|
||||||
preferred_username,
|
|
||||||
email,
|
|
||||||
email_verified,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
claims: Default::default(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// https://openid.net/specs/openid-connect-basic-1_0.html#UserInfoErrorResponse
|
// https://openid.net/specs/openid-connect-basic-1_0.html#UserInfoErrorResponse
|
||||||
|
@ -1577,6 +1554,47 @@ fn parse_basic_authz(client_authz: &str) -> Result<(String, String), Oauth2Error
|
||||||
Ok((client_id.to_string(), secret.to_string()))
|
Ok((client_id.to_string(), secret.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn s_claims_for_account(o2rs: &Oauth2RS, account: &Account, scopes: &[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(&"email".to_string()) {
|
||||||
|
if let Some(mp) = &account.mail_primary {
|
||||||
|
(Some(mp.clone()), Some(true))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
OidcClaims {
|
||||||
|
// Map from displayname
|
||||||
|
name: Some(account.displayname.clone()),
|
||||||
|
scopes: scopes.to_vec(),
|
||||||
|
preferred_username,
|
||||||
|
email,
|
||||||
|
email_verified,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn extra_claims_for_account(
|
||||||
|
account: &Account,
|
||||||
|
scopes: &[String],
|
||||||
|
) -> BTreeMap<String, serde_json::Value> {
|
||||||
|
let mut extra_claims = BTreeMap::new();
|
||||||
|
if scopes.contains(&"groups".to_string()) {
|
||||||
|
extra_claims.insert(
|
||||||
|
"groups".to_string(),
|
||||||
|
account.groups.iter().map(|x| x.to_proto().uuid).collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return extra_claims;
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
@ -1617,7 +1635,8 @@ mod tests {
|
||||||
$ident:expr,
|
$ident:expr,
|
||||||
$uat:expr,
|
$uat:expr,
|
||||||
$ct:expr,
|
$ct:expr,
|
||||||
$code_challenge:expr
|
$code_challenge:expr,
|
||||||
|
$scope:expr
|
||||||
) => {{
|
) => {{
|
||||||
let auth_req = AuthorisationRequest {
|
let auth_req = AuthorisationRequest {
|
||||||
response_type: "code".to_string(),
|
response_type: "code".to_string(),
|
||||||
|
@ -1628,7 +1647,7 @@ mod tests {
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
}),
|
}),
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||||
scope: "openid".to_string(),
|
scope: $scope,
|
||||||
nonce: Some("abcdef".to_string()),
|
nonce: Some("abcdef".to_string()),
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
unknown_keys: Default::default(),
|
unknown_keys: Default::default(),
|
||||||
|
@ -1666,7 +1685,7 @@ mod tests {
|
||||||
// System admins
|
// System admins
|
||||||
(
|
(
|
||||||
"oauth2_rs_scope_map",
|
"oauth2_rs_scope_map",
|
||||||
Value::new_oauthscopemap(UUID_SYSTEM_ADMINS, btreeset!["read".to_string()])
|
Value::new_oauthscopemap(UUID_SYSTEM_ADMINS, btreeset!["groups".to_string()])
|
||||||
.expect("invalid oauthscope")
|
.expect("invalid oauthscope")
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -1766,8 +1785,14 @@ mod tests {
|
||||||
// == Setup the authorisation request
|
// == Setup the authorisation request
|
||||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
// Should be in the consent phase;
|
// Should be in the consent phase;
|
||||||
let consent_token =
|
let consent_token =
|
||||||
|
@ -2020,8 +2045,14 @@ mod tests {
|
||||||
|
|
||||||
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
let consent_token = if let AuthoriseResponse::ConsentRequested {
|
let consent_token = if let AuthoriseResponse::ConsentRequested {
|
||||||
consent_token, ..
|
consent_token, ..
|
||||||
|
@ -2093,8 +2124,14 @@ mod tests {
|
||||||
|
|
||||||
// == Setup the authorisation request
|
// == Setup the authorisation request
|
||||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
let consent_token =
|
let consent_token =
|
||||||
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
||||||
|
@ -2255,8 +2292,14 @@ mod tests {
|
||||||
|
|
||||||
// == Setup the authorisation request
|
// == Setup the authorisation request
|
||||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
let consent_token =
|
let consent_token =
|
||||||
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
||||||
|
@ -2364,8 +2407,14 @@ mod tests {
|
||||||
|
|
||||||
// == Setup the authorisation request
|
// == Setup the authorisation request
|
||||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
let consent_token =
|
let consent_token =
|
||||||
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
||||||
|
@ -2539,8 +2588,14 @@ mod tests {
|
||||||
|
|
||||||
// == Setup the authorisation request
|
// == Setup the authorisation request
|
||||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
let consent_token =
|
let consent_token =
|
||||||
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
||||||
|
@ -2663,8 +2718,14 @@ mod tests {
|
||||||
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
// Check reject behaviour
|
// Check reject behaviour
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
let consent_token = if let AuthoriseResponse::ConsentRequested {
|
let consent_token = if let AuthoriseResponse::ConsentRequested {
|
||||||
consent_token, ..
|
consent_token, ..
|
||||||
|
@ -2804,8 +2865,8 @@ mod tests {
|
||||||
assert!(
|
assert!(
|
||||||
discovery.scopes_supported
|
discovery.scopes_supported
|
||||||
== Some(vec![
|
== Some(vec![
|
||||||
|
"groups".to_string(),
|
||||||
"openid".to_string(),
|
"openid".to_string(),
|
||||||
"read".to_string(),
|
|
||||||
"supplement".to_string(),
|
"supplement".to_string(),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
@ -2870,8 +2931,14 @@ mod tests {
|
||||||
|
|
||||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
let consent_token =
|
let consent_token =
|
||||||
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
||||||
|
@ -3002,8 +3069,14 @@ mod tests {
|
||||||
|
|
||||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
let consent_token =
|
let consent_token =
|
||||||
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
||||||
|
@ -3078,6 +3151,116 @@ mod tests {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_idm_oauth2_openid_group_claims() {
|
||||||
|
run_idm_test!(
|
||||||
|
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||||
|
// we run the same test as test_idm_oauth2_openid_extensions()
|
||||||
|
// but change the preferred_username setting on the RS
|
||||||
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
let (secret, uat, ident, _) =
|
||||||
|
setup_oauth2_resource_server(idms, ct, true, false, true);
|
||||||
|
let client_authz = Some(base64::encode(format!("test_resource_server:{}", secret)));
|
||||||
|
|
||||||
|
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||||
|
|
||||||
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
|
let consent_request = good_authorisation_request!(
|
||||||
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid groups".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let consent_token =
|
||||||
|
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
||||||
|
consent_request
|
||||||
|
{
|
||||||
|
consent_token
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
|
||||||
|
// == Manually submit the consent token to the permit for the permit_success
|
||||||
|
let permit_success = idms_prox_read
|
||||||
|
.check_oauth2_authorise_permit(&ident, &uat, &consent_token, ct)
|
||||||
|
.expect("Failed to perform oauth2 permit");
|
||||||
|
|
||||||
|
// Assert that the consent was submitted
|
||||||
|
match idms_delayed.async_rx.blocking_recv() {
|
||||||
|
Some(DelayedAction::Oauth2ConsentGrant(_)) => {}
|
||||||
|
_ => assert!(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
// == Submit the token exchange code.
|
||||||
|
let token_req = AccessTokenRequest {
|
||||||
|
grant_type: "authorization_code".to_string(),
|
||||||
|
code: permit_success.code,
|
||||||
|
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||||
|
client_id: None,
|
||||||
|
client_secret: None,
|
||||||
|
// From the first step.
|
||||||
|
code_verifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
let token_response = idms_prox_read
|
||||||
|
.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct)
|
||||||
|
.expect("Failed to perform oauth2 token exchange");
|
||||||
|
|
||||||
|
// Assert that the session creation was submitted
|
||||||
|
match idms_delayed.async_rx.blocking_recv() {
|
||||||
|
Some(DelayedAction::Oauth2SessionRecord(_)) => {}
|
||||||
|
_ => assert!(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
let id_token = token_response.id_token.expect("No id_token in response!");
|
||||||
|
let access_token = token_response.access_token;
|
||||||
|
|
||||||
|
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 =
|
||||||
|
JwsValidator::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 = oidc_unverified
|
||||||
|
.validate(&jws_validator, iat)
|
||||||
|
.expect("Failed to verify oidc");
|
||||||
|
|
||||||
|
// does our id_token contain the expected groups?
|
||||||
|
assert!(oidc.claims.contains_key(&"groups".to_string()));
|
||||||
|
|
||||||
|
assert!(oidc
|
||||||
|
.claims
|
||||||
|
.get(&"groups".to_string())
|
||||||
|
.expect("unable to find key")
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.contains(&serde_json::json!(STR_UUID_IDM_ALL_ACCOUNTS)));
|
||||||
|
|
||||||
|
// Do the id_token details line up to the userinfo?
|
||||||
|
let userinfo = idms_prox_read
|
||||||
|
.oauth2_openid_userinfo("test_resource_server", &access_token, ct)
|
||||||
|
.expect("failed to get userinfo");
|
||||||
|
|
||||||
|
// does the userinfo endpoint provide the same groups?
|
||||||
|
assert!(
|
||||||
|
oidc.claims.get(&"groups".to_string())
|
||||||
|
== userinfo.claims.get(&"groups".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Check insecure pkce behaviour.
|
// Check insecure pkce behaviour.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_idm_oauth2_insecure_pkce() {
|
fn test_idm_oauth2_insecure_pkce() {
|
||||||
|
@ -3094,8 +3277,14 @@ mod tests {
|
||||||
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
// Even in disable pkce mode, we will allow pkce
|
// Even in disable pkce mode, we will allow pkce
|
||||||
let _consent_request =
|
let _consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
// Check we allow none.
|
// Check we allow none.
|
||||||
let auth_req = AuthorisationRequest {
|
let auth_req = AuthorisationRequest {
|
||||||
|
@ -3155,8 +3344,14 @@ mod tests {
|
||||||
// Check that the id_token is signed with the correct key.
|
// Check that the id_token is signed with the correct key.
|
||||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
let consent_token =
|
let consent_token =
|
||||||
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
||||||
|
@ -3231,8 +3426,14 @@ mod tests {
|
||||||
let idms_prox_read = task::block_on(idms.proxy_read());
|
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||||
|
|
||||||
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
// Should be in the consent phase;
|
// Should be in the consent phase;
|
||||||
let consent_token =
|
let consent_token =
|
||||||
|
@ -3271,8 +3472,14 @@ mod tests {
|
||||||
.expect("Unable to process uat");
|
.expect("Unable to process uat");
|
||||||
|
|
||||||
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
// Should be in the consent phase;
|
// Should be in the consent phase;
|
||||||
let _permit_success =
|
let _permit_success =
|
||||||
|
@ -3435,8 +3642,14 @@ mod tests {
|
||||||
let idms_prox_read = task::block_on(idms.proxy_read());
|
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||||
|
|
||||||
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
// Should be in the consent phase;
|
// Should be in the consent phase;
|
||||||
let consent_token =
|
let consent_token =
|
||||||
|
@ -3635,8 +3848,14 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
// This does have https
|
// This does have https
|
||||||
let consent_request =
|
let consent_request = good_authorisation_request!(
|
||||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
&uat,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
"openid".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
// Should be in the consent phase;
|
// Should be in the consent phase;
|
||||||
let consent_token =
|
let consent_token =
|
||||||
|
|
|
@ -580,7 +580,7 @@ pub trait IdmServerTransaction<'a> {
|
||||||
parent_session_id: Uuid,
|
parent_session_id: Uuid,
|
||||||
iat: i64,
|
iat: i64,
|
||||||
ct: Duration,
|
ct: Duration,
|
||||||
) -> Result<Option<Account>, OperationError> {
|
) -> Result<Option<Arc<Entry<EntrySealed, EntryCommitted>>>, OperationError> {
|
||||||
let entry = self.get_qs_txn().internal_search_uuid(uuid).map_err(|e| {
|
let entry = self.get_qs_txn().internal_search_uuid(uuid).map_err(|e| {
|
||||||
admin_error!(?e, "check_oauth2_account_uuid_valid failed");
|
admin_error!(?e, "check_oauth2_account_uuid_valid failed");
|
||||||
e
|
e
|
||||||
|
@ -620,7 +620,7 @@ pub trait IdmServerTransaction<'a> {
|
||||||
security_info!("The token grace window is in effect. Assuming valid.");
|
security_info!("The token grace window is in effect. Assuming valid.");
|
||||||
};
|
};
|
||||||
|
|
||||||
Account::try_from_entry_no_groups(entry.as_ref()).map(Some)
|
Ok(Some(entry))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For any event/operation to proceed, we need to attach an identity to the
|
/// For any event/operation to proceed, we need to attach an identity to the
|
||||||
|
@ -1553,7 +1553,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
ct: Duration,
|
ct: Duration,
|
||||||
) -> Result<AccessTokenResponse, Oauth2Error> {
|
) -> Result<AccessTokenResponse, Oauth2Error> {
|
||||||
self.oauth2rs
|
self.oauth2rs
|
||||||
.check_oauth2_token_exchange(client_authz, token_req, ct, &self.async_tx)
|
.check_oauth2_token_exchange(self, client_authz, token_req, ct, &self.async_tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_oauth2_token_introspect(
|
pub fn check_oauth2_token_introspect(
|
||||||
|
|
Loading…
Reference in a new issue