636 consent remembering in oauth2 (#824)

This commit is contained in:
Firstyear 2022-06-20 11:37:39 +10:00 committed by GitHub
parent 4a1df985b9
commit 9d929b876c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1157 additions and 555 deletions

View file

@ -57,18 +57,23 @@ pub struct AuthorisationRequestOidc {
pub acr: Option<String>,
}
/// We ask our user to consent to this Authorisation Request with the
/// following data.
/// When we request to authorise, it can either prompt us for consent,
/// or it can immediately be granted due the past grant.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ConsentRequest {
pub enum AuthorisationResponse {
ConsentRequested {
// A pretty-name of the client
pub client_name: String,
client_name: String,
// A list of scopes requested / to be issued.
pub scopes: Vec<String>,
scopes: Vec<String>,
// Extra PII that may be requested
pii_scopes: Vec<String>,
// The users displayname (?)
// pub display_name: String,
// The token we need to be given back to allow this to proceed
pub consent_token: String,
consent_token: String,
},
Permitted,
}
// The resource server then contacts the token endpoint with

View file

@ -11,8 +11,8 @@ LABEL maintainer william@blackhats.net.au
RUN zypper install -y \
cargo \
rust \
gcc clang lld \
rust wasm-pack \
clang lld \
make automake autoconf \
libopenssl-devel pam-devel \
sqlite3-devel \
@ -20,7 +20,6 @@ RUN zypper install -y \
zypper clean -a
COPY . /usr/src/kanidm
WORKDIR /usr/src/kanidm/kanidmd/daemon
ARG SCCACHE_REDIS=""
ARG KANIDM_FEATURES
@ -31,8 +30,21 @@ RUN mkdir /scratch
RUN echo $KANIDM_BUILD_PROFILE
RUN echo $KANIDM_FEATURES
ENV RUSTFLAGS="-Clinker=clang -Clink-arg=-fuse-ld=/usr/bin/ld.lld"
ENV CARGO_HOME=/scratch/.cargo
ENV RUSTFLAGS="-Clinker=clang"
WORKDIR /usr/src/kanidm/kanidmd_web_ui
RUN if [ "${SCCACHE_REDIS}" != "" ]; \
then \
export CARGO_INCREMENTAL=false && \
export RUSTC_WRAPPER=sccache && \
sccache --start-server; \
fi && \
./build_wasm_dev.sh
WORKDIR /usr/src/kanidm/kanidmd/daemon
ENV RUSTFLAGS="-Clinker=clang -Clink-arg=-fuse-ld=/usr/bin/ld.lld"
RUN if [ "${SCCACHE_REDIS}" != "" ]; \
then \

View file

@ -19,7 +19,7 @@ path = "src/main.rs"
[dependencies]
kanidm = { path = "../idm" }
score = { path = "../score" }
clap = { version = "^3.2", features = ["derive"] }
clap = { version = "^3.2", features = ["derive", "env"] }
users = "^0.11.0"
serde = { version = "^1.0.137", features = ["derive"] }
tokio = { version = "^1.19.1", features = ["rt-multi-thread", "macros", "signal"] }

View file

@ -20,8 +20,8 @@ use kanidm_proto::v1::{BackupCodesView, OperationError, RadiusAuthToken};
use crate::filter::{Filter, FilterInvalid};
use crate::idm::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess, ConsentRequest, JwkKeySet,
Oauth2Error, OidcDiscoveryResponse, OidcToken,
AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess, AuthoriseResponse,
JwkKeySet, Oauth2Error, OidcDiscoveryResponse, OidcToken,
};
use crate::idm::server::{IdmServer, IdmServerTransaction};
use crate::ldap::{LdapBoundToken, LdapResponseState, LdapServer};
@ -1120,7 +1120,7 @@ impl QueryServerReadV1 {
uat: Option<String>,
auth_req: AuthorisationRequest,
eventid: Uuid,
) -> Result<ConsentRequest, Oauth2Error> {
) -> Result<AuthoriseResponse, Oauth2Error> {
let ct = duration_from_epoch_now();
let idms_prox_read = self.idms.proxy_read_async().await;
let res = spanned!("actors::v1_read::handle<Oauth2Authorise>", {

View file

@ -718,6 +718,37 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES: &str = r#"{
}
}"#;
pub const JSON_SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP: &str = r#"{
"attrs": {
"class": [
"object",
"system",
"attributetype"
],
"description": [
"A set of scopes mapped from a relying server to a user, where the user has previously consented to the following. If changed or deleted, consent will be re-sought."
],
"index": [
"EQUALITY"
],
"unique": [
"false"
],
"multivalue": [
"true"
],
"attributename": [
"oauth2_consent_scope_map"
],
"syntax": [
"OAUTH_SCOPE_MAP"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000097"
]
}
}"#;
pub const JSON_SCHEMA_ATTR_ES256_PRIVATE_KEY_DER: &str = r#"{
"attrs": {
"class": [
@ -973,7 +1004,8 @@ pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#"
"radius_secret",
"account_expire",
"account_valid_from",
"mail"
"mail",
"oauth2_consent_scope_map"
],
"systemmust": [
"displayname",

View file

@ -167,6 +167,8 @@ pub const UUID_SCHEMA_ATTR_FERNET_PRIVATE_KEY_STR: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000095");
pub const _UUID_SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000096");
pub const _UUID_SCHEMA_CLASS_OAUTH2_CONSENT_SCOPE_MAP: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000097");
// System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations.

View file

@ -6,6 +6,7 @@
use crate::prelude::*;
use kanidm_proto::v1::UserAuthToken;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::hash::Hash;
use std::sync::Arc;
@ -161,4 +162,14 @@ impl Identity {
.attribute_equality("memberof", &PartialValue::new_refer(group)),
}
}
pub fn get_oauth2_consent_scopes(&self, oauth2_rs: Uuid) -> Option<&BTreeSet<String>> {
match &self.origin {
IdentType::Internal => None,
IdentType::User(u) => u
.entry
.get_ava_as_oauthscopemaps("oauth2_consent_scope_map")
.and_then(|scope_map| scope_map.get(&oauth2_rs)),
}
}
}

View file

@ -6,6 +6,7 @@ pub(crate) enum DelayedAction {
UnixPwUpgrade(UnixPasswordUpgrade),
WebauthnCounterIncrement(WebauthnCounterIncrement),
BackupCodeRemoval(BackupCodeRemoval),
Oauth2ConsentGrant(Oauth2ConsentGrant),
}
pub(crate) struct PasswordUpgrade {
@ -28,3 +29,9 @@ pub(crate) struct BackupCodeRemoval {
pub target_uuid: Uuid,
pub code_to_remove: String,
}
pub(crate) struct Oauth2ConsentGrant {
pub target_uuid: Uuid,
pub oauth2_rs_uuid: Uuid,
pub scopes: Vec<String>,
}

View file

@ -6,6 +6,7 @@
//!
use crate::identity::IdentityId;
use crate::idm::delayed::{DelayedAction, Oauth2ConsentGrant};
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerTransaction};
use crate::prelude::*;
use crate::value::OAUTHSCOPE_RE;
@ -21,13 +22,14 @@ use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::mpsc::UnboundedSender as Sender;
use tracing::trace;
use url::{Origin, Url};
use webauthn_rs::base64_data::Base64UrlSafeData;
pub use kanidm_proto::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ConsentRequest, ErrorResponse,
AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse,
OidcDiscoveryResponse,
};
use kanidm_proto::oauth2::{
@ -149,6 +151,23 @@ struct Oauth2AccessToken {
pub auth_time: Option<i64>,
}
#[derive(Debug)]
pub enum AuthoriseResponse {
ConsentRequested {
// A pretty-name of the client
client_name: String,
// A list of scopes requested / to be issued.
scopes: Vec<String>,
// Extra PII that may be requested
pii_scopes: Vec<String>,
// The users displayname (?)
// pub display_name: String,
// The token we need to be given back to allow this to proceed
consent_token: String,
},
Permitted(AuthorisePermitSuccess),
}
#[derive(Debug)]
pub struct AuthorisePermitSuccess {
// Where the RS wants us to go back to.
@ -417,7 +436,7 @@ impl Oauth2ResourceServersReadTransaction {
uat: &UserAuthToken,
auth_req: &AuthorisationRequest,
ct: Duration,
) -> Result<ConsentRequest, Oauth2Error> {
) -> Result<AuthoriseResponse, Oauth2Error> {
// due to identity processing we already know that:
// * the session must be authenticated, and valid
// * is within it's valid time window.
@ -500,14 +519,17 @@ impl Oauth2ResourceServersReadTransaction {
}
// scopes - you need to have every requested scope or this req is denied.
let req_scopes: BTreeSet<_> = auth_req.scope.split_ascii_whitespace().collect();
let req_scopes: BTreeSet<String> = auth_req
.scope
.split_ascii_whitespace()
.map(str::to_string)
.collect();
if req_scopes.is_empty() {
admin_error!("Invalid oauth2 request - must contain at least one requested scope");
return Err(Oauth2Error::InvalidRequest);
}
// TODO: Check the scopes by our scope RE rules.
// Oauth2Error::InvalidScope
// Check the scopes by our scope regex validation rules.
if !req_scopes.iter().all(|s| OAUTHSCOPE_RE.is_match(s)) {
admin_error!(
"Invalid oauth2 request - requested scopes failed to pass validation rules"
@ -515,22 +537,22 @@ impl Oauth2ResourceServersReadTransaction {
return Err(Oauth2Error::InvalidScope);
}
let uat_scopes: BTreeSet<_> = o2rs
let uat_scopes: BTreeSet<String> = o2rs
.implicit_scopes
.iter()
.map(|s| s.as_str())
.chain(
o2rs.scope_maps
.iter()
.filter_map(|(u, m)| {
if ident.is_memberof(*u) {
Some(m.iter().map(|s| s.as_str()))
Some(m.iter())
} else {
None
}
})
.flatten(),
)
.cloned()
.collect();
// Needs to use s.to_string due to &&str which can't use the str::to_string
@ -548,6 +570,64 @@ impl Oauth2ResourceServersReadTransaction {
return Err(Oauth2Error::AccessDenied);
}
let consent_previously_granted =
if let Some(consent_scopes) = ident.get_oauth2_consent_scopes(o2rs.uuid) {
req_scopes.eq(consent_scopes)
} else {
false
};
if consent_previously_granted {
admin_info!(
"User has previously consented, permitting. {:?}",
req_scopes
);
// Setup for the permit success
let xchg_code = TokenExchangeCode {
uat: uat.clone(),
code_challenge,
redirect_uri: auth_req.redirect_uri.clone(),
scopes: avail_scopes.clone(),
nonce: auth_req.nonce.clone(),
};
// Encrypt the exchange token with the fernet key of the client resource server
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 {
// Check that the scopes are the same as a previous consent (if any)
// If oidc, what PII is visible?
// 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)
// * email - (email, email\_verified)
// * address - (address)
// * phone - (phone\_number, phone\_number\_verified)
//
// https://openid.net/specs/openid-connect-basic-1_0.html#StandardClaims
let pii_scopes = if req_scopes.contains("openid") {
let mut pii_scopes = Vec::with_capacity(2);
if req_scopes.contains("email") {
pii_scopes.push("email".to_string());
pii_scopes.push("email_verified".to_string());
}
pii_scopes
} else {
Vec::with_capacity(0)
};
// Subseqent we then return an encrypted session handle which allows
// the user to indicate their consent to this authorisation.
//
@ -574,19 +654,22 @@ impl Oauth2ResourceServersReadTransaction {
.fernet
.encrypt_at_time(&consent_data, ct.as_secs());
Ok(ConsentRequest {
Ok(AuthoriseResponse::ConsentRequested {
client_name: o2rs.displayname.clone(),
scopes: avail_scopes,
pii_scopes,
consent_token,
})
}
}
pub fn check_oauth2_authorise_permit(
pub(crate) fn check_oauth2_authorise_permit(
&self,
ident: &Identity,
uat: &UserAuthToken,
consent_token: &str,
ct: Duration,
async_tx: &Sender<DelayedAction>,
) -> Result<AuthorisePermitSuccess, OperationError> {
// Decode the consent req with our system fernet key. Use a ttl of 5 minutes.
let consent_req: ConsentToken = self
@ -627,12 +710,11 @@ impl Oauth2ResourceServersReadTransaction {
})?;
// Extract the state, code challenge, redirect_uri
let xchg_code = TokenExchangeCode {
uat: uat.clone(),
code_challenge: consent_req.code_challenge,
redirect_uri: consent_req.redirect_uri.clone(),
scopes: consent_req.scopes,
scopes: consent_req.scopes.clone(),
nonce: consent_req.nonce,
};
@ -644,6 +726,17 @@ impl Oauth2ResourceServersReadTransaction {
let code = o2rs.token_fernet.encrypt_at_time(&code_data, ct.as_secs());
// Everything is DONE! Now submit that it's all happy and the user consented correctly.
// this will let them bypass consent steps in the future.
// Submit that we consented to the delayed action queue
if let Err(_) = async_tx.send(DelayedAction::Oauth2ConsentGrant(Oauth2ConsentGrant {
target_uuid: uat.uuid,
oauth2_rs_uuid: o2rs.uuid,
scopes: consent_req.scopes,
})) {
admin_warn!("unable to queue delayed oauth2 consent grant, continuing ... ");
}
Ok(AuthorisePermitSuccess {
redirect_uri: consent_req.redirect_uri,
state: consent_req.state,
@ -706,6 +799,9 @@ impl Oauth2ResourceServersReadTransaction {
token_req: &AccessTokenRequest,
ct: Duration,
) -> Result<AccessTokenResponse, Oauth2Error> {
// TODO: add refresh token grant type.
// If it's a refresh token grant, are the consent permissions the same?
if token_req.grant_type != "authorization_code" {
admin_warn!("Invalid oauth2 grant_type (should be 'authorization_code')");
return Err(Oauth2Error::InvalidRequest);
@ -907,11 +1003,13 @@ impl Oauth2ResourceServersReadTransaction {
.token_fernet
.encrypt_at_time(&access_token_data, ct.as_secs());
let refresh_token = None;
Ok(AccessTokenResponse {
access_token,
token_type: "bearer".to_string(),
expires_in,
refresh_token: None,
refresh_token,
scope,
id_token,
})
@ -1237,11 +1335,12 @@ fn parse_basic_authz(client_authz: &str) -> Result<(String, String), Oauth2Error
#[cfg(test)]
mod tests {
use crate::event::CreateEvent;
use crate::idm::oauth2::Oauth2Error;
use crate::idm::delayed::DelayedAction;
use crate::idm::oauth2::{AuthoriseResponse, Oauth2Error};
use crate::idm::server::{IdmServer, IdmServerTransaction};
use crate::prelude::*;
use crate::event::ModifyEvent;
use crate::event::{DeleteEvent, ModifyEvent};
use kanidm_proto::oauth2::*;
use kanidm_proto::v1::{AuthType, UserAuthToken};
@ -1304,7 +1403,7 @@ mod tests {
ct: Duration,
enable_pkce: bool,
enable_legacy_crypto: bool,
) -> (String, UserAuthToken, Identity) {
) -> (String, UserAuthToken, Identity, Uuid) {
let mut idms_prox_write = idms.proxy_write(ct);
let uuid = Uuid::new_v4();
@ -1365,7 +1464,7 @@ mod tests {
idms_prox_write.commit().expect("failed to commit");
(secret, uat, ident)
(secret, uat, ident, uuid)
}
fn setup_idm_admin(
@ -1392,11 +1491,10 @@ mod tests {
#[test]
fn test_idm_oauth2_basic_function() {
run_idm_test!(|_qs: &QueryServer,
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed| {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident) = setup_oauth2_resource_server(idms, ct, true, false);
let (secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, true, false);
let idms_prox_read = idms.proxy_read();
@ -1408,11 +1506,27 @@ mod tests {
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_request.consent_token, ct)
.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),
}
// Check we are reflecting the CSRF properly.
assert!(permit_success.state == "123");
@ -1434,7 +1548,8 @@ mod tests {
// 🎉 We got a token! In the future we can then check introspection from this point.
assert!(token_response.token_type == "bearer");
})
}
)
}
#[test]
@ -1444,7 +1559,7 @@ mod tests {
_idms_delayed: &mut IdmServerDelayed| {
// Test invalid oauth2 authorisation states/requests.
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident) = setup_oauth2_resource_server(idms, ct, true, false);
let (_secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, true, false);
let (anon_uat, anon_ident) = setup_idm_admin(idms, ct, AuthType::Anonymous);
let (idm_admin_uat, idm_admin_ident) = setup_idm_admin(idms, ct, AuthType::PasswordMfa);
@ -1608,7 +1723,7 @@ mod tests {
_idms_delayed: &mut IdmServerDelayed| {
// Test invalid oauth2 authorisation states/requests.
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident) = setup_oauth2_resource_server(idms, ct, true, false);
let (_secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, true, false);
let (uat2, ident2) = {
let mut idms_prox_write = idms.proxy_write(ct);
@ -1632,6 +1747,15 @@ mod tests {
let consent_request =
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
let consent_token = if let AuthoriseResponse::ConsentRequested {
consent_token, ..
} = consent_request
{
consent_token
} else {
unreachable!();
};
// Invalid permits
// * expired token, aka past ttl.
assert!(
@ -1639,7 +1763,7 @@ mod tests {
.check_oauth2_authorise_permit(
&ident,
&uat,
&consent_request.consent_token,
&consent_token,
ct + Duration::from_secs(TOKEN_EXPIRE),
)
.unwrap_err()
@ -1652,12 +1776,7 @@ mod tests {
assert!(
idms_prox_read
.check_oauth2_authorise_permit(
&ident2,
&uat,
&consent_request.consent_token,
ct,
)
.check_oauth2_authorise_permit(&ident2, &uat, &consent_token, ct,)
.unwrap_err()
== OperationError::InvalidSessionState
);
@ -1665,12 +1784,7 @@ mod tests {
// * incorrect session id
assert!(
idms_prox_read
.check_oauth2_authorise_permit(
&ident,
&uat2,
&consent_request.consent_token,
ct,
)
.check_oauth2_authorise_permit(&ident, &uat2, &consent_token, ct,)
.unwrap_err()
== OperationError::InvalidSessionState
);
@ -1679,11 +1793,11 @@ mod tests {
#[test]
fn test_idm_oauth2_invalid_token_exchange_requests() {
run_idm_test!(|_qs: &QueryServer,
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed| {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, mut uat, ident) = setup_oauth2_resource_server(idms, ct, true, false);
let (secret, mut uat, ident, _) =
setup_oauth2_resource_server(idms, ct, true, false);
// ⚠️ We set the uat expiry time to 5 seconds from TEST_CURRENT_TIME. This
// allows all our other tests to pass, but it means when we specifically put the
@ -1704,11 +1818,26 @@ mod tests {
let consent_request =
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
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_request.consent_token, ct)
.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.
// Invalid token exchange
@ -1831,16 +1960,16 @@ mod tests {
.unwrap_err()
== Oauth2Error::InvalidRequest
);
})
}
)
}
#[test]
fn test_idm_oauth2_token_introspect() {
run_idm_test!(|_qs: &QueryServer,
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed| {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident) = setup_oauth2_resource_server(idms, ct, true, false);
let (secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, true, false);
let client_authz = Some(base64::encode(format!("test_resource_server:{}", secret)));
let idms_prox_read = idms.proxy_read();
@ -1850,11 +1979,26 @@ mod tests {
let consent_request =
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
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_request.consent_token, ct)
.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),
}
let token_req = AccessTokenRequest {
grant_type: "authorization_code".to_string(),
code: permit_success.code.clone(),
@ -1873,7 +2017,11 @@ mod tests {
token_type_hint: None,
};
let intr_response = idms_prox_read
.check_oauth2_token_introspect(client_authz.as_deref().unwrap(), &intr_request, ct)
.check_oauth2_token_introspect(
client_authz.as_deref().unwrap(),
&intr_request,
ct,
)
.expect("Failed to inspect token");
eprintln!("👉 {:?}", intr_response);
@ -1890,7 +2038,8 @@ mod tests {
let idms_prox_write = idms.proxy_write(ct);
// Expire the account, should cause introspect to return inactive.
let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_CURRENT_TIME - 1));
let v_expire =
Value::new_datetime_epoch(Duration::from_secs(TEST_CURRENT_TIME - 1));
let me_inv_m = unsafe {
ModifyEvent::new_internal_invalid(
filter!(f_eq("name", PartialValue::new_iname("admin"))),
@ -1912,7 +2061,8 @@ mod tests {
.expect("Failed to inspect token");
assert!(!intr_response.active);
})
}
)
}
#[test]
@ -1921,7 +2071,7 @@ mod tests {
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident) = setup_oauth2_resource_server(idms, ct, true, false);
let (_secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, true, false);
let (uat2, ident2) = {
let mut idms_prox_write = idms.proxy_write(ct);
@ -1946,8 +2096,17 @@ mod tests {
let consent_request =
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
let consent_token = if let AuthoriseResponse::ConsentRequested {
consent_token, ..
} = consent_request
{
consent_token
} else {
unreachable!();
};
let reject_success = idms_prox_read
.check_oauth2_authorise_reject(&ident, &uat, &consent_request.consent_token, ct)
.check_oauth2_authorise_reject(&ident, &uat, &consent_token, ct)
.expect("Failed to perform oauth2 reject");
assert!(reject_success == redirect_uri);
@ -1956,12 +2115,7 @@ mod tests {
let past_ct = Duration::from_secs(TEST_CURRENT_TIME + 301);
assert!(
idms_prox_read
.check_oauth2_authorise_reject(
&ident,
&uat,
&consent_request.consent_token,
past_ct
)
.check_oauth2_authorise_reject(&ident, &uat, &consent_token, past_ct)
.unwrap_err()
== OperationError::CryptographyError
);
@ -1977,24 +2131,14 @@ mod tests {
// Wrong UAT
assert!(
idms_prox_read
.check_oauth2_authorise_reject(
&ident,
&uat2,
&consent_request.consent_token,
ct
)
.check_oauth2_authorise_reject(&ident, &uat2, &consent_token, ct)
.unwrap_err()
== OperationError::InvalidSessionState
);
// Wrong ident
assert!(
idms_prox_read
.check_oauth2_authorise_reject(
&ident2,
&uat,
&consent_request.consent_token,
ct
)
.check_oauth2_authorise_reject(&ident2, &uat, &consent_token, ct)
.unwrap_err()
== OperationError::InvalidSessionState
);
@ -2007,7 +2151,7 @@ mod tests {
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, _uat, _ident) = setup_oauth2_resource_server(idms, ct, true, false);
let (_secret, _uat, _ident, _) = setup_oauth2_resource_server(idms, ct, true, false);
let idms_prox_read = idms.proxy_read();
@ -2139,11 +2283,10 @@ mod tests {
#[test]
fn test_idm_oauth2_openid_extensions() {
run_idm_test!(|_qs: &QueryServer,
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed| {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident) = setup_oauth2_resource_server(idms, ct, true, false);
let (secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, true, false);
let client_authz = Some(base64::encode(format!("test_resource_server:{}", secret)));
let idms_prox_read = idms.proxy_read();
@ -2153,11 +2296,26 @@ mod tests {
let consent_request =
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
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_request.consent_token, ct)
.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(),
@ -2240,7 +2398,8 @@ mod tests {
assert!(userinfo.jti.is_none());
assert!(oidc.s_claims == userinfo.s_claims);
assert!(userinfo.claims.is_empty());
})
}
)
}
// Check insecure pkce behaviour.
@ -2250,7 +2409,7 @@ mod tests {
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident) = setup_oauth2_resource_server(idms, ct, false, false);
let (_secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, false, false);
let idms_prox_read = idms.proxy_read();
@ -2282,11 +2441,10 @@ mod tests {
#[test]
fn test_idm_oauth2_openid_legacy_crypto() {
run_idm_test!(|_qs: &QueryServer,
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed| {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (secret, uat, ident) = setup_oauth2_resource_server(idms, ct, false, true);
let (secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, false, true);
let idms_prox_read = idms.proxy_read();
// The public key url should offer an rs key
// discovery should offer RS256
@ -2322,11 +2480,26 @@ mod tests {
let consent_request =
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
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_request.consent_token, ct)
.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(),
@ -2359,6 +2532,209 @@ mod tests {
.expect("Failed to verify oidc");
assert!(oidc.sub == OidcSubject::U(*UUID_ADMIN));
})
}
)
}
#[test]
fn test_idm_oauth2_consent_granted_and_changed_workflow() {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, true, false);
let idms_prox_read = 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);
// 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");
drop(idms_prox_read);
// Assert that the consent was submitted
let o2cg = match idms_delayed.async_rx.blocking_recv() {
Some(DelayedAction::Oauth2ConsentGrant(o2cg)) => o2cg,
_ => unreachable!(),
};
// Manually submit the consent.
let mut idms_prox_write = idms.proxy_write(ct);
assert!(idms_prox_write.process_oauth2consentgrant(&o2cg).is_ok());
assert!(idms_prox_write.commit().is_ok());
// == Now try the authorise again, should be in the permitted state.
let idms_prox_read = idms.proxy_read();
// We need to reload our identity
let ident = idms_prox_read
.process_uat_to_identity(&uat, ct)
.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, &uat, ct, code_challenge);
// Should be in the consent phase;
let _permit_success =
if let AuthoriseResponse::Permitted(permit_success) = consent_request {
permit_success
} else {
unreachable!();
};
drop(idms_prox_read);
// Great! Now change the scopes on the oauth2 instance, this revokes the permit.
let idms_prox_write = idms.proxy_write(ct);
let me_extend_scopes = unsafe {
ModifyEvent::new_internal_invalid(
filter!(f_eq(
"oauth2_rs_name",
PartialValue::new_iname("test_resource_server")
)),
ModifyList::new_list(vec![Modify::Present(
AttrString::from("oauth2_rs_implicit_scopes"),
Value::new_oauthscope("email").expect("invalid oauthscope"),
)]),
)
};
assert!(idms_prox_write.qs_write.modify(&me_extend_scopes).is_ok());
assert!(idms_prox_write.commit().is_ok());
// And do the workflow once more to see if we need to consent again.
let idms_prox_read = idms.proxy_read();
// We need to reload our identity
let ident = idms_prox_read
.process_uat_to_identity(&uat, ct)
.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: Base64UrlSafeData(code_challenge),
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: "openid email".to_string(),
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
unknown_keys: Default::default(),
};
let consent_request = idms_prox_read
.check_oauth2_authorisation(&ident, &uat, &auth_req, ct)
.expect("Oauth2 authorisation failed");
// Should be in the consent phase;
let _consent_token =
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
consent_request
{
consent_token
} else {
unreachable!();
};
// Success! We had to consent again due to the change :)
}
)
}
#[test]
fn test_idm_oauth2_consent_granted_refint_cleanup_on_delete() {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (_secret, uat, ident, o2rs_uuid) =
setup_oauth2_resource_server(idms, ct, true, false);
// Assert there are no consent maps yet.
assert!(ident.get_oauth2_consent_scopes(o2rs_uuid).is_none());
let idms_prox_read = 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);
// 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");
drop(idms_prox_read);
// Assert that the consent was submitted
let o2cg = match idms_delayed.async_rx.blocking_recv() {
Some(DelayedAction::Oauth2ConsentGrant(o2cg)) => o2cg,
_ => unreachable!(),
};
// Manually submit the consent.
let mut idms_prox_write = idms.proxy_write(ct);
assert!(idms_prox_write.process_oauth2consentgrant(&o2cg).is_ok());
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)
.expect("Unable to process uat");
// Assert that the ident now has the consents.
assert!(
ident.get_oauth2_consent_scopes(o2rs_uuid)
== Some(&btreeset!["openid".to_string()])
);
// Now trigger the delete of the RS
let de = unsafe {
DeleteEvent::new_internal_invalid(filter!(f_eq(
"oauth2_rs_name",
PartialValue::new_iname("test_resource_server")
)))
};
trace!("ATTACHHERE");
assert!(idms_prox_write.qs_write.delete(&de).is_ok());
// Assert the consent maps are gone.
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)
.expect("Unable to process uat");
assert!(ident.get_oauth2_consent_scopes(o2rs_uuid).is_none());
assert!(idms_prox_write.commit().is_ok());
}
)
}
}

View file

@ -17,8 +17,8 @@ use crate::idm::event::{
use crate::idm::mfareg::{MfaRegCred, MfaRegNext, MfaRegSession};
use crate::idm::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess, ConsentRequest, JwkKeySet,
Oauth2Error, Oauth2ResourceServers, Oauth2ResourceServersReadTransaction,
AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess, AuthoriseResponse,
JwkKeySet, Oauth2Error, Oauth2ResourceServers, Oauth2ResourceServersReadTransaction,
Oauth2ResourceServersWriteTransaction, OidcDiscoveryResponse, OidcToken,
};
use crate::idm::radius::RadiusAccount;
@ -34,7 +34,8 @@ use crate::utils::{
use crate::actors::v1_write::QueryServerWriteV1;
use crate::idm::delayed::{
DelayedAction, PasswordUpgrade, UnixPasswordUpgrade, WebauthnCounterIncrement,
DelayedAction, Oauth2ConsentGrant, PasswordUpgrade, UnixPasswordUpgrade,
WebauthnCounterIncrement,
};
use hashbrown::HashSet;
@ -138,6 +139,7 @@ pub struct IdmServerProxyReadTransaction<'a> {
pub qs_read: QueryServerReadTransaction<'a>,
uat_jwt_validator: CowCellReadTxn<JwsValidator>,
oauth2rs: Oauth2ResourceServersReadTransaction,
async_tx: Sender<DelayedAction>,
}
pub struct IdmServerProxyWriteTransaction<'a> {
@ -158,7 +160,7 @@ pub struct IdmServerProxyWriteTransaction<'a> {
}
pub struct IdmServerDelayed {
async_rx: Receiver<DelayedAction>,
pub(crate) async_rx: Receiver<DelayedAction>,
}
impl IdmServer {
@ -307,6 +309,7 @@ impl IdmServer {
qs_read: self.qs.read_async().await,
uat_jwt_validator: self.uat_jwt_validator.read(),
oauth2rs: self.oauth2rs.read(),
async_tx: self.async_tx.clone(),
}
}
@ -1177,7 +1180,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
uat: &UserAuthToken,
auth_req: &AuthorisationRequest,
ct: Duration,
) -> Result<ConsentRequest, Oauth2Error> {
) -> Result<AuthoriseResponse, Oauth2Error> {
self.oauth2rs
.check_oauth2_authorisation(ident, uat, auth_req, ct)
}
@ -1190,7 +1193,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
ct: Duration,
) -> Result<AuthorisePermitSuccess, OperationError> {
self.oauth2rs
.check_oauth2_authorise_permit(ident, uat, consent_req, ct)
.check_oauth2_authorise_permit(ident, uat, consent_req, ct, &self.async_tx)
}
pub fn check_oauth2_authorise_reject(
@ -2044,6 +2047,27 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
)
}
pub(crate) fn process_oauth2consentgrant(
&mut self,
o2cg: &Oauth2ConsentGrant,
) -> Result<(), OperationError> {
let modlist = ModifyList::new_list(vec![
Modify::Removed(
AttrString::from("oauth2_consent_scope_map"),
PartialValue::OauthScopeMap(o2cg.oauth2_rs_uuid),
),
Modify::Present(
AttrString::from("oauth2_consent_scope_map"),
Value::OauthScopeMap(o2cg.oauth2_rs_uuid, o2cg.scopes.iter().cloned().collect()),
),
]);
self.qs_write.internal_modify(
&filter_all!(f_eq("uuid", PartialValue::new_uuid(o2cg.target_uuid))),
&modlist,
)
}
pub(crate) fn process_delayedaction(
&mut self,
da: DelayedAction,
@ -2053,6 +2077,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
DelayedAction::UnixPwUpgrade(upwu) => self.process_unixpwupgrade(&upwu),
DelayedAction::WebauthnCounterIncrement(wci) => self.process_webauthncounterinc(&wci),
DelayedAction::BackupCodeRemoval(bcr) => self.process_backupcoderemoval(&bcr),
DelayedAction::Oauth2ConsentGrant(o2cg) => self.process_oauth2consentgrant(&o2cg),
}
}

View file

@ -2318,6 +2318,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
JSON_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER,
JSON_SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN,
JSON_SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP,
JSON_SCHEMA_CLASS_PERSON,
JSON_SCHEMA_CLASS_ORGPERSON,
JSON_SCHEMA_CLASS_GROUP,

View file

@ -2,9 +2,10 @@ use super::v1::{json_rest_event_get, json_rest_event_post};
use super::{to_tide_response, AppState, RequestExtensions};
use kanidm::idm::oauth2::{
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess,
ErrorResponse, Oauth2Error,
AuthoriseResponse, ErrorResponse, Oauth2Error,
};
use kanidm::prelude::*;
use kanidm_proto::oauth2::AuthorisationResponse;
use kanidm_proto::v1::Entry as ProtoEntry;
use serde::{Deserialize, Serialize};
@ -185,7 +186,13 @@ pub async fn oauth2_id_delete(req: tide::Request<AppState>) -> tide::Result {
pub async fn oauth2_authorise_post(mut req: tide::Request<AppState>) -> tide::Result {
let auth_req: AuthorisationRequest = req.body_json().await?;
oauth2_authorise(req, auth_req).await
oauth2_authorise(req, auth_req).await.map(|mut res| {
if res.status() == 302 {
// in post, we need the redirect not to be issued, so we mask 302 to 200
res.set_status(200);
}
res
})
}
pub async fn oauth2_authorise_get(req: tide::Request<AppState>) -> tide::Result {
@ -219,12 +226,43 @@ async fn oauth2_authorise(
.await;
match res {
Ok(consent_req) => {
Ok(AuthoriseResponse::ConsentRequested {
client_name,
scopes,
pii_scopes,
consent_token,
}) => {
// Render a redirect to the consent page for the user to interact with
// to authorise this session-id
let mut res = tide::Response::new(200);
// This is json so later we can expand it with better detail.
tide::Body::from_json(&consent_req).map(|b| {
tide::Body::from_json(&AuthorisationResponse::ConsentRequested {
client_name,
scopes,
pii_scopes,
consent_token,
})
.map(|b| {
res.set_body(b);
res
})
}
Ok(AuthoriseResponse::Permitted(AuthorisePermitSuccess {
mut redirect_uri,
state,
code,
})) => {
let mut res = tide::Response::new(302);
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("state", &state)
.append_pair("code", &code);
res.insert_header("Location", redirect_uri.as_str());
// I think the client server needs this
res.insert_header("Access-Control-Allow-Origin", redirect_uri.origin().ascii_serialization());
tide::Body::from_json(&AuthorisationResponse::Permitted).map(|b| {
res.set_body(b);
res
})
@ -265,8 +303,10 @@ pub async fn oauth2_authorise_permit_post(mut req: tide::Request<AppState>) -> t
oauth2_authorise_permit(req, consent_req)
.await
.map(|mut res| {
if res.status() == 302 {
// in post, we need the redirect not to be issued, so we mask 302 to 200
res.set_status(200);
}
res
})
}
@ -318,7 +358,7 @@ async fn oauth2_authorise_permit(
.append_pair("code", &code);
res.insert_header("Location", redirect_uri.as_str());
// I think the client server needs this
// res.insert_header("Access-Control-Allow-Origin", redirect_uri.origin().ascii_serialization());
res.insert_header("Access-Control-Allow-Origin", redirect_uri.origin().ascii_serialization());
res
}
Err(_e) => {

View file

@ -5,7 +5,7 @@ use crate::common::{setup_async_test, ADMIN_TEST_PASSWORD};
use compact_jwt::{JwkKeySet, JwsValidator, OidcToken, OidcUnverified};
use kanidm_proto::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
AccessTokenResponse, ConsentRequest, OidcDiscoveryResponse,
AccessTokenResponse, AuthorisationResponse, OidcDiscoveryResponse,
};
use oauth2_ext::PkceCodeChallenge;
use std::collections::HashMap;
@ -217,18 +217,25 @@ async fn test_oauth2_openid_basic_flow() {
assert!(response.status() == reqwest::StatusCode::OK);
assert_no_cache!(response);
let consent_req: ConsentRequest = response
let consent_req: AuthorisationResponse = response
.json()
.await
.expect("Failed to access response body");
let consent_token =
if let AuthorisationResponse::ConsentRequested { consent_token, .. } = consent_req {
consent_token
} else {
unreachable!();
};
// Step 2 - we now send the consent get to the server which yields a redirect with a
// state and code.
let response = client
.get(format!("{}/oauth2/authorise/permit", url))
.bearer_auth(admin_uat)
.query(&[("token", consent_req.consent_token.as_str())])
.query(&[("token", consent_token.as_str())])
.send()
.await
.expect("Failed to send request.");

View file

@ -24,7 +24,7 @@ If you want to deploy Kanidm to see what it can do, you should read the kanidm b
- [Kanidm book (Latest stable)](https://kanidm.github.io/kanidm/stable/)
We also publish limited [support guidelines](https://github.com/kanidm/kanidm/blob/master/project_docs/RELEASE_AND_SUPPORT.md)).
We also publish limited [support guidelines](https://github.com/kanidm/kanidm/blob/master/project_docs/RELEASE_AND_SUPPORT.md).
## Code of Conduct / Ethics

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<text y=".9em" font-size="90">🦀</text>
</svg>

Before

Width:  |  Height:  |  Size: 111 B

View file

@ -12,9 +12,9 @@ export interface InitOutput {
readonly __wbindgen_malloc: (a: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
readonly __wbindgen_export_2: WebAssembly.Table;
readonly _dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha6256632cf9b6e15: (a: number, b: number, c: number) => void;
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h026107843485189d: (a: number, b: number, c: number) => void;
readonly _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf1579e791c670fad: (a: number, b: number, c: number) => void;
readonly _dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h517d7fce3d158796: (a: number, b: number, c: number) => void;
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6c41bce435f08bdb: (a: number, b: number, c: number) => void;
readonly _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hea19f293916f5b3a: (a: number, b: number, c: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_free: (a: number, b: number) => void;
readonly __wbindgen_exn_store: (a: number) => void;

View file

@ -241,7 +241,7 @@ function logError(f, args) {
function __wbg_adapter_30(arg0, arg1, arg2) {
_assertNum(arg0);
_assertNum(arg1);
wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha6256632cf9b6e15(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h517d7fce3d158796(arg0, arg1, addHeapObject(arg2));
}
function makeMutClosure(arg0, arg1, dtor, f) {
@ -271,7 +271,7 @@ function makeMutClosure(arg0, arg1, dtor, f) {
function __wbg_adapter_33(arg0, arg1, arg2) {
_assertNum(arg0);
_assertNum(arg1);
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h026107843485189d(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6c41bce435f08bdb(arg0, arg1, addHeapObject(arg2));
}
let stack_pointer = 32;
@ -285,7 +285,7 @@ function __wbg_adapter_36(arg0, arg1, arg2) {
try {
_assertNum(arg0);
_assertNum(arg1);
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf1579e791c670fad(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hea19f293916f5b3a(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
@ -425,6 +425,9 @@ async function init(input) {
const ret = arg0;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbg_error_09919627ac0992f5 = function() { return logError(function (arg0, arg1) {
try {
console.error(getStringFromWasm0(arg0, arg1));
@ -443,9 +446,6 @@ async function init(input) {
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = takeObject(arg0).original;
if (obj.cnt-- == 1) {
@ -869,16 +869,16 @@ async function init(input) {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper19774 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeClosure(arg0, arg1, 1253, __wbg_adapter_30);
imports.wbg.__wbindgen_closure_wrapper19806 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeClosure(arg0, arg1, 1261, __wbg_adapter_30);
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_closure_wrapper21955 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1273, __wbg_adapter_33);
imports.wbg.__wbindgen_closure_wrapper21982 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1281, __wbg_adapter_33);
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_closure_wrapper22397 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1300, __wbg_adapter_36);
imports.wbg.__wbindgen_closure_wrapper22336 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1307, __wbg_adapter_36);
return addHeapObject(ret);
}, arguments) };

View file

@ -5,9 +5,9 @@ export function run_app(a: number): void;
export function __wbindgen_malloc(a: number): number;
export function __wbindgen_realloc(a: number, b: number, c: number): number;
export const __wbindgen_export_2: WebAssembly.Table;
export function _dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha6256632cf9b6e15(a: number, b: number, c: number): void;
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h026107843485189d(a: number, b: number, c: number): void;
export function _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf1579e791c670fad(a: number, b: number, c: number): void;
export function _dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h517d7fce3d158796(a: number, b: number, c: number): void;
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6c41bce435f08bdb(a: number, b: number, c: number): void;
export function _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hea19f293916f5b3a(a: number, b: number, c: number): void;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_free(a: number, b: number): void;
export function __wbindgen_exn_store(a: number): void;

View file

@ -14,8 +14,8 @@ use crate::models;
use crate::utils;
pub use kanidm_proto::oauth2::{
AccessTokenRequest, AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod,
ConsentRequest, ErrorResponse,
AccessTokenRequest, AccessTokenResponse, AuthorisationRequest, AuthorisationResponse,
CodeChallengeMethod, ErrorResponse,
};
enum State {
@ -25,7 +25,13 @@ enum State {
TokenCheck(String),
// Token check done, lets do it.
SubmitAuthReq(String),
Consent(String, ConsentRequest),
Consent {
token: String,
client_name: String,
scopes: Vec<String>,
pii_scopes: Vec<String>,
consent_token: String,
},
ConsentGranted,
ErrInvalidRequest,
}
@ -38,9 +44,17 @@ pub enum Oauth2Msg {
LoginProceed,
ConsentGranted,
TokenValid,
Consent(ConsentRequest),
Consent {
client_name: String,
scopes: Vec<String>,
pii_scopes: Vec<String>,
consent_token: String,
},
Redirect(String),
Error { emsg: String, kopid: Option<String> },
Error {
emsg: String,
kopid: Option<String>,
},
}
impl From<FetchError> for Oauth2Msg {
@ -116,13 +130,41 @@ impl Oauth2App {
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
let status = resp.status();
let headers = resp.headers();
let kopid = headers.get("x-kanidm-opid").ok().flatten();
if status == 200 {
let jsval = JsFuture::from(resp.json()?).await?;
let state: ConsentRequest = jsval.into_serde().expect_throw("Invalid response type");
Ok(Oauth2Msg::Consent(state))
let state: AuthorisationResponse = jsval
.into_serde()
.map_err(|e| {
let e_msg = format!("serde error -> {:?}", e);
console::log!(e_msg.as_str());
})
.expect_throw("Invalid response type");
match state {
AuthorisationResponse::ConsentRequested {
client_name,
scopes,
pii_scopes,
consent_token,
} => Ok(Oauth2Msg::Consent {
client_name,
scopes,
pii_scopes,
consent_token,
}),
AuthorisationResponse::Permitted => {
if let Some(loc) = headers.get("location").ok().flatten() {
Ok(Oauth2Msg::Redirect(loc))
} else {
Ok(Oauth2Msg::Error {
emsg: "no location header".to_string(),
kopid,
})
}
}
}
} else {
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
Ok(Oauth2Msg::Error { emsg, kopid })
@ -131,9 +173,9 @@ impl Oauth2App {
async fn fetch_consent_token(
token: String,
consent_req: ConsentRequest,
consent_token: String,
) -> Result<Oauth2Msg, FetchError> {
let consentreq_jsvalue = serde_json::to_string(&consent_req.consent_token)
let consentreq_jsvalue = serde_json::to_string(&consent_token)
.map(|s| JsValue::from(&s))
.expect_throw("Failed to serialise consent_req");
@ -294,9 +336,20 @@ impl Component for Oauth2App {
};
true
}
Oauth2Msg::Consent(consent_req) => {
Oauth2Msg::Consent {
client_name,
scopes,
pii_scopes,
consent_token,
} => {
self.state = match &self.state {
State::SubmitAuthReq(token) => State::Consent(token.clone(), consent_req),
State::SubmitAuthReq(token) => State::Consent {
token: token.clone(),
client_name,
scopes,
pii_scopes,
consent_token,
},
_ => {
console::log!("Invalid state transition");
State::ErrInvalidRequest
@ -306,9 +359,13 @@ impl Component for Oauth2App {
}
Oauth2Msg::ConsentGranted => {
self.state = match &self.state {
State::Consent(token, consent_req) => {
State::Consent {
token,
consent_token,
..
} => {
let token_c = token.clone();
let cr_c = (*consent_req).clone();
let cr_c = consent_token.clone();
ctx.link().send_future(async {
match Self::fetch_consent_token(token_c, cr_c).await {
Ok(v) => v,
@ -380,8 +437,36 @@ impl Component for Oauth2App {
</main>
}
}
State::Consent(_, query) => {
let client_name = query.client_name.clone();
State::Consent {
token: _,
client_name,
scopes: _,
pii_scopes,
consent_token: _,
} => {
let client_name = client_name.clone();
let pii_req = if pii_scopes.is_empty() {
html! {
<div>
<p>{ "This site will not have access to your personal information" }</p>
<p>{ "If this site requests personal information in the future we will check with you" }</p>
</div>
}
} else {
html! {
<div>
<p>{ "This site has requested to see the following personal information" }</p>
<ul>
{
pii_scopes.iter().map(|s| html! { <li>{ s }</li> } ).collect::<Html>()
}
</ul>
<p>{ "If this site requests different personal information in the future we will check with you again" }</p>
</div>
}
};
// <body class="html-body form-body">
html! {
<main class="form-signin">
@ -393,7 +478,9 @@ impl Component for Oauth2App {
} ) }
action="javascript:void(0);"
>
<h1 class="h3 mb-3 fw-normal">{"Consent to Proceed to " }{ client_name }</h1>
<h2 class="h3 mb-3 fw-normal">{"Consent to Proceed to " }{ client_name }</h2>
{ pii_req }
<button id="autofocus" class="w-100 btn btn-lg btn-primary" type="submit">{ "Proceed" }</button>
</form>
</main>