mirror of
https://github.com/kanidm/kanidm.git
synced 2025-06-03 14:53:55 +02:00
20230330 oauth2 refresh tokens (#1502)
This commit is contained in:
parent
90d4fe1d58
commit
155c93c931
book/src
proto/src
server
core/src
lib/src
testkit/tests
web_ui
|
@ -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)
|
||||
|
||||
|
|
161
book/src/developers/designs/oauth2_refresh_tokens.md
Normal file
161
book/src/developers/designs/oauth2_refresh_tokens.md
Normal 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.
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
})?;
|
||||
|
|
|
@ -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
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue