mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
20211010 rfc7662 token introspect (#607)
This commit is contained in:
parent
4ef064e4ed
commit
761bed0569
|
@ -67,7 +67,7 @@ Kanidm will expose it's oauth2 apis at the urls:
|
|||
For an authorisation to proceed, the resource server will request a list of scopes, which are
|
||||
unique to that resource server. For example, when a user wishes to login to the admin panel
|
||||
of the resource server, it may request the "admin" scope from kanidm for authorisation. But when
|
||||
a user wants to login, it may only request "acces" as a scope from kanidm.
|
||||
a user wants to login, it may only request "access" as a scope from kanidm.
|
||||
|
||||
As each resource server may have it's own scopes and understanding of these, Kanidm isolates
|
||||
scopes to each resource server connected to Kanidm. Kanidm has two methods of granting scopes to accounts (users).
|
||||
|
|
|
@ -2,7 +2,10 @@ mod common;
|
|||
use crate::common::{run_test, ADMIN_TEST_PASSWORD};
|
||||
use kanidm_client::KanidmClient;
|
||||
|
||||
use kanidm_proto::oauth2::{AccessTokenRequest, AccessTokenResponse, ConsentRequest};
|
||||
use kanidm_proto::oauth2::{
|
||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||
AccessTokenResponse, ConsentRequest,
|
||||
};
|
||||
use oauth2_ext::PkceCodeChallenge;
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
@ -169,7 +172,7 @@ fn test_oauth2_basic_flow() {
|
|||
|
||||
let response = client
|
||||
.post(format!("{}/oauth2/token", url))
|
||||
.basic_auth("test_integration", Some(client_secret))
|
||||
.basic_auth("test_integration", Some(client_secret.clone()))
|
||||
.form(&form_req)
|
||||
.send()
|
||||
.await
|
||||
|
@ -180,12 +183,45 @@ fn test_oauth2_basic_flow() {
|
|||
|
||||
// The body is a json AccessTokenResponse
|
||||
|
||||
let _atr = response
|
||||
let atr = response
|
||||
.json::<AccessTokenResponse>()
|
||||
.await
|
||||
.expect("Unable to decode AccessTokenResponse");
|
||||
|
||||
// Step 4 - inspect the granted token.
|
||||
let intr_request = AccessTokenIntrospectRequest {
|
||||
token: atr.access_token.clone(),
|
||||
token_type_hint: None,
|
||||
};
|
||||
|
||||
let response = client
|
||||
.post(format!("{}/oauth2/token/introspect", url))
|
||||
.basic_auth("test_integration", Some(client_secret))
|
||||
.form(&intr_request)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send token introspect request.");
|
||||
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
|
||||
let tir = response
|
||||
.json::<AccessTokenIntrospectResponse>()
|
||||
.await
|
||||
.expect("Unable to decode AccessTokenIntrospectResponse");
|
||||
|
||||
assert!(tir.active);
|
||||
assert!(tir.scope.is_some());
|
||||
assert!(tir.client_id.as_deref() == Some("test_integration"));
|
||||
assert!(tir.username.as_deref() == Some("admin@example.com"));
|
||||
assert!(tir.token_type.as_deref() == Some("access_token"));
|
||||
assert!(tir.exp.is_some());
|
||||
assert!(tir.iat.is_some());
|
||||
assert!(tir.nbf.is_some());
|
||||
assert!(tir.sub.is_some());
|
||||
assert!(tir.aud.is_none());
|
||||
assert!(tir.iss.is_none());
|
||||
assert!(tir.jti.is_none());
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -78,6 +78,60 @@ pub struct AccessTokenResponse {
|
|||
pub scope: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AccessTokenIntrospectRequest {
|
||||
pub token: String,
|
||||
/// https://datatracker.ietf.org/doc/html/rfc7009#section-4.1.2
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub token_type_hint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AccessTokenIntrospectResponse {
|
||||
pub active: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub scope: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub client_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub username: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub token_type: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exp: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iat: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nbf: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sub: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aud: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iss: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jti: Option<String>,
|
||||
}
|
||||
|
||||
impl AccessTokenIntrospectResponse {
|
||||
pub fn inactive() -> Self {
|
||||
AccessTokenIntrospectResponse {
|
||||
active: false,
|
||||
scope: None,
|
||||
client_id: None,
|
||||
username: None,
|
||||
token_type: None,
|
||||
exp: None,
|
||||
iat: None,
|
||||
nbf: None,
|
||||
sub: None,
|
||||
aud: None,
|
||||
iss: None,
|
||||
jti: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ErrorResponse {
|
||||
pub error: String,
|
||||
|
|
|
@ -18,8 +18,8 @@ use kanidm_proto::v1::{BackupCodesView, OperationError, RadiusAuthToken};
|
|||
|
||||
use crate::filter::{Filter, FilterInvalid};
|
||||
use crate::idm::oauth2::{
|
||||
AccessTokenRequest, AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess,
|
||||
ConsentRequest, Oauth2Error,
|
||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||
AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess, ConsentRequest, Oauth2Error,
|
||||
};
|
||||
use crate::idm::server::{IdmServer, IdmServerTransaction};
|
||||
use crate::ldap::{LdapBoundToken, LdapResponseState, LdapServer};
|
||||
|
@ -1053,6 +1053,27 @@ impl QueryServerReadV1 {
|
|||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "oauth2_token_introspect",
|
||||
skip(self, client_authz, intr_req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_oauth2_token_introspect(
|
||||
&self,
|
||||
client_authz: String,
|
||||
intr_req: AccessTokenIntrospectRequest,
|
||||
eventid: Uuid,
|
||||
) -> Result<AccessTokenIntrospectResponse, Oauth2Error> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_prox_read = self.idms.proxy_read_async().await;
|
||||
let res = spanned!("actors::v1_read::handle<Oauth2TokenIntrospect>", {
|
||||
// Now we can send to the idm server for introspection checking.
|
||||
idms_prox_read.check_oauth2_token_introspect(&client_authz, &intr_req, ct)
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "auth_valid",
|
||||
|
|
|
@ -399,11 +399,9 @@ pub fn create_https_server(
|
|||
.post(oauth2_authorise_permit_post)
|
||||
.get(oauth2_authorise_permit_get);
|
||||
oauth2_process.at("/token").post(oauth2_token_post);
|
||||
/*
|
||||
oauth2_process
|
||||
.at("/token/introspect")
|
||||
.get(oauth2_token_introspect_get);
|
||||
*/
|
||||
.post(oauth2_token_introspect_post);
|
||||
|
||||
let mut raw_route = appserver.at("/v1/raw");
|
||||
raw_route.at("/create").post(create);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use super::v1::{json_rest_event_get, json_rest_event_post};
|
||||
use super::{to_tide_response, AppState, RequestExtensions};
|
||||
use crate::idm::oauth2::{
|
||||
AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess, ErrorResponse, Oauth2Error,
|
||||
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess,
|
||||
ErrorResponse, Oauth2Error,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
|
@ -401,7 +402,70 @@ pub async fn get_openid_configuration(_req: tide::Request<AppState>) -> tide::Re
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
/*
|
||||
pub async fn oauth2_token_introspect_get(req: tide::Request<AppState>) -> tide::Result {
|
||||
pub async fn oauth2_token_introspect_post(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
// This is called directly by the resource server, where we then issue
|
||||
// information about this token to the caller.
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let client_authz = req
|
||||
.header("authorization")
|
||||
.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 introspection request, could we accept json or form? Prob needs content type here.
|
||||
let intr_req: AccessTokenIntrospectRequest = req.body_form().await.map_err(|e| {
|
||||
request_error!("{:?}", e);
|
||||
tide::Error::from_str(
|
||||
tide::StatusCode::BadRequest,
|
||||
"Invalid Oauth2 AccessTokenIntrospectRequest",
|
||||
)
|
||||
})?;
|
||||
|
||||
request_trace!("Introspect Request - {:?}", intr_req);
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_r_ref
|
||||
.handle_oauth2_token_introspect(client_authz, intr_req, eventid)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(atr) => {
|
||||
let mut res = tide::Response::new(200);
|
||||
tide::Body::from_json(&atr).map(|b| {
|
||||
res.set_body(b);
|
||||
res
|
||||
})
|
||||
}
|
||||
Err(Oauth2Error::AuthenticationRequired) => {
|
||||
// This will trigger our ui to auth and retry.
|
||||
Ok(tide::Response::new(tide::StatusCode::Unauthorized))
|
||||
}
|
||||
Err(e) => {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
let err = ErrorResponse {
|
||||
error: e.to_string(),
|
||||
error_description: None,
|
||||
error_uri: None,
|
||||
};
|
||||
|
||||
let mut res = tide::Response::new(400);
|
||||
tide::Body::from_json(&err).map(|b| {
|
||||
res.set_body(b);
|
||||
res
|
||||
})
|
||||
}
|
||||
}
|
||||
.map(|mut res| {
|
||||
res.insert_header("X-KANIDM-OPID", hvalue);
|
||||
res
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//!
|
||||
|
||||
use crate::identity::IdentityId;
|
||||
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerTransaction};
|
||||
use crate::prelude::*;
|
||||
use concread::cowcell::*;
|
||||
use fernet::Fernet;
|
||||
|
@ -13,6 +14,7 @@ use hashbrown::HashMap;
|
|||
use kanidm_proto::v1::UserAuthToken;
|
||||
use openssl::sha;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::trace;
|
||||
|
@ -20,8 +22,8 @@ use url::{Origin, Url};
|
|||
use webauthn_rs::base64_data::Base64UrlSafeData;
|
||||
|
||||
pub use kanidm_proto::oauth2::{
|
||||
AccessTokenRequest, AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod,
|
||||
ConsentRequest, ErrorResponse,
|
||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||
AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ConsentRequest, ErrorResponse,
|
||||
};
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
@ -82,8 +84,6 @@ struct ConsentToken {
|
|||
pub scopes: Vec<String>,
|
||||
}
|
||||
|
||||
// consent token?
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct TokenExchangeCode {
|
||||
// We don't need the client_id here, because it's signed with an RS specific
|
||||
|
@ -97,6 +97,32 @@ struct TokenExchangeCode {
|
|||
pub scopes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum Oauth2TokenType {
|
||||
Access,
|
||||
Refresh,
|
||||
}
|
||||
|
||||
impl fmt::Display for Oauth2TokenType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Oauth2TokenType::Access => write!(f, "access_token"),
|
||||
Oauth2TokenType::Refresh => write!(f, "refresh_token"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct Oauth2UserToken {
|
||||
pub toktype: Oauth2TokenType,
|
||||
pub uat: UserAuthToken,
|
||||
pub scopes: Vec<String>,
|
||||
// Oauth2 exp is seperate to uat expiry
|
||||
pub exp: i64,
|
||||
pub iat: i64,
|
||||
pub nbf: i64,
|
||||
}
|
||||
|
||||
// consentPermitResponse
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -476,34 +502,10 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
return Err(Oauth2Error::InvalidRequest);
|
||||
}
|
||||
|
||||
// Check the client_authz
|
||||
let authz = base64::decode(&client_authz)
|
||||
.map_err(|_| {
|
||||
admin_error!("Basic authz invalid base64");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})
|
||||
.and_then(|data| {
|
||||
String::from_utf8(data).map_err(|_| {
|
||||
admin_error!("Basic authz invalid utf8");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})
|
||||
})?;
|
||||
|
||||
// Get the first :, it should be our delim.
|
||||
//
|
||||
let mut split_iter = authz.split(':');
|
||||
|
||||
let client_id = split_iter.next().ok_or_else(|| {
|
||||
admin_error!("Basic authz invalid format (corrupt input?)");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})?;
|
||||
let secret = split_iter.next().ok_or_else(|| {
|
||||
admin_error!("Basic authz invalid format (missing ':' seperator?)");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})?;
|
||||
let (client_id, secret) = parse_basic_authz(client_authz)?;
|
||||
|
||||
// Get the o2rs for the handle.
|
||||
let o2rs = self.inner.rs_set.get(client_id).ok_or_else(|| {
|
||||
let o2rs = self.inner.rs_set.get(&client_id).ok_or_else(|| {
|
||||
admin_warn!("Invalid oauth2 client_id");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})?;
|
||||
|
@ -568,8 +570,20 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
Some(code_xchg.scopes.join(" "))
|
||||
};
|
||||
|
||||
let iat = ct.as_secs() as i64;
|
||||
|
||||
let o2uat = Oauth2UserToken {
|
||||
toktype: Oauth2TokenType::Access,
|
||||
uat: code_xchg.uat,
|
||||
scopes: code_xchg.scopes,
|
||||
iat,
|
||||
nbf: iat,
|
||||
// TODO: Make configurable!
|
||||
exp: iat + 480,
|
||||
};
|
||||
|
||||
// If we are type == Uat, then we re-use the same encryption material here.
|
||||
let access_token_data = serde_json::to_vec(&code_xchg.uat).map_err(|e| {
|
||||
let access_token_data = serde_json::to_vec(&o2uat).map_err(|e| {
|
||||
admin_error!(err = ?e, "Unable to encode uat data");
|
||||
Oauth2Error::ServerError(OperationError::SerdeJsonError)
|
||||
})?;
|
||||
|
@ -588,6 +602,110 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
scope,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn check_oauth2_token_introspect(
|
||||
&self,
|
||||
idms: &IdmServerProxyReadTransaction<'_>,
|
||||
client_authz: &str,
|
||||
intr_req: &AccessTokenIntrospectRequest,
|
||||
ct: Duration,
|
||||
) -> Result<AccessTokenIntrospectResponse, Oauth2Error> {
|
||||
let (client_id, secret) = parse_basic_authz(client_authz)?;
|
||||
|
||||
// Get the o2rs for the handle.
|
||||
let o2rs = self.inner.rs_set.get(&client_id).ok_or_else(|| {
|
||||
admin_warn!("Invalid oauth2 client_id");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})?;
|
||||
|
||||
// check the secret.
|
||||
if o2rs.authz_secret != secret {
|
||||
security_info!("Invalid oauth2 client_id secret");
|
||||
return Err(Oauth2Error::AuthenticationRequired);
|
||||
}
|
||||
// We are authenticated! Yay! Now we can actually check things ...
|
||||
|
||||
// In these cases instead of error, we need to return Active:false
|
||||
// Check the Token is correctly signed.
|
||||
let access_token: Result<Oauth2UserToken, _> = o2rs
|
||||
.token_fernet
|
||||
.decrypt_at_time(&intr_req.token, None, ct.as_secs())
|
||||
.map_err(|_| {
|
||||
admin_error!("Failed to decrypt access token introspect request");
|
||||
Oauth2Error::InvalidRequest
|
||||
})
|
||||
.and_then(|data| {
|
||||
serde_json::from_slice(&data).map_err(|e| {
|
||||
admin_error!("Failed to deserialise access token - {:?}", e);
|
||||
Oauth2Error::InvalidRequest
|
||||
})
|
||||
});
|
||||
|
||||
let access_token = match access_token {
|
||||
Ok(a) => a,
|
||||
Err(_) => return Ok(AccessTokenIntrospectResponse::inactive()),
|
||||
};
|
||||
|
||||
let valid = idms
|
||||
.check_uat_valid(&access_token.uat, ct)
|
||||
.map_err(|_| admin_error!("Account is not valid"));
|
||||
|
||||
match valid {
|
||||
Ok(true) => {}
|
||||
_ => return Ok(AccessTokenIntrospectResponse::inactive()),
|
||||
};
|
||||
|
||||
let scope = if access_token.scopes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(access_token.scopes.join(" "))
|
||||
};
|
||||
|
||||
Ok(AccessTokenIntrospectResponse {
|
||||
active: true,
|
||||
scope,
|
||||
client_id: Some(client_id),
|
||||
username: Some(access_token.uat.spn.clone()),
|
||||
token_type: Some(access_token.toktype.to_string()),
|
||||
exp: Some(access_token.exp),
|
||||
iat: Some(access_token.iat),
|
||||
nbf: Some(access_token.nbf),
|
||||
sub: Some(access_token.uat.uuid.to_string()),
|
||||
aud: None,
|
||||
iss: None,
|
||||
jti: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_basic_authz(client_authz: &str) -> Result<(String, String), Oauth2Error> {
|
||||
// Check the client_authz
|
||||
let authz = base64::decode(&client_authz)
|
||||
.map_err(|_| {
|
||||
admin_error!("Basic authz invalid base64");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})
|
||||
.and_then(|data| {
|
||||
String::from_utf8(data).map_err(|_| {
|
||||
admin_error!("Basic authz invalid utf8");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})
|
||||
})?;
|
||||
|
||||
// Get the first :, it should be our delim.
|
||||
//
|
||||
let mut split_iter = authz.split(':');
|
||||
|
||||
let client_id = split_iter.next().ok_or_else(|| {
|
||||
admin_error!("Basic authz invalid format (corrupt input?)");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})?;
|
||||
let secret = split_iter.next().ok_or_else(|| {
|
||||
admin_error!("Basic authz invalid format (missing ':' seperator?)");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})?;
|
||||
|
||||
Ok((client_id.to_string(), secret.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -597,6 +715,9 @@ mod tests {
|
|||
use crate::idm::server::{IdmServer, IdmServerTransaction};
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::event::ModifyEvent;
|
||||
use crate::modify::{Modify, ModifyList};
|
||||
|
||||
use kanidm_proto::oauth2::*;
|
||||
use kanidm_proto::v1::{AuthType, UserAuthToken};
|
||||
use webauthn_rs::base64_data::Base64UrlSafeData;
|
||||
|
@ -1102,4 +1223,83 @@ mod tests {
|
|||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_oauth2_token_introspect() {
|
||||
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);
|
||||
let client_authz = base64::encode(format!("test_resource_server:{}", secret));
|
||||
|
||||
let idms_prox_read = idms.proxy_read();
|
||||
|
||||
// == Setup the authorisation request
|
||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||
let consent_request =
|
||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
||||
|
||||
// == 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)
|
||||
.expect("Failed to perform oauth2 permit");
|
||||
|
||||
let token_req = AccessTokenRequest {
|
||||
grant_type: "authorization_code".to_string(),
|
||||
code: permit_success.code.clone(),
|
||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||
client_id: None,
|
||||
code_verifier,
|
||||
};
|
||||
let oauth2_token = idms_prox_read
|
||||
.check_oauth2_token_exchange(&client_authz, &token_req, ct)
|
||||
.expect("Unable to exchange for oauth2 token");
|
||||
|
||||
// Okay, now we have the token, we can check it works with introspect.
|
||||
let intr_request = AccessTokenIntrospectRequest {
|
||||
token: oauth2_token.access_token.clone(),
|
||||
token_type_hint: None,
|
||||
};
|
||||
let intr_response = idms_prox_read
|
||||
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
|
||||
.expect("Failed to inspect token");
|
||||
|
||||
assert!(intr_response.active);
|
||||
assert!(intr_response.scope.as_deref() == Some("test"));
|
||||
assert!(intr_response.client_id.as_deref() == Some("test_resource_server"));
|
||||
assert!(intr_response.username.as_deref() == Some("admin@example.com"));
|
||||
assert!(intr_response.token_type.as_deref() == Some("access_token"));
|
||||
assert!(intr_response.iat == Some(ct.as_secs() as i64));
|
||||
assert!(intr_response.nbf == Some(ct.as_secs() as i64));
|
||||
|
||||
drop(idms_prox_read);
|
||||
// start a write,
|
||||
|
||||
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 me_inv_m = unsafe {
|
||||
ModifyEvent::new_internal_invalid(
|
||||
filter!(f_eq("name", PartialValue::new_iname("admin"))),
|
||||
ModifyList::new_list(vec![Modify::Present(
|
||||
AttrString::from("account_expire"),
|
||||
v_expire,
|
||||
)]),
|
||||
)
|
||||
};
|
||||
// go!
|
||||
assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok());
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
|
||||
// start a new read
|
||||
// check again.
|
||||
let idms_prox_read = idms.proxy_read();
|
||||
let intr_response = idms_prox_read
|
||||
.check_oauth2_token_introspect(&client_authz, &intr_request, ct)
|
||||
.expect("Failed to inspect token");
|
||||
|
||||
assert!(!intr_response.active);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,9 @@ use crate::idm::event::{
|
|||
};
|
||||
use crate::idm::mfareg::{MfaRegCred, MfaRegNext, MfaRegSession};
|
||||
use crate::idm::oauth2::{
|
||||
AccessTokenRequest, AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess,
|
||||
ConsentRequest, Oauth2Error, Oauth2ResourceServers, Oauth2ResourceServersReadTransaction,
|
||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||
AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess, ConsentRequest, Oauth2Error,
|
||||
Oauth2ResourceServers, Oauth2ResourceServersReadTransaction,
|
||||
Oauth2ResourceServersWriteTransaction,
|
||||
};
|
||||
use crate::idm::radius::RadiusAccount;
|
||||
|
@ -346,7 +347,6 @@ pub trait IdmServerTransaction<'a> {
|
|||
|
||||
fn get_uat_bundy_txn(&self) -> &HS512;
|
||||
|
||||
// ! TRACING INTEGRATED
|
||||
fn validate_and_parse_uat(
|
||||
&self,
|
||||
token: Option<&str>,
|
||||
|
@ -373,7 +373,22 @@ pub trait IdmServerTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// ! TRACING INTEGRATED
|
||||
fn check_uat_valid(&self, uat: &UserAuthToken, ct: Duration) -> Result<bool, OperationError> {
|
||||
let entry = self
|
||||
.get_qs_txn()
|
||||
.internal_search_uuid(&uat.uuid)
|
||||
.map_err(|e| {
|
||||
admin_error!(?e, "from_ro_uat failed");
|
||||
e
|
||||
})?;
|
||||
|
||||
Ok(Account::check_within_valid_time(
|
||||
ct,
|
||||
entry.get_ava_single_datetime("account_valid_from").as_ref(),
|
||||
entry.get_ava_single_datetime("account_expire").as_ref(),
|
||||
))
|
||||
}
|
||||
|
||||
fn process_uat_to_identity(
|
||||
&self,
|
||||
uat: &UserAuthToken,
|
||||
|
@ -1076,6 +1091,16 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
self.oauth2rs
|
||||
.check_oauth2_token_exchange(client_authz, token_req, ct)
|
||||
}
|
||||
|
||||
pub fn check_oauth2_token_introspect(
|
||||
&self,
|
||||
client_authz: &str,
|
||||
intr_req: &AccessTokenIntrospectRequest,
|
||||
ct: Duration,
|
||||
) -> Result<AccessTokenIntrospectResponse, Oauth2Error> {
|
||||
self.oauth2rs
|
||||
.check_oauth2_token_introspect(self, client_authz, intr_req, ct)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IdmServerTransaction<'a> for IdmServerProxyWriteTransaction<'a> {
|
||||
|
|
Loading…
Reference in a new issue