mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
636 consent remembering in oauth2 (#824)
This commit is contained in:
parent
4a1df985b9
commit
9d929b876c
|
@ -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
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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>", {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 |
6
kanidmd_web_ui/pkg/kanidmd_web_ui.d.ts
vendored
6
kanidmd_web_ui/pkg/kanidmd_web_ui.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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) };
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue