mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Review oauth2 best practices document (#1181)
This commit is contained in:
parent
1fe97a9879
commit
2e864be37f
|
@ -305,6 +305,8 @@ async fn oauth2_authorise(
|
||||||
state,
|
state,
|
||||||
code,
|
code,
|
||||||
})) => {
|
})) => {
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11
|
||||||
|
// We could consider changing this to 303?
|
||||||
let mut res = tide::Response::new(302);
|
let mut res = tide::Response::new(302);
|
||||||
|
|
||||||
redirect_uri
|
redirect_uri
|
||||||
|
@ -411,6 +413,8 @@ async fn oauth2_authorise_permit(
|
||||||
state,
|
state,
|
||||||
code,
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11
|
||||||
|
// We could consider changing this to 303?
|
||||||
let mut res = tide::Response::new(302);
|
let mut res = tide::Response::new(302);
|
||||||
redirect_uri
|
redirect_uri
|
||||||
.query_pairs_mut()
|
.query_pairs_mut()
|
||||||
|
|
|
@ -174,6 +174,7 @@ pub struct Oauth2RS {
|
||||||
displayname: String,
|
displayname: String,
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
origin: Origin,
|
origin: Origin,
|
||||||
|
origin_https: bool,
|
||||||
scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
// Client Auth Type (basic is all we support for now.
|
// Client Auth Type (basic is all we support for now.
|
||||||
|
@ -290,9 +291,9 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.ok_or(OperationError::InvalidValueState)?;
|
.ok_or(OperationError::InvalidValueState)?;
|
||||||
trace!("origin");
|
trace!("origin");
|
||||||
let origin = ent
|
let (origin, origin_https) = ent
|
||||||
.get_ava_single_url("oauth2_rs_origin")
|
.get_ava_single_url("oauth2_rs_origin")
|
||||||
.map(|url| url.origin())
|
.map(|url| (url.origin(), url.scheme() == "https"))
|
||||||
.ok_or(OperationError::InvalidValueState)?;
|
.ok_or(OperationError::InvalidValueState)?;
|
||||||
trace!("authz_secret");
|
trace!("authz_secret");
|
||||||
let authz_secret = ent
|
let authz_secret = ent
|
||||||
|
@ -396,6 +397,7 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
displayname,
|
displayname,
|
||||||
uuid,
|
uuid,
|
||||||
origin,
|
origin,
|
||||||
|
origin_https,
|
||||||
scope_maps,
|
scope_maps,
|
||||||
sup_scope_maps,
|
sup_scope_maps,
|
||||||
authz_secret,
|
authz_secret,
|
||||||
|
@ -477,6 +479,14 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
return Err(Oauth2Error::InvalidOrigin);
|
return Err(Oauth2Error::InvalidOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o2rs.origin_https && auth_req.redirect_uri.scheme() != "https" {
|
||||||
|
admin_warn!(
|
||||||
|
origin = ?o2rs.origin,
|
||||||
|
"Invalid oauth2 redirect_uri (must be https for secure origin) - got {:?}", auth_req.redirect_uri.scheme()
|
||||||
|
);
|
||||||
|
return Err(Oauth2Error::InvalidOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
let code_challenge = if let Some(pkce_request) = &auth_req.pkce_request {
|
let code_challenge = if let Some(pkce_request) = &auth_req.pkce_request {
|
||||||
if !o2rs.enable_pkce {
|
if !o2rs.enable_pkce {
|
||||||
security_info!(?o2rs.name, "Insecure rs configuration - pkce is not enforced, but rs is requesting it!");
|
security_info!(?o2rs.name, "Insecure rs configuration - pkce is not enforced, but rs is requesting it!");
|
||||||
|
@ -929,7 +939,7 @@ impl Oauth2ResourceServersReadTransaction {
|
||||||
// Validate the redirect_uri is the same as the original.
|
// Validate the redirect_uri is the same as the original.
|
||||||
if token_req.redirect_uri != code_xchg.redirect_uri {
|
if token_req.redirect_uri != code_xchg.redirect_uri {
|
||||||
security_info!("Invalid oauth2 redirect_uri (differs from original request uri)");
|
security_info!("Invalid oauth2 redirect_uri (differs from original request uri)");
|
||||||
return Err(Oauth2Error::InvalidRequest);
|
return Err(Oauth2Error::InvalidOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==== We are now GOOD TO GO! ====
|
// ==== We are now GOOD TO GO! ====
|
||||||
|
@ -2027,7 +2037,7 @@ mod tests {
|
||||||
idms_prox_read
|
idms_prox_read
|
||||||
.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct)
|
.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct)
|
||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
== Oauth2Error::InvalidRequest
|
== Oauth2Error::InvalidOrigin
|
||||||
);
|
);
|
||||||
|
|
||||||
// * code verifier incorrect
|
// * code verifier incorrect
|
||||||
|
@ -3001,4 +3011,92 @@ mod tests {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.1
|
||||||
|
//
|
||||||
|
// If the origin configured is https, do not allow downgrading to http on redirect
|
||||||
|
fn test_idm_oauth2_redir_http_downgrade() {
|
||||||
|
run_idm_test!(
|
||||||
|
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||||
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
// Enable pkce is set to FALSE
|
||||||
|
let (secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, false, false);
|
||||||
|
|
||||||
|
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||||
|
|
||||||
|
// Get an ident/uat for now.
|
||||||
|
|
||||||
|
// == Setup the authorisation request
|
||||||
|
// We attempt pkce even though the rs is set to not support pkce.
|
||||||
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
|
// First, NOTE the lack of https on the redir uri.
|
||||||
|
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: Base64UrlSafeData(code_challenge.clone()),
|
||||||
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
|
}),
|
||||||
|
redirect_uri: Url::parse("http://demo.example.com/oauth2/result").unwrap(),
|
||||||
|
scope: "openid".to_string(),
|
||||||
|
nonce: None,
|
||||||
|
oidc_ext: Default::default(),
|
||||||
|
unknown_keys: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
idms_prox_read
|
||||||
|
.check_oauth2_authorisation(&ident, &uat, &auth_req, ct)
|
||||||
|
.unwrap_err()
|
||||||
|
== Oauth2Error::InvalidOrigin
|
||||||
|
);
|
||||||
|
|
||||||
|
// This does have https
|
||||||
|
let consent_request =
|
||||||
|
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
||||||
|
|
||||||
|
// Should be in the consent phase;
|
||||||
|
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.
|
||||||
|
// NOTE the url is http again
|
||||||
|
let token_req = AccessTokenRequest {
|
||||||
|
grant_type: "authorization_code".to_string(),
|
||||||
|
code: permit_success.code,
|
||||||
|
redirect_uri: Url::parse("http://demo.example.com/oauth2/result").unwrap(),
|
||||||
|
client_id: Some("test_resource_server".to_string()),
|
||||||
|
client_secret: Some(secret),
|
||||||
|
// Note the code verifier is set to "something else"
|
||||||
|
code_verifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assert the exchange fails.
|
||||||
|
assert!(matches!(
|
||||||
|
idms_prox_read.check_oauth2_token_exchange(None, &token_req, ct),
|
||||||
|
Err(Oauth2Error::InvalidOrigin)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue