20230330 oauth2 refresh tokens ()

This commit is contained in:
Firstyear 2023-04-20 08:34:21 +10:00 committed by GitHub
parent 90d4fe1d58
commit 155c93c931
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1693 additions and 861 deletions

View file

@ -56,6 +56,7 @@
- [Access Profiles Original](developers/designs/access_profiles_and_security.md)
- [REST Interface](developers/designs/rest_interface.md)
- [Elevated Priv Mode](developers/designs/elevated_priv_mode.md)
- [Oauth2 Refresh Tokens](developers/designs/oauth2_refresh_tokens.md)
- [Python Module](developers/python.md)
- [RADIUS Integration](developers/radius.md)

View file

@ -0,0 +1,161 @@
# Oauth2 Refresh Tokens
Due to how Kanidm authentication sessions were originally implemented they had short session times
(1 hour) due to the lack of privilege separation in tokens. Now with privilege separation being
implemented session lengths have been extended to 8 hours with possible increases in the future.
However, this leaves us with an issue with oauth2 - oauth2 access tokens are considered valid until
their expiry and we should not issue tokens with a validity of 8 hours or longer since that would
allow rogue users to have a long window of usage of the token before they were forced to re-auth. It
also means that in the case that an account must be forcefully terminated then the user would retain
access to applications for up to 8 hours or more.
To prevent this, we need oauth2 tokens to "check in" periodically to re-afirm their session
validity.
This is performed with access tokens and refresh tokens. The access token has a short lifespan
(proposed 15 minutes) and must be refreshed with Kanidm which can check the true session validity
and if the session has been revoked. This creates a short window for revocation to propagate to
oauth2 applications since each oauth2 application must periodically check in to keep their access
token alive.
## Risks
Refresh tokens are presented to the relying server where they receive an access token and an
optional new refresh token. Because of this, it could be possible to present a refresh token
multiple times to proliferate extra refresh and access tokens away from the system. Preventing this
is important to limit where the tokens are used and monitor and revoke them effectively.
In addition, old refresh tokens should not be able to be used once exchanged, they should be "at
most once". If this is not enforced then old refresh tokens can be used to gain access to sessions
even if the associated access token was expired by many hours and it's refresh token was already
used.
This is supported by
[draft oauth security topics section 2.2.2](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.2.2)
and
[draft oauth security topics refresh token protection](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#refresh_token_protection)
Refresh tokens must only be used by the client application associated. Kanidm strictly enforces this
already with our client authorisation checks. This is discussed in
[rfc6749 section 10.4](https://www.rfc-editor.org/rfc/rfc6749#section-10.4).
## Design
┌─────────────────────────────────────────┐
│Kanidm │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ Session │ 3. Update │ Session │ │
│ │ NIB 1 │─────NIB───────▶│ NIB 2 │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ └─────────┘ └─────────┘ │
│ │ │ │
└───┼───────────────────────────┼─────────┘
┌────┘ ▲ ┌────┘
│ │ │
│ │ │
1. Issued │ 4. Issued
│ │ │
│ │ │
│ │ │
▼ │ ▼
┌───────┐ │ ┌───────┐
│ │ │ │ │
│Access │ │ │Access │
│ + │ │ │ + │
│Refresh│──2. Refresh──┘ │Refresh│
│ IAT 1 │ │ IAT 2 │
│ │ │ │
└───────┘ └───────┘
In this design we associate a "not issued before" (NIB) timestamp to our sessions. For a refresh
token to be valid for issuance, the refresh tokens IAT must be greater than or equal to the NIB.
In this example were the refresh token with IAT 1 re-used after the second token was issued, then
this condition would fail as the NIB has advanced to 2. Since IAT 1 is not greater or equal to NIB 2
then the refresh token _must_ have previously been used for access token exchange.
In a replicated environment this system is also stable and correct even if a session update is
missed.
2.
┌───────────────────────Replicate────────────────┐
│ │
│ │
┌───────┼─────────────────────────────────┐ ┌──────┼──────────────────────────────────┐
│Kanidm │ │ │Kanidm│ │
│ │ │ │ ▼ │
│ ┌─────────┐ ┌─────────┐ │ │ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ Session │ 4. Update │ Session │ │ │ │ Session │ 7. Update │ Session │ │
│ │ NIB 1 │─────NIB───────▶│ NIB 2 │ │ │ │ NIB 1 │ ─────NIB───────▶│ NIB 3 │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ │ │ └─────────┘ └─────────┘ │
│ │ │ │ │ ▲ │ │
└───┼───────────────────────────┼─────────┘ └──────┼────────────────────────┼─────────┘
┌────┘ ▲ ┌────┘ │ ┌────┘
│ │ │ │ │
│ │ │ │ │
1. Issued │ 5. Issued │ 8. Issued
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
▼ │ ▼ │ ▼
┌───────┐ │ ┌───────┐ │ ┌───────┐
│ │ │ │ │ │ │ │
│Access │ │ │Access │ │ │Access │
│ + │ │ │ + │ │ │ + │
│Refresh│──3. Refresh──┘ │Refresh│ │ │Refresh│
│ IAT 1 │ │ IAT 2 │─────6. Refresh──────────┘ │ IAT 3 │
│ │ │ │ │ │
└───────┘ └───────┘ └───────┘
In this example, we can see that the replication of the session with NIB 1 happens to the second
Kanidm server, but the replication of session with NIB 2 has not occurred yet. If the token that was
later issued with IAT 2 was presented to the second server it would still be valid and able to
refresh since IAT 2 is greater or equal to NIB 1. This would also prompt the session to advance to
NIB 3 such that when replication begun again, the session with NIB 3 would take precedence over the
former NIB 2 session.
While this allows a short window where a former access token could be used on the second replica,
this infrastructure being behind load balancers and outside of an attackers influence significantly
hinders the ability to attack this for very little gain.
## Attack Detection
[draft oauth security topics section 4.14.2](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.14.2)
specifically calls out that when refresh token re-use is detected then all tokens of the session
should be canceled to cause a new authorisation code flow to be initiated.
## Inactive Refresh Tokens
Similar
[draft oauth security topics section 4.14.2](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.14.2)
also discusses that inactive tokens should be invalidated after a period of time. From the view of
the refresh token this is performed by an internal exp field in the encrypted refresh token.
From the servers side we will require a "not after" parameter that is updated on token activity.
This will also require inactive session cleanup in the server which can be extended into the session
consistency plugin that already exists.
Since the act of refreshing a token is implied activity then we do not require other signaling
mechanisms.
# Questions
Currently with authorisation code grants and sessions we issue these where the sessions are recorded
in an async manner. For consistency I believe the same should be true here but is there a concern
with the refresh being issued but a slight delay before it's recorded? I think given the nature of
our future replication we already have to consider the async/eventual nature of things, so this
doesn't impact that further, and may just cause client latency in the update process.
However, we also don't want a situation where our async/delayed action queues become too full or
overworked. Maybe queue monitoring/backlog issues are a separate problem though.

View file

@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use base64urlsafedata::Base64UrlSafeData;
use serde::{Deserialize, Serialize};
@ -66,9 +66,9 @@ pub enum AuthorisationResponse {
// A pretty-name of the client
client_name: String,
// A list of scopes requested / to be issued.
scopes: Vec<String>,
scopes: BTreeSet<String>,
// Extra PII that may be requested
pii_scopes: Vec<String>,
pii_scopes: BTreeSet<String>,
// The users displayname (?)
// pub display_name: String,
// The token we need to be given back to allow this to proceed
@ -77,21 +77,44 @@ pub enum AuthorisationResponse {
Permitted,
}
// The resource server then contacts the token endpoint with
//
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "grant_type", rename_all = "snake_case")]
pub enum GrantTypeReq {
AuthorizationCode {
// As sent by the authorisationCode
code: String,
// Must be the same as the original redirect uri.
redirect_uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
code_verifier: Option<String>,
},
RefreshToken {
refresh_token: String,
#[serde(skip_serializing_if = "Option::is_none")]
scope: Option<BTreeSet<String>>,
},
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessTokenRequest {
// must be authorization_code
pub grant_type: String,
// As sent by the authorisationCode
pub code: String,
// Must be the same as the original redirect uri.
pub redirect_uri: Url,
#[serde(flatten)]
pub grant_type: GrantTypeReq,
// REQUIRED, if the client is not authenticating with the
// authorization server as described in Section 3.2.1.
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_secret: Option<String>,
pub code_verifier: Option<String>,
}
impl From<GrantTypeReq> for AccessTokenRequest {
fn from(req: GrantTypeReq) -> AccessTokenRequest {
AccessTokenRequest {
grant_type: req,
client_id: None,
client_secret: None,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
@ -360,3 +383,21 @@ pub struct ErrorResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub error_uri: Option<Url>,
}
#[cfg(test)]
mod tests {
use super::{AccessTokenRequest, GrantTypeReq};
use url::Url;
#[test]
fn test_oauth2_access_token_req() {
let atr: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: "demo code".to_string(),
redirect_uri: Url::parse("http://[::1]").unwrap(),
code_verifier: None,
}
.into();
println!("{:?}", serde_json::to_string(&atr).expect("JSON failure"));
}
}

View file

@ -27,9 +27,8 @@ use kanidmd_lib::{
},
idm::ldap::{LdapBoundToken, LdapResponseState, LdapServer},
idm::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess, AuthoriseResponse,
JwkKeySet, Oauth2Error, OidcDiscoveryResponse, OidcToken,
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AuthorisationRequest,
AuthoriseResponse, JwkKeySet, Oauth2Error, OidcDiscoveryResponse, OidcToken,
},
idm::server::{IdmServer, IdmServerTransaction},
idm::serviceaccount::ListApiTokenEvent,
@ -1249,34 +1248,6 @@ impl QueryServerReadV1 {
idms_prox_read.check_oauth2_authorisation(&ident, &uat, &auth_req, ct)
}
#[instrument(
level = "info",
skip_all,
fields(uuid = ?eventid)
)]
pub async fn handle_oauth2_authorise_permit(
&self,
uat: Option<String>,
consent_req: String,
eventid: Uuid,
) -> Result<AuthorisePermitSuccess, OperationError> {
let ct = duration_from_epoch_now();
let mut idms_prox_read = self.idms.proxy_read().await;
let (ident, uat) = idms_prox_read
.validate_and_parse_uat(uat.as_deref(), ct)
.and_then(|uat| {
idms_prox_read
.process_uat_to_identity(&uat, ct)
.map(|ident| (ident, uat))
})
.map_err(|e| {
admin_error!("Invalid identity: {:?}", e);
e
})?;
idms_prox_read.check_oauth2_authorise_permit(&ident, &uat, &consent_req, ct)
}
#[instrument(
level = "info",
skip_all,
@ -1305,23 +1276,6 @@ impl QueryServerReadV1 {
idms_prox_read.check_oauth2_authorise_reject(&ident, &uat, &consent_req, ct)
}
#[instrument(
level = "info",
skip_all,
fields(uuid = ?eventid)
)]
pub async fn handle_oauth2_token_exchange(
&self,
client_authz: Option<String>,
token_req: AccessTokenRequest,
eventid: Uuid,
) -> Result<AccessTokenResponse, Oauth2Error> {
let ct = duration_from_epoch_now();
let mut idms_prox_read = self.idms.proxy_read().await;
// Now we can send to the idm server for authorisation checking.
idms_prox_read.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct)
}
#[instrument(
level = "info",
skip_all,

View file

@ -24,7 +24,10 @@ use kanidmd_lib::{
},
idm::delayed::DelayedAction,
idm::event::{GeneratePasswordEvent, RegenerateRadiusSecretEvent, UnixPasswordChangeEvent},
idm::oauth2::{Oauth2Error, TokenRevokeRequest},
idm::oauth2::{
AccessTokenRequest, AccessTokenResponse, AuthorisePermitSuccess, Oauth2Error,
TokenRevokeRequest,
},
idm::server::{IdmServer, IdmServerTransaction},
idm::serviceaccount::{DestroyApiTokenEvent, GenerateApiTokenEvent},
modify::{Modify, ModifyInvalid, ModifyList},
@ -1398,6 +1401,63 @@ impl QueryServerWriteV1 {
.and_then(|_| idms_prox_write.commit().map(|_| ()))
}
#[instrument(
level = "info",
skip_all,
fields(uuid = ?eventid)
)]
pub async fn handle_oauth2_authorise_permit(
&self,
uat: Option<String>,
consent_req: String,
eventid: Uuid,
) -> Result<AuthorisePermitSuccess, OperationError> {
let ct = duration_from_epoch_now();
let mut idms_prox_write = self.idms.proxy_write(ct).await;
let (ident, uat) = idms_prox_write
.validate_and_parse_uat(uat.as_deref(), ct)
.and_then(|uat| {
idms_prox_write
.process_uat_to_identity(&uat, ct)
.map(|ident| (ident, uat))
})
.map_err(|e| {
admin_error!("Invalid identity: {:?}", e);
e
})?;
idms_prox_write
.check_oauth2_authorise_permit(&ident, &uat, &consent_req, ct)
.and_then(|r| idms_prox_write.commit().map(|()| r))
}
#[instrument(
level = "info",
skip_all,
fields(uuid = ?eventid)
)]
pub async fn handle_oauth2_token_exchange(
&self,
client_authz: Option<String>,
token_req: AccessTokenRequest,
eventid: Uuid,
) -> Result<AccessTokenResponse, Oauth2Error> {
let ct = duration_from_epoch_now();
let mut idms_prox_write = self.idms.proxy_write(ct).await;
// Now we can send to the idm server for authorisation checking.
let resp =
idms_prox_write.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct);
match &resp {
Err(Oauth2Error::InvalidGrant) | Ok(_) => {
idms_prox_write.commit().map_err(Oauth2Error::ServerError)?;
}
_ => {}
};
resp
}
#[instrument(
level = "info",
skip_all,

View file

@ -403,7 +403,7 @@ async fn oauth2_authorise_permit(
let res = req
.state()
.qe_r_ref
.qe_w_ref
.handle_oauth2_authorise_permit(uat, consent_req, eventid)
.await;
@ -520,15 +520,6 @@ pub async fn oauth2_token_post(mut req: tide::Request<AppState>) -> tide::Result
.and_then(|hv| hv.get(0))
.and_then(|h| h.as_str().strip_prefix("Basic "))
.map(str::to_string);
/*
.ok_or_else(|| {
error!("Basic Authentication Not Provided");
tide::Error::from_str(
tide::StatusCode::Unauthorized,
"Invalid Basic Authorisation",
)
})?;
*/
// Get the accessToken Request
let tok_req: AccessTokenRequest = req.body_form().await.map_err(|e| {
@ -539,9 +530,13 @@ pub async fn oauth2_token_post(mut req: tide::Request<AppState>) -> tide::Result
)
})?;
// Do we change the method/path we take here based on the type of requested
// grant? Should we cease the delayed/async session update here and just opt
// for a wr txn?
let res = req
.state()
.qe_r_ref
.qe_w_ref
.handle_oauth2_token_exchange(client_authz, tok_req, eventid)
.await;

View file

@ -76,10 +76,12 @@ pub const AUTH_SESSION_TIMEOUT: u64 = 300;
pub const MFAREG_SESSION_TIMEOUT: u64 = 300;
pub const PW_MIN_LENGTH: usize = 10;
// Default
// Default - sessions last for 1 hour.
pub const AUTH_SESSION_EXPIRY: u64 = 3600;
// Ten minutes by default;
// Default - privileges last for 10 minutes.
pub const AUTH_PRIVILEGE_EXPIRY: u64 = 600;
// Default - oauth refresh tokens last for 16 hours.
pub const OAUTH_REFRESH_TOKEN_EXPIRY: u64 = 3600 * 8;
// The time that a token can be used before session
// status is enforced. This needs to be longer than
@ -88,4 +90,4 @@ pub const GRACE_WINDOW: Duration = Duration::from_secs(300);
/// How long access tokens should last. This is NOT the length
/// of the refresh token, which is bound to the issuing session.
pub const OAUTH2_ACCESS_TOKEN_EXPIRY: u32 = 4 * 3600;
pub const OAUTH2_ACCESS_TOKEN_EXPIRY: u32 = 15 * 60;

View file

@ -1133,7 +1133,8 @@ impl AuthSession {
issued_by: IdentityId::User(self.account.uuid),
scope,
}))
.map_err(|_| {
.map_err(|e| {
debug!(?e, "queue failure");
admin_error!("unable to queue failing authentication as the session will not validate ... ");
OperationError::InvalidState
})?;

View file

@ -11,9 +11,7 @@ pub enum DelayedAction {
UnixPwUpgrade(UnixPasswordUpgrade),
WebauthnCounterIncrement(WebauthnCounterIncrement),
BackupCodeRemoval(BackupCodeRemoval),
Oauth2ConsentGrant(Oauth2ConsentGrant),
AuthSessionRecord(AuthSessionRecord),
Oauth2SessionRecord(Oauth2SessionRecord),
}
pub struct PasswordUpgrade {
@ -54,13 +52,6 @@ pub struct BackupCodeRemoval {
pub code_to_remove: String,
}
#[derive(Debug)]
pub struct Oauth2ConsentGrant {
pub target_uuid: Uuid,
pub oauth2_rs_uuid: Uuid,
pub scopes: Vec<String>,
}
#[derive(Debug)]
pub struct AuthSessionRecord {
pub target_uuid: Uuid,
@ -72,14 +63,3 @@ pub struct AuthSessionRecord {
pub issued_by: IdentityId,
pub scope: SessionScope,
}
#[derive(Debug)]
pub struct Oauth2SessionRecord {
pub target_uuid: Uuid,
pub parent_session_id: Uuid,
pub session_id: Uuid,
pub expiry: Option<OffsetDateTime>,
pub issued_at: OffsetDateTime,
// Which rs is this related to?
pub rs_uuid: Uuid,
}

File diff suppressed because it is too large Load diff

View file

@ -32,8 +32,8 @@ use crate::idm::account::Account;
use crate::idm::authsession::AuthSession;
use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
use crate::idm::delayed::{
AuthSessionRecord, BackupCodeRemoval, DelayedAction, Oauth2ConsentGrant, Oauth2SessionRecord,
PasswordUpgrade, UnixPasswordUpgrade, WebauthnCounterIncrement,
AuthSessionRecord, BackupCodeRemoval, DelayedAction, PasswordUpgrade, UnixPasswordUpgrade,
WebauthnCounterIncrement,
};
#[cfg(test)]
use crate::idm::event::PasswordChangeEvent;
@ -54,7 +54,7 @@ use crate::idm::unix::{UnixGroup, UnixUserAccount};
use crate::idm::AuthState;
use crate::prelude::*;
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
use crate::value::{Oauth2Session, Session};
use crate::value::Session;
pub(crate) type AuthSessionMutex = Arc<Mutex<AuthSession>>;
pub(crate) type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;
@ -120,7 +120,7 @@ pub struct IdmServerProxyReadTransaction<'a> {
pub qs_read: QueryServerReadTransaction<'a>,
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
pub(crate) oauth2rs: Oauth2ResourceServersReadTransaction,
pub(crate) async_tx: Sender<DelayedAction>,
// pub(crate) async_tx: Sender<DelayedAction>,
}
pub struct IdmServerProxyWriteTransaction<'a> {
@ -290,7 +290,7 @@ impl IdmServer {
qs_read: self.qs.read().await,
domain_keys: self.domain_keys.read(),
oauth2rs: self.oauth2rs.read(),
async_tx: self.async_tx.clone(),
// async_tx: self.async_tx.clone(),
}
}
@ -2043,58 +2043,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// Done!
}
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::Refer(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::Uuid(o2cg.target_uuid))),
&modlist,
)
}
pub(crate) fn process_oauth2sessionrecord(
&mut self,
osr: &Oauth2SessionRecord,
) -> Result<(), OperationError> {
let session = Value::Oauth2Session(
osr.session_id,
Oauth2Session {
parent: osr.parent_session_id,
expiry: osr.expiry,
issued_at: osr.issued_at,
rs_uuid: osr.rs_uuid,
},
);
info!(session_id = %osr.session_id, "Persisting auth session");
// modify the account to put the session onto it.
let modlist = ModifyList::new_append("oauth2_session", session);
self.qs_write
.internal_modify(
&filter!(f_eq("uuid", PartialValue::Uuid(osr.target_uuid))),
&modlist,
)
.map_err(|e| {
admin_error!("Failed to persist user auth token {:?}", e);
e
})
// Done!
}
pub fn process_delayedaction(
&mut self,
da: DelayedAction,
@ -2105,9 +2053,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),
DelayedAction::AuthSessionRecord(asr) => self.process_authsessionrecord(&asr),
DelayedAction::Oauth2SessionRecord(osr) => self.process_oauth2sessionrecord(&osr),
}
}

View file

@ -118,7 +118,7 @@ impl SessionConsistency {
// The parent exists, go ahead
None
} else {
info!(%o2_session_id, "Removing unbound oauth2 session");
info!(%o2_session_id, parent_id = %session.parent, "Removing unbound oauth2 session");
Some(PartialValue::Refer(*o2_session_id))
}
} else {

View file

@ -994,12 +994,18 @@ impl QueryServer {
pub async fn read(&self) -> QueryServerReadTransaction<'_> {
// We need to ensure a db conn will be available
#[allow(clippy::expect_used)]
let db_ticket = self
.db_tickets
.acquire()
.await
.expect("unable to acquire db_ticket for qsr");
let db_ticket = if cfg!(test) {
#[allow(clippy::expect_used)]
self.db_tickets
.try_acquire()
.expect("unable to acquire db_ticket for qsr")
} else {
#[allow(clippy::expect_used)]
self.db_tickets
.acquire()
.await
.expect("unable to acquire db_ticket for qsr")
};
QueryServerReadTransaction {
be_txn: self.be.read(),
@ -1014,18 +1020,30 @@ impl QueryServer {
pub async fn write(&self, curtime: Duration) -> QueryServerWriteTransaction<'_> {
// Guarantee we are the only writer on the thread pool
#[allow(clippy::expect_used)]
let write_ticket = self
.write_ticket
.acquire()
.await
.expect("unable to acquire writer_ticket for qsw");
let write_ticket = if cfg!(debug_assertions) {
self.write_ticket
.try_acquire()
.expect("unable to acquire writer_ticket for qsw")
} else {
self.write_ticket
.acquire()
.await
.expect("unable to acquire writer_ticket for qsw")
};
// We need to ensure a db conn will be available
#[allow(clippy::expect_used)]
let db_ticket = self
.db_tickets
.acquire()
.await
.expect("unable to acquire db_ticket for qsw");
let db_ticket = if cfg!(test) {
#[allow(clippy::expect_used)]
self.db_tickets
.try_acquire()
.expect("unable to acquire db_ticket for qsw")
} else {
#[allow(clippy::expect_used)]
self.db_tickets
.acquire()
.await
.expect("unable to acquire db_ticket for qsw")
};
let schema_write = self.schema.write();
let mut be_txn = self.be.write();

View file

@ -6,7 +6,7 @@ use std::str::FromStr;
use compact_jwt::{JwkKeySet, JwsValidator, OidcToken, OidcUnverified};
use kanidm_proto::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
AccessTokenResponse, AuthorisationResponse, OidcDiscoveryResponse,
AccessTokenResponse, AuthorisationResponse, GrantTypeReq, OidcDiscoveryResponse,
};
use oauth2_ext::PkceCodeChallenge;
use url::Url;
@ -275,14 +275,12 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
// Step 3 - the "resource server" then uses this state and code to directly contact
// the authorisation server to request a token.
let form_req = AccessTokenRequest {
grant_type: "authorization_code".to_string(),
let form_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
code: code.to_string(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/flow").expect("Invalid URL"),
client_id: None,
client_secret: None,
code_verifier: Some(pkce_code_verifier.secret().clone()),
};
}
.into();
let response = client
.post(format!("{}/oauth2/token", url))

View file

@ -233,19 +233,19 @@ function addBorrowedObject(obj) {
}
function __wbg_adapter_48(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hac369e4ee86f495a(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h42f81153b61b4799(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
}
function __wbg_adapter_51(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__he1d6c0533ad33db4(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h37d4c14cb2b8cc24(arg0, arg1, addHeapObject(arg2));
}
function __wbg_adapter_54(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0f629f079cf8b186(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h708b258e80cbbf71(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
@ -407,14 +407,6 @@ function getImports() {
imports.wbg.__wbg_modalhidebyid_6dd8ae230b194210 = function(arg0, arg1) {
modal_hide_by_id(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_listener_id;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
};
imports.wbg.__wbg_setlistenerid_3183aae8fa5840fb = function(arg0, arg1) {
getObject(arg0).__yew_listener_id = arg1 >>> 0;
};
imports.wbg.__wbg_cachekey_b61393159c57fd7b = function(arg0, arg1) {
const ret = getObject(arg1).__yew_subtree_cache_key;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
@ -431,6 +423,14 @@ function getImports() {
imports.wbg.__wbg_setcachekey_80183b7cfc421143 = function(arg0, arg1) {
getObject(arg0).__yew_subtree_cache_key = arg1 >>> 0;
};
imports.wbg.__wbg_setlistenerid_3183aae8fa5840fb = function(arg0, arg1) {
getObject(arg0).__yew_listener_id = arg1 >>> 0;
};
imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_listener_id;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
};
imports.wbg.__wbg_new_abda76e883ba8a5f = function() {
const ret = new Error();
return addHeapObject(ret);
@ -1156,16 +1156,16 @@ function getImports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4757 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1106, __wbg_adapter_48);
imports.wbg.__wbindgen_closure_wrapper4807 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1108, __wbg_adapter_48);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper5652 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1432, __wbg_adapter_51);
imports.wbg.__wbindgen_closure_wrapper5708 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1434, __wbg_adapter_51);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper5715 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1458, __wbg_adapter_54);
imports.wbg.__wbindgen_closure_wrapper5771 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1460, __wbg_adapter_54);
return addHeapObject(ret);
};

View file

@ -14,6 +14,8 @@ use crate::error::*;
use crate::manager::Route;
use crate::{models, utils};
use std::collections::BTreeSet;
enum State {
LoginRequired,
// We are in the process of check the auth token to be sure we can proceed.
@ -23,8 +25,8 @@ enum State {
Consent {
client_name: String,
#[allow(dead_code)]
scopes: Vec<String>,
pii_scopes: Vec<String>,
scopes: BTreeSet<String>,
pii_scopes: BTreeSet<String>,
consent_token: String,
},
ConsentGranted(String),
@ -43,8 +45,8 @@ pub enum Oauth2Msg {
TokenValid,
Consent {
client_name: String,
scopes: Vec<String>,
pii_scopes: Vec<String>,
scopes: BTreeSet<String>,
pii_scopes: BTreeSet<String>,
consent_token: String,
},
Redirect(String),