kanidmd_lib/idm/
oauth2.rs

1//! Oauth2 resource server configurations
2//!
3//! This contains the in memory and loaded set of active OAuth2 resource server
4//! integrations, which are then able to be used an accessed from the IDM layer
5//! for operations involving OAuth2 authentication processing.
6
7use crate::idm::account::Account;
8use crate::idm::server::{
9    IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
10};
11use crate::prelude::*;
12use crate::server::keys::{KeyObject, KeyProvidersTransaction, KeyProvidersWriteTransaction};
13use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE};
14use base64::{engine::general_purpose, Engine as _};
15pub use compact_jwt::{compact::JwkKeySet, OidcToken};
16use compact_jwt::{
17    crypto::{JweA128GCMEncipher, JweA128KWEncipher},
18    jwe::Jwe,
19    jws::JwsBuilder,
20    JweCompact, JwsCompact, OidcClaims, OidcSubject,
21};
22use concread::cowcell::*;
23use hashbrown::HashMap;
24use hashbrown::HashSet;
25use kanidm_proto::constants::*;
26pub use kanidm_proto::oauth2::{
27    AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
28    AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse, GrantTypeReq,
29    OAuth2RFC9068Token, OAuth2RFC9068TokenExtensions, Oauth2Rfc8414MetadataResponse,
30    OidcDiscoveryResponse, OidcWebfingerRel, OidcWebfingerResponse, PkceAlg, TokenRevokeRequest,
31};
32use kanidm_proto::oauth2::{
33    AccessTokenType, ClaimType, ClientAuth, ClientPostAuth, DeviceAuthorizationResponse,
34    DisplayValue, GrantType, IdTokenSignAlg, ResponseMode, ResponseType, SubjectType,
35    TokenEndpointAuthMethod,
36};
37use openssl::sha;
38use serde::{Deserialize, Serialize};
39use serde_with::{formats, serde_as};
40use std::collections::btree_map::Entry as BTreeEntry;
41use std::collections::{BTreeMap, BTreeSet};
42use std::fmt;
43use std::str::FromStr;
44use std::sync::Arc;
45use std::time::Duration;
46use time::OffsetDateTime;
47use tracing::trace;
48use uri::{OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT};
49use url::{Host, Origin, Url};
50use utoipa::ToSchema;
51
52#[derive(Serialize, Deserialize, Debug, PartialEq, ToSchema)]
53#[serde(rename_all = "snake_case")]
54pub enum Oauth2Error {
55    // Non-standard - these are used to guide some control flow.
56    AuthenticationRequired,
57    InvalidClientId,
58    InvalidOrigin,
59    // Standard
60    InvalidRequest,
61    InvalidGrant,
62    UnauthorizedClient,
63    AccessDenied,
64    UnsupportedResponseType,
65    InvalidScope,
66    ServerError(OperationError),
67    TemporarilyUnavailable,
68    // from https://datatracker.ietf.org/doc/html/rfc6750
69    InvalidToken,
70    InsufficientScope,
71    // from https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.1
72    UnsupportedTokenType,
73    /// <https://datatracker.ietf.org/doc/html/rfc8628#section-3.5>  A variant of "authorization_pending", the authorization request is
74    ///   still pending and polling should continue, but the interval MUST
75    ///   be increased by 5 seconds for this and all subsequent requests.
76    SlowDown,
77    /// The authorization request is still pending as the end user hasn't
78    ///   yet completed the user-interaction steps (Section 3.3).  The
79    ///   client SHOULD repeat the access token request to the token
80    ///   endpoint (a process known as polling).  Before each new request,
81    ///   the client MUST wait at least the number of seconds specified by
82    ///   the "interval" parameter of the device authorization response (see
83    ///   Section 3.2), or 5 seconds if none was provided, and respect any
84    ///   increase in the polling interval required by the "slow_down"
85    ///   error.
86    AuthorizationPending,
87    /// The "device_code" has expired, and the device authorization
88    ///   session has concluded.  The client MAY commence a new device
89    ///   authorization request but SHOULD wait for user interaction before
90    ///   restarting to avoid unnecessary polling.
91    ExpiredToken,
92}
93
94impl std::fmt::Display for Oauth2Error {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        f.write_str(match self {
97            Oauth2Error::AuthenticationRequired => "authentication_required",
98            Oauth2Error::InvalidClientId => "invalid_client_id",
99            Oauth2Error::InvalidOrigin => "invalid_origin",
100            Oauth2Error::InvalidGrant => "invalid_grant",
101            Oauth2Error::InvalidRequest => "invalid_request",
102            Oauth2Error::UnauthorizedClient => "unauthorized_client",
103            Oauth2Error::AccessDenied => "access_denied",
104            Oauth2Error::UnsupportedResponseType => "unsupported_response_type",
105            Oauth2Error::InvalidScope => "invalid_scope",
106            Oauth2Error::ServerError(_) => "server_error",
107            Oauth2Error::TemporarilyUnavailable => "temporarily_unavailable",
108            Oauth2Error::InvalidToken => "invalid_token",
109            Oauth2Error::InsufficientScope => "insufficient_scope",
110            Oauth2Error::UnsupportedTokenType => "unsupported_token_type",
111            Oauth2Error::SlowDown => "slow_down",
112            Oauth2Error::AuthorizationPending => "authorization_pending",
113            Oauth2Error::ExpiredToken => "expired_token",
114        })
115    }
116}
117
118// == internal state formats that we encrypt and send.
119#[derive(Serialize, Deserialize, Debug, PartialEq)]
120enum SupportedResponseMode {
121    Query,
122    Fragment,
123}
124
125#[serde_as]
126#[derive(Serialize, Deserialize, Debug, PartialEq)]
127struct ConsentToken {
128    pub client_id: String,
129    // Must match the session id of the Uat,
130    pub session_id: Uuid,
131    pub expiry: u64,
132
133    // So we can ensure that we really match the same uat to prevent confusions.
134    pub ident_id: IdentityId,
135    // CSRF
136    pub state: Option<String>,
137    // The S256 code challenge.
138    #[serde_as(
139        as = "Option<serde_with::base64::Base64<serde_with::base64::UrlSafe, formats::Unpadded>>"
140    )]
141    pub code_challenge: Option<Vec<u8>>,
142    // Where the client wants us to go back to.
143    pub redirect_uri: Url,
144    // The scopes being granted
145    pub scopes: BTreeSet<String>,
146    // We stash some details here for oidc.
147    pub nonce: Option<String>,
148    /// The format the response should be returned to the application in.
149    pub response_mode: SupportedResponseMode,
150}
151
152#[serde_as]
153#[derive(Serialize, Deserialize, Debug)]
154struct TokenExchangeCode {
155    // We don't need the client_id here, because it's signed with an RS specific
156    // key which gives us the assurance that it's the correct combination.
157    pub account_uuid: Uuid,
158    pub session_id: Uuid,
159
160    pub expiry: u64,
161
162    // The S256 code challenge.
163    #[serde_as(
164        as = "Option<serde_with::base64::Base64<serde_with::base64::UrlSafe, formats::Unpadded>>"
165    )]
166    pub code_challenge: Option<Vec<u8>>,
167    // The original redirect uri
168    pub redirect_uri: Url,
169    // The scopes being granted
170    pub scopes: BTreeSet<String>,
171    // We stash some details here for oidc.
172    pub nonce: Option<String>,
173}
174
175#[derive(Serialize, Deserialize, Debug)]
176pub(crate) enum Oauth2TokenType {
177    Refresh {
178        scopes: BTreeSet<String>,
179        parent_session_id: Uuid,
180        session_id: Uuid,
181        exp: i64,
182        uuid: Uuid,
183        //
184        iat: i64,
185        nbf: i64,
186        // We stash some details here for oidc.
187        nonce: Option<String>,
188    },
189    ClientAccess {
190        scopes: BTreeSet<String>,
191        session_id: Uuid,
192        uuid: Uuid,
193        exp: i64,
194        iat: i64,
195        nbf: i64,
196    },
197}
198
199impl fmt::Display for Oauth2TokenType {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        match self {
202            Oauth2TokenType::Refresh { session_id, .. } => {
203                write!(f, "refresh_token ({session_id}) ")
204            }
205            Oauth2TokenType::ClientAccess { session_id, .. } => {
206                write!(f, "client_access_token ({session_id})")
207            }
208        }
209    }
210}
211
212#[derive(Debug)]
213pub enum AuthoriseResponse {
214    AuthenticationRequired {
215        // A pretty-name of the client
216        client_name: String,
217        // A username hint, if any
218        login_hint: Option<String>,
219    },
220    ConsentRequested {
221        // A pretty-name of the client
222        client_name: String,
223        // A list of scopes requested / to be issued.
224        scopes: BTreeSet<String>,
225        // Extra PII that may be requested
226        pii_scopes: BTreeSet<String>,
227        // The users displayname (?)
228        // pub display_name: String,
229        // The token we need to be given back to allow this to proceed
230        consent_token: String,
231    },
232    Permitted(AuthorisePermitSuccess),
233}
234
235#[derive(Debug)]
236pub struct AuthorisePermitSuccess {
237    // Where the client wants us to go back to.
238    pub redirect_uri: Url,
239    // The CSRF as a string
240    pub state: Option<String>,
241    // The exchange code as a String
242    pub code: String,
243    /// The format the response should be returned to the application in.
244    response_mode: SupportedResponseMode,
245}
246
247impl AuthorisePermitSuccess {
248    /// Builds a redirect URI to go back to the application when permission was
249    /// granted.
250    pub fn build_redirect_uri(&self) -> Url {
251        let mut redirect_uri = self.redirect_uri.clone();
252
253        // Always clear the fragment per RFC
254        redirect_uri.set_fragment(None);
255
256        match self.response_mode {
257            SupportedResponseMode::Query => {
258                redirect_uri
259                    .query_pairs_mut()
260                    .append_pair("code", &self.code);
261
262                if let Some(state) = self.state.as_ref() {
263                    redirect_uri.query_pairs_mut().append_pair("state", state);
264                };
265            }
266            SupportedResponseMode::Fragment => {
267                redirect_uri.set_query(None);
268
269                // Per [the RFC](https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2), we can't set query pairs on fragment-containing redirects, only query ones.
270                let mut uri_builder = url::form_urlencoded::Serializer::new(String::new());
271                uri_builder.append_pair("code", &self.code);
272                if let Some(state) = self.state.as_ref() {
273                    uri_builder.append_pair("state", state);
274                };
275                let encoded = uri_builder.finish();
276
277                redirect_uri.set_fragment(Some(&encoded))
278            }
279        }
280
281        redirect_uri
282    }
283}
284
285#[derive(Debug)]
286pub struct AuthoriseReject {
287    // Where the client wants us to go back to.
288    pub redirect_uri: Url,
289    /// The format the response should be returned to the application in.
290    response_mode: SupportedResponseMode,
291}
292
293impl AuthoriseReject {
294    /// Builds a redirect URI to go back to the application when permission was
295    /// rejected.
296    pub fn build_redirect_uri(&self) -> Url {
297        let mut redirect_uri = self.redirect_uri.clone();
298
299        // Always clear query and fragment, regardless of the response mode
300        redirect_uri.set_query(None);
301        redirect_uri.set_fragment(None);
302
303        // We can't set query pairs on fragments, only query.
304        let encoded = url::form_urlencoded::Serializer::new(String::new())
305            .append_pair("error", "access_denied")
306            .append_pair("error_description", "authorisation rejected")
307            .finish();
308
309        match self.response_mode {
310            SupportedResponseMode::Query => redirect_uri.set_query(Some(&encoded)),
311            SupportedResponseMode::Fragment => redirect_uri.set_fragment(Some(&encoded)),
312        }
313
314        redirect_uri
315    }
316}
317
318#[derive(Clone)]
319enum OauthRSType {
320    Basic {
321        authz_secret: String,
322        enable_pkce: bool,
323    },
324    // Public clients must have pkce.
325    Public {
326        allow_localhost_redirect: bool,
327    },
328}
329
330impl OauthRSType {
331    /// We only allow localhost redirects if PKCE is enabled/required
332    fn allow_localhost_redirect(&self) -> bool {
333        match self {
334            OauthRSType::Basic { .. } => false,
335            OauthRSType::Public {
336                allow_localhost_redirect,
337            } => *allow_localhost_redirect,
338        }
339    }
340}
341
342impl std::fmt::Debug for OauthRSType {
343    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
344        let mut ds = f.debug_struct("OauthRSType");
345        match self {
346            OauthRSType::Basic { enable_pkce, .. } => {
347                ds.field("type", &"basic").field("pkce", enable_pkce)
348            }
349            OauthRSType::Public {
350                allow_localhost_redirect,
351            } => ds
352                .field("type", &"public")
353                .field("allow_localhost_redirect", allow_localhost_redirect),
354        };
355        ds.finish()
356    }
357}
358
359#[derive(Clone, Debug)]
360struct ClaimValue {
361    join: OauthClaimMapJoin,
362    values: BTreeSet<String>,
363}
364
365impl ClaimValue {
366    fn merge(&mut self, other: &Self) {
367        self.values.extend(other.values.iter().cloned())
368    }
369
370    fn to_json_value(&self) -> serde_json::Value {
371        let join_str = match self.join {
372            OauthClaimMapJoin::JsonArray => {
373                let arr: Vec<_> = self
374                    .values
375                    .iter()
376                    .cloned()
377                    .map(serde_json::Value::String)
378                    .collect();
379
380                // This shortcuts out.
381                return serde_json::Value::Array(arr);
382            }
383            joiner => joiner.to_str(),
384        };
385
386        let joined = str_concat!(&self.values, join_str);
387
388        serde_json::Value::String(joined)
389    }
390}
391
392#[derive(Clone, Copy, Debug)]
393enum SignatureAlgo {
394    Es256,
395    Rs256,
396}
397
398#[derive(Clone)]
399pub struct Oauth2RS {
400    name: String,
401    displayname: String,
402    uuid: Uuid,
403
404    origins: HashSet<Origin>,
405    opaque_origins: HashSet<Url>,
406    redirect_uris: HashSet<Url>,
407    origin_https_required: bool,
408    strict_redirect_uri: bool,
409
410    claim_map: BTreeMap<Uuid, Vec<(String, ClaimValue)>>,
411    scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
412    sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
413    client_scopes: BTreeSet<String>,
414    client_sup_scopes: BTreeSet<String>,
415    // Our internal exchange encryption material for this rs.
416    sign_alg: SignatureAlgo,
417    key_object: Arc<KeyObject>,
418
419    // For oidc we also need our issuer url.
420    iss: Url,
421    // For discovery we need to build and keep a number of values.
422    authorization_endpoint: Url,
423    token_endpoint: Url,
424    revocation_endpoint: Url,
425    introspection_endpoint: Url,
426    userinfo_endpoint: Url,
427    jwks_uri: Url,
428    scopes_supported: BTreeSet<String>,
429    prefer_short_username: bool,
430    type_: OauthRSType,
431    /// Does the RS have a custom image set? If not, we use the default.
432    has_custom_image: bool,
433
434    device_authorization_endpoint: Option<Url>,
435}
436
437impl Oauth2RS {
438    pub fn is_basic(&self) -> bool {
439        match self.type_ {
440            OauthRSType::Basic { .. } => true,
441            OauthRSType::Public { .. } => false,
442        }
443    }
444
445    pub fn is_pkce(&self) -> bool {
446        match self.type_ {
447            OauthRSType::Basic { .. } => false,
448            OauthRSType::Public { .. } => true,
449        }
450    }
451
452    /// Does this client require PKCE?
453    pub fn require_pkce(&self) -> bool {
454        match &self.type_ {
455            OauthRSType::Basic { enable_pkce, .. } => *enable_pkce,
456            OauthRSType::Public { .. } => true,
457        }
458    }
459
460    /// Does this RS have device flow enabled?
461    pub fn device_flow_enabled(&self) -> bool {
462        self.device_authorization_endpoint.is_some()
463    }
464}
465
466impl std::fmt::Debug for Oauth2RS {
467    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
468        f.debug_struct("Oauth2RS")
469            .field("name", &self.name)
470            .field("displayname", &self.displayname)
471            .field("uuid", &self.uuid)
472            .field("type", &self.type_)
473            .field("origins", &self.origins)
474            .field("opaque_origins", &self.opaque_origins)
475            .field("scope_maps", &self.scope_maps)
476            .field("sup_scope_maps", &self.sup_scope_maps)
477            .field("claim_map", &self.claim_map)
478            .field("has_custom_image", &self.has_custom_image)
479            .finish()
480    }
481}
482
483#[derive(Clone)]
484struct Oauth2RSInner {
485    origin: Url,
486    consent_key: JweA128KWEncipher,
487    private_rs_set: HashMap<String, Oauth2RS>,
488}
489
490impl Oauth2RSInner {
491    fn rs_set_get(&self, client_id: &str) -> Option<&Oauth2RS> {
492        self.private_rs_set.get(client_id.to_lowercase().as_str())
493    }
494}
495
496pub struct Oauth2ResourceServers {
497    inner: CowCell<Oauth2RSInner>,
498}
499
500pub struct Oauth2ResourceServersReadTransaction {
501    inner: CowCellReadTxn<Oauth2RSInner>,
502}
503
504pub struct Oauth2ResourceServersWriteTransaction<'a> {
505    inner: CowCellWriteTxn<'a, Oauth2RSInner>,
506}
507
508impl Oauth2ResourceServers {
509    pub fn new(origin: Url) -> Result<Self, OperationError> {
510        let consent_key = JweA128KWEncipher::generate_ephemeral()
511            .map_err(|_| OperationError::CryptographyError)?;
512
513        Ok(Oauth2ResourceServers {
514            inner: CowCell::new(Oauth2RSInner {
515                origin,
516                consent_key,
517                private_rs_set: HashMap::new(),
518            }),
519        })
520    }
521
522    pub fn read(&self) -> Oauth2ResourceServersReadTransaction {
523        Oauth2ResourceServersReadTransaction {
524            inner: self.inner.read(),
525        }
526    }
527
528    pub fn write(&self) -> Oauth2ResourceServersWriteTransaction<'_> {
529        Oauth2ResourceServersWriteTransaction {
530            inner: self.inner.write(),
531        }
532    }
533}
534
535/// For when you've got the bearer auth and the post auth and you just want the resulting auth attempt
536fn get_client_auth(
537    client_auth_info: &ClientAuthInfo,
538    client_post_auth: &ClientPostAuth,
539) -> Result<ClientAuth, Oauth2Error> {
540    if let Some(client_authz) = client_auth_info.basic_authz.as_ref() {
541        parse_basic_authz(client_authz.as_str())
542    } else if let Some(client_id) = &client_post_auth.client_id {
543        Ok(ClientAuth {
544            client_id: client_id.clone(),
545            client_secret: client_post_auth.client_secret.clone(),
546        })
547    } else {
548        admin_warn!("OAuth2 client authentication not provided");
549        Err(Oauth2Error::AuthenticationRequired)
550    }
551}
552
553impl Oauth2ResourceServersWriteTransaction<'_> {
554    #[instrument(level = "debug", name = "oauth2::reload", skip_all)]
555    pub fn reload(
556        &mut self,
557        value: Vec<Arc<EntrySealedCommitted>>,
558        key_providers: &KeyProvidersWriteTransaction,
559        domain_level: DomainVersion,
560    ) -> Result<(), OperationError> {
561        let rs_set: Result<HashMap<_, _>, _> = value
562            .into_iter()
563            .map(|ent| {
564                let uuid = ent.get_uuid();
565                trace!(?uuid, "Checking OAuth2 configuration");
566                // From each entry, attempt to make an OAuth2 configuration.
567                if !ent
568                    .attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
569                {
570                    error!("Missing class oauth2_resource_server");
571                    // Check we have oauth2_resource_server class
572                    return Err(OperationError::InvalidEntryState);
573                }
574
575                let Some(key_object) = key_providers.get_key_object_handle(uuid) else {
576                    error!("OAuth2 RS is missing its key object!");
577                    return Err(OperationError::InvalidEntryState);
578                };
579
580                let type_ = if ent.attribute_equality(
581                    Attribute::Class,
582                    &EntryClass::OAuth2ResourceServerBasic.into(),
583                ) {
584                    let authz_secret = ent
585                        .get_ava_single_secret(Attribute::OAuth2RsBasicSecret)
586                        .map(str::to_string)
587                        .ok_or(OperationError::InvalidValueState)?;
588
589                    let enable_pkce = ent
590                        .get_ava_single_bool(Attribute::OAuth2AllowInsecureClientDisablePkce)
591                        .map(|e| !e)
592                        .unwrap_or(true);
593
594                    OauthRSType::Basic {
595                        authz_secret,
596                        enable_pkce,
597                    }
598                } else if ent.attribute_equality(
599                    Attribute::Class,
600                    &EntryClass::OAuth2ResourceServerPublic.into(),
601                ) {
602                    let allow_localhost_redirect = ent
603                        .get_ava_single_bool(Attribute::OAuth2AllowLocalhostRedirect)
604                        .unwrap_or(false);
605
606                    OauthRSType::Public {
607                        allow_localhost_redirect,
608                    }
609                } else {
610                    error!("Missing class determining OAuth2 rs type");
611                    return Err(OperationError::InvalidEntryState);
612                };
613
614                // Now we know we can load the shared attrs.
615                let name = ent
616                    .get_ava_single_iname(Attribute::Name)
617                    .map(str::to_string)
618                    .ok_or(OperationError::InvalidValueState)?;
619
620                let displayname = ent
621                    .get_ava_single_utf8(Attribute::DisplayName)
622                    .map(str::to_string)
623                    .ok_or(OperationError::InvalidValueState)?;
624
625                // Setup the landing uri and its implied origin, as well as
626                // the supplemental origins.
627                let landing_url = ent
628                    .get_ava_single_url(Attribute::OAuth2RsOriginLanding)
629                    .cloned()
630                    .ok_or(OperationError::InvalidValueState)?;
631
632                let maybe_extra_urls = ent
633                    .get_ava_set(Attribute::OAuth2RsOrigin)
634                    .and_then(|s| s.as_url_set());
635
636                let len_uris = maybe_extra_urls.map(|s| s.len() + 1).unwrap_or(1);
637
638                // If we are DL8, then strict enforcement is always required.
639                let strict_redirect_uri = cfg!(test)
640                    || domain_level >= DOMAIN_LEVEL_8
641                    || ent
642                        .get_ava_single_bool(Attribute::OAuth2StrictRedirectUri)
643                        .unwrap_or(false);
644
645                // The reason we have to allocate this is that we need to do some processing on these
646                // urls to determine if they are opaque or not.
647                let mut redirect_uris_v = Vec::with_capacity(len_uris);
648
649                redirect_uris_v.push(landing_url);
650                if let Some(extra_origins) = maybe_extra_urls {
651                    for x_origin in extra_origins {
652                        redirect_uris_v.push(x_origin.clone());
653                    }
654                }
655
656                // Now redirect_uris has the full set of the landing uri and the other uris
657                // that may or may not be an opaque origin. We need to split these up now.
658
659                let mut origins = HashSet::with_capacity(len_uris);
660                let mut redirect_uris = HashSet::with_capacity(len_uris);
661                let mut opaque_origins = HashSet::with_capacity(len_uris);
662                let mut origin_https_required = false;
663
664                for mut uri in redirect_uris_v.into_iter() {
665                    // https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2
666                    // Must not include a fragment.
667                    uri.set_fragment(None);
668                    // Given the presence of a single https url, then all other urls must be https.
669                    if uri.scheme() == "https" {
670                        origin_https_required = true;
671                        origins.insert(uri.origin());
672                        redirect_uris.insert(uri);
673                    } else if uri.scheme() == "http" {
674                        origins.insert(uri.origin());
675                        redirect_uris.insert(uri);
676                    } else {
677                        opaque_origins.insert(uri);
678                    }
679                }
680
681                let scope_maps = ent
682                    .get_ava_as_oauthscopemaps(Attribute::OAuth2RsScopeMap)
683                    .cloned()
684                    .unwrap_or_default();
685
686                let sup_scope_maps = ent
687                    .get_ava_as_oauthscopemaps(Attribute::OAuth2RsSupScopeMap)
688                    .cloned()
689                    .unwrap_or_default();
690
691                // From our scope maps we can now determine what scopes would be granted to our
692                // client during a client credentials authentication.
693                let (client_scopes, client_sup_scopes) =
694                    if let Some(client_member_of) = ent.get_ava_refer(Attribute::MemberOf) {
695                        let client_scopes = scope_maps
696                            .iter()
697                            .filter_map(|(u, m)| {
698                                if client_member_of.contains(u) {
699                                    Some(m.iter())
700                                } else {
701                                    None
702                                }
703                            })
704                            .flatten()
705                            .cloned()
706                            .collect::<BTreeSet<_>>();
707
708                        let client_sup_scopes = sup_scope_maps
709                            .iter()
710                            .filter_map(|(u, m)| {
711                                if client_member_of.contains(u) {
712                                    Some(m.iter())
713                                } else {
714                                    None
715                                }
716                            })
717                            .flatten()
718                            .cloned()
719                            .collect::<BTreeSet<_>>();
720
721                        (client_scopes, client_sup_scopes)
722                    } else {
723                        (BTreeSet::default(), BTreeSet::default())
724                    };
725
726                let e_claim_maps = ent
727                    .get_ava_set(Attribute::OAuth2RsClaimMap)
728                    .and_then(|vs| vs.as_oauthclaim_map());
729
730                // ⚠️  Claim Maps as they are stored in the DB are optimised
731                // for referential integrity and user interaction. However we
732                // need to "invert" these for fast lookups during actual
733                // operation of the oauth2 client.
734                let claim_map = if let Some(e_claim_maps) = e_claim_maps {
735                    let mut claim_map = BTreeMap::default();
736
737                    for (claim_name, claim_mapping) in e_claim_maps.iter() {
738                        for (group_uuid, claim_values) in claim_mapping.values().iter() {
739                            // We always insert/append here because the outer claim_name has
740                            // to be unique.
741                            match claim_map.entry(*group_uuid) {
742                                BTreeEntry::Vacant(e) => {
743                                    e.insert(vec![(
744                                        claim_name.clone(),
745                                        ClaimValue {
746                                            join: claim_mapping.join(),
747                                            values: claim_values.clone(),
748                                        },
749                                    )]);
750                                }
751                                BTreeEntry::Occupied(mut e) => {
752                                    e.get_mut().push((
753                                        claim_name.clone(),
754                                        ClaimValue {
755                                            join: claim_mapping.join(),
756                                            values: claim_values.clone(),
757                                        },
758                                    ));
759                                }
760                            }
761                        }
762                    }
763
764                    claim_map
765                } else {
766                    BTreeMap::default()
767                };
768
769                let sign_alg = if ent
770                    .get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable)
771                    .unwrap_or(false)
772                {
773                    SignatureAlgo::Rs256
774                } else {
775                    SignatureAlgo::Es256
776                };
777
778                let prefer_short_username = ent
779                    .get_ava_single_bool(Attribute::OAuth2PreferShortUsername)
780                    .unwrap_or(false);
781
782                let has_custom_image = ent.get_ava_single_image(Attribute::Image).is_some();
783
784                let mut authorization_endpoint = self.inner.origin.clone();
785                authorization_endpoint.set_path("/ui/oauth2");
786
787                let mut token_endpoint = self.inner.origin.clone();
788                token_endpoint.set_path(uri::OAUTH2_TOKEN_ENDPOINT);
789
790                let mut revocation_endpoint = self.inner.origin.clone();
791                revocation_endpoint.set_path(OAUTH2_TOKEN_REVOKE_ENDPOINT);
792
793                let mut introspection_endpoint = self.inner.origin.clone();
794                introspection_endpoint.set_path(OAUTH2_TOKEN_INTROSPECT_ENDPOINT);
795
796                let mut userinfo_endpoint = self.inner.origin.clone();
797                userinfo_endpoint.set_path(&format!("/oauth2/openid/{name}/userinfo"));
798
799                let mut jwks_uri = self.inner.origin.clone();
800                jwks_uri.set_path(&format!("/oauth2/openid/{name}/public_key.jwk"));
801
802                let mut iss = self.inner.origin.clone();
803                iss.set_path(&format!("/oauth2/openid/{name}"));
804
805                let scopes_supported: BTreeSet<String> = scope_maps
806                    .values()
807                    .flat_map(|bts| bts.iter())
808                    .chain(sup_scope_maps.values().flat_map(|bts| bts.iter()))
809                    .cloned()
810                    .collect();
811
812                let device_authorization_endpoint: Option<Url> =
813                    match cfg!(feature = "dev-oauth2-device-flow") {
814                        true => {
815                            match ent
816                                .get_ava_single_bool(Attribute::OAuth2DeviceFlowEnable)
817                                .unwrap_or(false)
818                            {
819                                true => {
820                                    let mut device_authorization_endpoint =
821                                        self.inner.origin.clone();
822                                    device_authorization_endpoint
823                                        .set_path(uri::OAUTH2_AUTHORISE_DEVICE);
824                                    Some(device_authorization_endpoint)
825                                }
826                                false => None,
827                            }
828                        }
829                        false => None,
830                    };
831                let client_id = name.clone();
832                let rscfg = Oauth2RS {
833                    name,
834                    displayname,
835                    uuid,
836                    origins,
837                    opaque_origins,
838                    redirect_uris,
839                    origin_https_required,
840                    strict_redirect_uri,
841                    scope_maps,
842                    sup_scope_maps,
843                    client_scopes,
844                    client_sup_scopes,
845                    claim_map,
846                    sign_alg,
847                    key_object,
848                    iss,
849                    authorization_endpoint,
850                    token_endpoint,
851                    revocation_endpoint,
852                    introspection_endpoint,
853                    userinfo_endpoint,
854                    jwks_uri,
855                    scopes_supported,
856                    prefer_short_username,
857                    type_,
858                    has_custom_image,
859                    device_authorization_endpoint,
860                };
861
862                Ok((client_id, rscfg))
863            })
864            .collect();
865
866        rs_set.map(|mut rs_set| {
867            // Delay getting the inner mut (which may clone) until we know we are ok.
868            let inner_ref = self.inner.get_mut();
869            // Swap them if we are ok
870            std::mem::swap(&mut inner_ref.private_rs_set, &mut rs_set);
871        })
872    }
873
874    pub fn commit(self) {
875        self.inner.commit();
876    }
877}
878
879impl IdmServerProxyWriteTransaction<'_> {
880    #[instrument(level = "debug", skip_all)]
881    pub fn oauth2_token_revoke(
882        &mut self,
883        client_auth_info: &ClientAuthInfo,
884        revoke_req: &TokenRevokeRequest,
885        ct: Duration,
886    ) -> Result<(), Oauth2Error> {
887        let client_auth = get_client_auth(client_auth_info, &revoke_req.client_post_auth)?;
888
889        // Get the o2rs for the handle.
890        let o2rs = self
891            .oauth2rs
892            .inner
893            .rs_set_get(client_auth.client_id.as_str())
894            .ok_or_else(|| {
895                warn!("Invalid OAuth2 client_id");
896                Oauth2Error::AuthenticationRequired
897            })?;
898
899        // check the secret.
900        match &o2rs.type_ {
901            OauthRSType::Basic { authz_secret, .. } => {
902                if Some(authz_secret) != client_auth.client_secret.as_ref() {
903                    info!("Invalid OAuth2 client_id secret, this can happen if your RS is public but you configured a 'basic' type.");
904                    return Err(Oauth2Error::AuthenticationRequired);
905                }
906            }
907            // Relies on the token to be valid.
908            OauthRSType::Public { .. } => {}
909        };
910
911        // We are authenticated! Yay! Now we can actually check things ...
912
913        // Because this is the only path that deals with the tokens that
914        // are either signed *or* encrypted, we need to check both options.
915
916        let (session_id, expiry, uuid) = if let Ok(jwsc) = JwsCompact::from_str(&revoke_req.token) {
917            let access_token = o2rs
918                .key_object
919                .jws_verify(&jwsc)
920                .map_err(|err| {
921                    admin_error!(?err, "Unable to verify access token");
922                    Oauth2Error::InvalidRequest
923                })
924                .and_then(|jws| {
925                    jws.from_json().map_err(|err| {
926                        admin_error!(?err, "Unable to deserialise access token");
927                        Oauth2Error::InvalidRequest
928                    })
929                })?;
930
931            let OAuth2RFC9068Token::<_> {
932                sub: uuid,
933                exp,
934                extensions: OAuth2RFC9068TokenExtensions { session_id, .. },
935                ..
936            } = access_token;
937
938            (session_id, exp, uuid)
939        } else {
940            // Assume it's encrypted.
941            let jwe_compact = JweCompact::from_str(&revoke_req.token).map_err(|_| {
942                error!("Failed to deserialise a valid JWE");
943                Oauth2Error::InvalidRequest
944            })?;
945
946            let token: Oauth2TokenType = o2rs
947                .key_object
948                .jwe_decrypt(&jwe_compact)
949                .map_err(|_| {
950                    error!("Failed to decrypt token revoke request");
951                    Oauth2Error::InvalidRequest
952                })
953                .and_then(|jwe| {
954                    jwe.from_json().map_err(|err| {
955                        error!(?err, "Failed to deserialise token");
956                        Oauth2Error::InvalidRequest
957                    })
958                })?;
959
960            match token {
961                Oauth2TokenType::ClientAccess {
962                    session_id,
963                    exp,
964                    uuid,
965                    ..
966                }
967                | Oauth2TokenType::Refresh {
968                    session_id,
969                    exp,
970                    uuid,
971                    ..
972                } => (session_id, exp, uuid),
973            }
974        };
975
976        // Only submit a revocation if the token is not yet expired.
977        if expiry <= ct.as_secs() as i64 {
978            security_info!(?uuid, "token has expired, returning inactive");
979            return Ok(());
980        }
981
982        // Consider replication. We have servers A and B. A issues our oauth2
983        // token to the client. The resource server then issues the revoke request
984        // to B. In this case A has not yet replicated the session to B, but we
985        // still need to ensure the revoke is respected. As a result, we don't
986        // actually consult if the session is present on the account, we simply
987        // submit the Modify::Remove. This way it's inserted into the entry changelog
988        // and when replication converges the session is actually removed.
989
990        let modlist: ModifyList<ModifyInvalid> = ModifyList::new_list(vec![Modify::Removed(
991            Attribute::OAuth2Session,
992            PartialValue::Refer(session_id),
993        )]);
994
995        self.qs_write
996            .internal_modify(
997                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(uuid))),
998                &modlist,
999            )
1000            .map_err(|e| {
1001                admin_error!("Failed to modify - revoke OAuth2 session {:?}", e);
1002                Oauth2Error::ServerError(e)
1003            })
1004    }
1005
1006    #[instrument(level = "debug", skip_all)]
1007    pub fn check_oauth2_token_exchange(
1008        &mut self,
1009        client_auth_info: &ClientAuthInfo,
1010        token_req: &AccessTokenRequest,
1011        ct: Duration,
1012    ) -> Result<AccessTokenResponse, Oauth2Error> {
1013        // Public clients will send the client_id via the ATR, so we need to handle this case.
1014        let client_auth = get_client_auth(client_auth_info, &token_req.client_post_auth)?;
1015
1016        let o2rs = self.get_client(&client_auth.client_id)?;
1017
1018        // check the secret.
1019        let client_authentication_valid = match &o2rs.type_ {
1020            OauthRSType::Basic { authz_secret, .. } => {
1021                match client_auth.client_secret {
1022                    Some(secret) => {
1023                        if authz_secret == &secret {
1024                            true
1025                        } else {
1026                            info!("Invalid OAuth2 client_id secret");
1027                            return Err(Oauth2Error::AuthenticationRequired);
1028                        }
1029                    }
1030                    None => {
1031                        // We can only get here if we relied on the atr for the client_id and secret
1032                        info!(
1033                            "Invalid OAuth2 authentication - no secret in access token request - this can happen if you're expecting a public client and configured a basic one."
1034                        );
1035                        return Err(Oauth2Error::AuthenticationRequired);
1036                    }
1037                }
1038            }
1039            // Relies on the token to be valid - no further action needed.
1040            OauthRSType::Public { .. } => false,
1041        };
1042
1043        // We are authenticated! Yay! Now we can actually check things ...
1044        match &token_req.grant_type {
1045            GrantTypeReq::AuthorizationCode {
1046                code,
1047                redirect_uri,
1048                code_verifier,
1049            } => self.check_oauth2_token_exchange_authorization_code(
1050                &o2rs,
1051                code,
1052                redirect_uri,
1053                code_verifier.as_deref(),
1054                ct,
1055            ),
1056            GrantTypeReq::ClientCredentials { scope } => {
1057                if client_authentication_valid {
1058                    self.check_oauth2_token_client_credentials(&o2rs, scope.as_ref(), ct)
1059                } else {
1060                    security_info!(
1061                        "Unable to proceed with client credentials grant unless client authentication is provided and valid"
1062                    );
1063                    Err(Oauth2Error::AuthenticationRequired)
1064                }
1065            }
1066            GrantTypeReq::RefreshToken {
1067                refresh_token,
1068                scope,
1069            } => self.check_oauth2_token_refresh(&o2rs, refresh_token, scope.as_ref(), ct),
1070            GrantTypeReq::DeviceCode { device_code, scope } => {
1071                self.check_oauth2_device_code_status(device_code, scope)
1072            }
1073        }
1074    }
1075
1076    fn get_client(&self, client_id: &str) -> Result<Oauth2RS, Oauth2Error> {
1077        let s = self
1078            .oauth2rs
1079            .inner
1080            .rs_set_get(client_id)
1081            .ok_or_else(|| {
1082                warn!("Invalid OAuth2 client_id {}", client_id);
1083                Oauth2Error::AuthenticationRequired
1084            })?
1085            .clone();
1086        Ok(s)
1087    }
1088
1089    #[instrument(level = "info", skip(self))]
1090    pub fn handle_oauth2_start_device_flow(
1091        &mut self,
1092        _client_auth_info: ClientAuthInfo,
1093        _client_id: &str,
1094        _scope: &Option<BTreeSet<String>>,
1095        _eventid: Uuid,
1096    ) -> Result<DeviceAuthorizationResponse, Oauth2Error> {
1097        // let o2rs = self.get_client(client_id)?;
1098
1099        // info!("Got Client: {:?}", o2rs);
1100
1101        // // TODO: change this to checking if it's got device flow enabled
1102        // if !o2rs.require_pkce() {
1103        //     security_info!("Device flow is only available for PKCE-enabled clients");
1104        //     return Err(Oauth2Error::InvalidRequest);
1105        // }
1106
1107        // info!(
1108        //     "Starting device flow for client_id={} scopes={} source={:?}",
1109        //     client_id,
1110        //     scope
1111        //         .as_ref()
1112        //         .map(|s| s.iter().cloned().collect::<Vec<_>>().into_iter().join(","))
1113        //         .unwrap_or("[]".to_string()),
1114        //     client_auth_info.source
1115        // );
1116
1117        // let mut verification_uri = self.oauth2rs.inner.origin.clone();
1118        // verification_uri.set_path(uri::OAUTH2_DEVICE_LOGIN);
1119
1120        // let (user_code_string, _user_code) = gen_user_code();
1121        // let expiry =
1122        //     Duration::from_secs(OAUTH2_DEVICE_CODE_EXPIRY_SECONDS) + duration_from_epoch_now();
1123        // let device_code = gen_device_code()
1124        //     .inspect_err(|err| error!("Failed to generate a device code! {:?}", err))?;
1125
1126        Err(Oauth2Error::InvalidGrant)
1127
1128        // TODO: store user_code / expiry / client_id / device_code in the backend, needs to be checked on the token exchange.
1129        // Ok(DeviceAuthorizationResponse::new(
1130        //     verification_uri,
1131        //     device_code,
1132        //     user_code_string,
1133        // ))
1134    }
1135
1136    #[instrument(level = "info", skip(self))]
1137    fn check_oauth2_device_code_status(
1138        &mut self,
1139        device_code: &str,
1140        scope: &Option<BTreeSet<String>>,
1141    ) -> Result<AccessTokenResponse, Oauth2Error> {
1142        // TODO: check the device code is valid, do the needful
1143
1144        error!(
1145            "haven't done the device grant yet! Got device_code={} scope={:?}",
1146            device_code, scope
1147        );
1148        Err(Oauth2Error::AuthorizationPending)
1149
1150        // if it's an expired code, then just delete it from the db and return an error.
1151        // Err(Oauth2Error::ExpiredToken)
1152    }
1153
1154    #[instrument(level = "debug", skip_all)]
1155    pub fn check_oauth2_authorise_permit(
1156        &mut self,
1157        ident: &Identity,
1158        consent_token: &str,
1159        ct: Duration,
1160    ) -> Result<AuthorisePermitSuccess, OperationError> {
1161        let Some(account_uuid) = ident.get_uuid() else {
1162            error!("consent request ident does not have a valid uuid, unable to proceed");
1163            return Err(OperationError::InvalidSessionState);
1164        };
1165
1166        let consent_token_jwe = JweCompact::from_str(consent_token).map_err(|err| {
1167            error!(?err, "Consent token is not a valid jwe compact");
1168            OperationError::InvalidSessionState
1169        })?;
1170
1171        let consent_req: ConsentToken = self
1172            .oauth2rs
1173            .inner
1174            .consent_key
1175            .decipher(&consent_token_jwe)
1176            .map_err(|err| {
1177                error!(?err, "Failed to decrypt consent request");
1178                OperationError::CryptographyError
1179            })
1180            .and_then(|jwe| {
1181                jwe.from_json().map_err(|err| {
1182                    error!(?err, "Failed to deserialise consent request");
1183                    OperationError::SerdeJsonError
1184                })
1185            })?;
1186
1187        // Validate that the ident_id matches our current ident.
1188        if consent_req.ident_id != ident.get_event_origin_id() {
1189            security_info!("consent request ident id does not match the identity of our UAT.");
1190            return Err(OperationError::InvalidSessionState);
1191        }
1192
1193        // Validate that the session id matches our uat.
1194        if consent_req.session_id != ident.get_session_id() {
1195            security_info!("consent request session id does not match the session id of our UAT.");
1196            return Err(OperationError::InvalidSessionState);
1197        }
1198
1199        if consent_req.expiry <= ct.as_secs() {
1200            // Token is expired
1201            error!("Failed to decrypt consent request");
1202            return Err(OperationError::CryptographyError);
1203        }
1204
1205        // The exchange must be performed in the next 60 seconds.
1206        let expiry = ct.as_secs() + 60;
1207
1208        // Get the resource server config based on this client_id.
1209        let o2rs = self
1210            .oauth2rs
1211            .inner
1212            .rs_set_get(&consent_req.client_id)
1213            .ok_or_else(|| {
1214                admin_error!("Invalid consent request OAuth2 client_id");
1215                OperationError::InvalidRequestState
1216            })?;
1217
1218        // Extract the state, code challenge, redirect_uri
1219        let xchg_code = TokenExchangeCode {
1220            account_uuid,
1221            session_id: ident.get_session_id(),
1222            expiry,
1223            code_challenge: consent_req.code_challenge,
1224            redirect_uri: consent_req.redirect_uri.clone(),
1225            scopes: consent_req.scopes.clone(),
1226            nonce: consent_req.nonce,
1227        };
1228
1229        // Encrypt the exchange token
1230        let code_data_jwe = Jwe::into_json(&xchg_code).map_err(|err| {
1231            error!(?err, "Unable to encode xchg_code data");
1232            OperationError::SerdeJsonError
1233        })?;
1234
1235        let code = o2rs
1236            .key_object
1237            .jwe_a128gcm_encrypt(&code_data_jwe, ct)
1238            .map(|code| code.to_string())
1239            .map_err(|err| {
1240                error!(?err, "Unable to encrypt xchg_code");
1241                OperationError::CryptographyError
1242            })?;
1243
1244        // Everything is DONE! Now submit that it's all happy and the user consented correctly.
1245        // this will let them bypass consent steps in the future.
1246        // Submit that we consented to the delayed action queue
1247
1248        let modlist = ModifyList::new_list(vec![
1249            Modify::Removed(
1250                Attribute::OAuth2ConsentScopeMap,
1251                PartialValue::Refer(o2rs.uuid),
1252            ),
1253            Modify::Present(
1254                Attribute::OAuth2ConsentScopeMap,
1255                Value::OauthScopeMap(o2rs.uuid, consent_req.scopes.iter().cloned().collect()),
1256            ),
1257        ]);
1258
1259        self.qs_write.internal_modify(
1260            &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(account_uuid))),
1261            &modlist,
1262        )?;
1263
1264        Ok(AuthorisePermitSuccess {
1265            redirect_uri: consent_req.redirect_uri,
1266            state: consent_req.state,
1267            code,
1268            response_mode: consent_req.response_mode,
1269        })
1270    }
1271
1272    #[instrument(level = "debug", skip_all)]
1273    fn check_oauth2_token_exchange_authorization_code(
1274        &mut self,
1275        o2rs: &Oauth2RS,
1276        token_req_code: &str,
1277        token_req_redirect_uri: &Url,
1278        token_req_code_verifier: Option<&str>,
1279        ct: Duration,
1280    ) -> Result<AccessTokenResponse, Oauth2Error> {
1281        // Check the token_req is within the valid time, and correctly signed for
1282        // this client.
1283        let jwe_compact = JweCompact::from_str(token_req_code).map_err(|_| {
1284            error!("Failed to deserialise a valid JWE");
1285            Oauth2Error::InvalidRequest
1286        })?;
1287
1288        let code_xchg: TokenExchangeCode = o2rs
1289            .key_object
1290            .jwe_decrypt(&jwe_compact)
1291            .map_err(|_| {
1292                admin_error!("Failed to decrypt token exchange request");
1293                Oauth2Error::InvalidRequest
1294            })
1295            .and_then(|jwe| {
1296                debug!(?jwe);
1297                jwe.from_json::<TokenExchangeCode>().map_err(|err| {
1298                    error!(?err, "Failed to deserialise token exchange code");
1299                    Oauth2Error::InvalidRequest
1300                })
1301            })?;
1302
1303        if code_xchg.expiry <= ct.as_secs() {
1304            error!("Expired token exchange request");
1305            return Err(Oauth2Error::InvalidRequest);
1306        }
1307
1308        // If we have a verifier present, we MUST assert that a code challenge is present!
1309        // It is worth noting here that code_xchg is *server issued* and encrypted, with
1310        // a short validity period. The client controlled value is in token_req.code_verifier
1311        if let Some(code_challenge) = code_xchg.code_challenge {
1312            // Validate the code_verifier
1313            let code_verifier = token_req_code_verifier
1314                    .ok_or_else(|| {
1315                        security_info!("PKCE code verification failed - code challenge is present, but no verifier was provided");
1316                        Oauth2Error::InvalidRequest
1317                    })?;
1318            let mut hasher = sha::Sha256::new();
1319            hasher.update(code_verifier.as_bytes());
1320            let code_verifier_hash: Vec<u8> = hasher.finish().to_vec();
1321
1322            if code_challenge != code_verifier_hash {
1323                security_info!(
1324                    "PKCE code verification failed - this may indicate malicious activity"
1325                );
1326                return Err(Oauth2Error::InvalidRequest);
1327            }
1328        } else if o2rs.require_pkce() {
1329            security_info!(
1330                "PKCE code verification failed - no code challenge present in PKCE enforced mode"
1331            );
1332            return Err(Oauth2Error::InvalidRequest);
1333        } else if token_req_code_verifier.is_some() {
1334            security_info!(
1335                "PKCE code verification failed - a code verifier is present, but no code challenge in exchange"
1336            );
1337            return Err(Oauth2Error::InvalidRequest);
1338        }
1339
1340        // Validate the redirect_uri is the same as the original.
1341        if token_req_redirect_uri != &code_xchg.redirect_uri {
1342            security_info!("Invalid OAuth2 redirect_uri (differs from original request uri)");
1343            return Err(Oauth2Error::InvalidOrigin);
1344        }
1345
1346        /*
1347        // Check that the UAT we are issuing for still is valid.
1348        //
1349        // Not sure this is actually needed. To create the token exchange code you need to have
1350        // a valid, non-expired session, so why do we double check this here?
1351        let odt_ct = OffsetDateTime::UNIX_EPOCH + ct;
1352        if let Some(expiry) = code_xchg.uat.expiry {
1353            if expiry <= odt_ct {
1354                security_info!(
1355                    "User Auth Token has expired before we could publish the OAuth2 response"
1356                );
1357                return Err(Oauth2Error::AccessDenied);
1358            }
1359        }
1360        */
1361
1362        // ==== We are now GOOD TO GO! ====
1363        // Grant the access token response.
1364        let parent_session_id = code_xchg.session_id;
1365        let session_id = Uuid::new_v4();
1366
1367        let scopes = code_xchg.scopes;
1368        let account_uuid = code_xchg.account_uuid;
1369        let nonce = code_xchg.nonce;
1370
1371        self.generate_access_token_response(
1372            o2rs,
1373            ct,
1374            scopes,
1375            account_uuid,
1376            parent_session_id,
1377            session_id,
1378            nonce,
1379        )
1380    }
1381
1382    #[instrument(level = "debug", skip_all)]
1383    fn check_oauth2_token_refresh(
1384        &mut self,
1385        o2rs: &Oauth2RS,
1386        refresh_token: &str,
1387        req_scopes: Option<&BTreeSet<String>>,
1388        ct: Duration,
1389    ) -> Result<AccessTokenResponse, Oauth2Error> {
1390        let jwe_compact = JweCompact::from_str(refresh_token).map_err(|_| {
1391            error!("Failed to deserialise a valid JWE");
1392            Oauth2Error::InvalidRequest
1393        })?;
1394
1395        // Validate the refresh token decrypts and it's expiry is within the valid window.
1396        let token: Oauth2TokenType = o2rs
1397            .key_object
1398            .jwe_decrypt(&jwe_compact)
1399            .map_err(|_| {
1400                admin_error!("Failed to decrypt refresh token request");
1401                Oauth2Error::InvalidRequest
1402            })
1403            .and_then(|jwe| {
1404                jwe.from_json().map_err(|err| {
1405                    error!(?err, "Failed to deserialise token");
1406                    Oauth2Error::InvalidRequest
1407                })
1408            })?;
1409
1410        match token {
1411            // Oauth2TokenType::Access { .. } |
1412            Oauth2TokenType::ClientAccess { .. } => {
1413                admin_error!("attempt to refresh with an access token");
1414                Err(Oauth2Error::InvalidRequest)
1415            }
1416            Oauth2TokenType::Refresh {
1417                scopes,
1418                parent_session_id,
1419                session_id,
1420                exp,
1421                uuid,
1422                iat,
1423                nbf: _,
1424                nonce,
1425            } => {
1426                if exp <= ct.as_secs() as i64 {
1427                    security_info!(?uuid, "refresh token has expired, ");
1428                    return Err(Oauth2Error::InvalidGrant);
1429                }
1430
1431                // Check the session is still valid. This call checks the parent session
1432                // and the OAuth2 session.
1433                let valid = self
1434                    .check_oauth2_account_uuid_valid(
1435                        uuid,
1436                        session_id,
1437                        Some(parent_session_id),
1438                        iat,
1439                        ct,
1440                    )
1441                    .map_err(|_| admin_error!("Account is not valid"));
1442
1443                let Ok(Some(entry)) = valid else {
1444                    security_info!(
1445                        ?uuid,
1446                        "access token has no account not valid, returning inactive"
1447                    );
1448                    return Err(Oauth2Error::InvalidGrant);
1449                };
1450
1451                // Check the not issued before of the session relative to this refresh iat
1452                let oauth2_session = entry
1453                    .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
1454                    .and_then(|map| map.get(&session_id))
1455                    .ok_or_else(|| {
1456                        security_info!(
1457                            ?session_id,
1458                            "No OAuth2 session found, unable to proceed with refresh"
1459                        );
1460                        Oauth2Error::InvalidGrant
1461                    })?;
1462
1463                // If the refresh token was issued previous to the time listed in our oauth2_session
1464                // this indicates session desync / replay. We must nuke the session at this point.
1465                //
1466                // Need to think about how to handle this nicely give transactions.
1467                if iat < oauth2_session.issued_at.unix_timestamp() {
1468                    security_info!(
1469                        ?session_id,
1470                        "Attempt to reuse a refresh token detected, destroying session"
1471                    );
1472
1473                    // Revoke it
1474                    let modlist = ModifyList::new_list(vec![Modify::Removed(
1475                        Attribute::OAuth2Session,
1476                        PartialValue::Refer(session_id),
1477                    )]);
1478
1479                    self.qs_write
1480                        .internal_modify(
1481                            &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(uuid))),
1482                            &modlist,
1483                        )
1484                        .map_err(|e| {
1485                            admin_error!("Failed to modify - revoke OAuth2 session {:?}", e);
1486                            Oauth2Error::ServerError(e)
1487                        })?;
1488
1489                    return Err(Oauth2Error::InvalidGrant);
1490                }
1491
1492                // Check the scopes are equal or subset, OR none.
1493                let update_scopes = if let Some(req_scopes) = req_scopes {
1494                    if req_scopes.is_subset(&scopes) {
1495                        debug!("oauth2 scopes requested, checked as valid.");
1496                        // We have to return the requested set since it
1497                        // may be constrained.
1498                        req_scopes.clone()
1499                    } else {
1500                        warn!("oauth2 scopes requested, invalid.");
1501                        return Err(Oauth2Error::InvalidScope);
1502                    }
1503                } else {
1504                    debug!("No OAuth2 scopes requested, this is valid.");
1505                    // Return the initial set of scopes.
1506                    scopes
1507                };
1508
1509                // ----------
1510                // good to go
1511
1512                let account_uuid = uuid;
1513
1514                self.generate_access_token_response(
1515                    o2rs,
1516                    ct,
1517                    update_scopes,
1518                    account_uuid,
1519                    parent_session_id,
1520                    session_id,
1521                    nonce,
1522                )
1523            }
1524        }
1525    }
1526
1527    #[instrument(level = "debug", skip_all)]
1528    fn check_oauth2_token_client_credentials(
1529        &mut self,
1530        o2rs: &Oauth2RS,
1531        req_scopes: Option<&BTreeSet<String>>,
1532        ct: Duration,
1533    ) -> Result<AccessTokenResponse, Oauth2Error> {
1534        let req_scopes = req_scopes.cloned().unwrap_or_default();
1535
1536        // Validate all request scopes have valid syntax.
1537        validate_scopes(&req_scopes)?;
1538
1539        // Of these scopes, which do we have available?
1540        let avail_scopes: Vec<String> = req_scopes
1541            .intersection(&o2rs.client_scopes)
1542            .map(|s| s.to_string())
1543            .collect();
1544
1545        if avail_scopes.len() != req_scopes.len() {
1546            admin_warn!(
1547                ident = %o2rs.name,
1548                requested_scopes = ?req_scopes,
1549                available_scopes = ?o2rs.client_scopes,
1550                "Client does not have access to the requested scopes"
1551            );
1552            return Err(Oauth2Error::AccessDenied);
1553        }
1554
1555        // == ready to build the access token ==
1556
1557        let granted_scopes = avail_scopes
1558            .into_iter()
1559            .chain(o2rs.client_sup_scopes.iter().cloned())
1560            .collect::<BTreeSet<_>>();
1561
1562        let odt_ct = OffsetDateTime::UNIX_EPOCH + ct;
1563        let iat = ct.as_secs() as i64;
1564        let exp = iat + OAUTH2_ACCESS_TOKEN_EXPIRY as i64;
1565        let odt_exp = odt_ct + Duration::from_secs(OAUTH2_ACCESS_TOKEN_EXPIRY as u64);
1566        let expires_in = OAUTH2_ACCESS_TOKEN_EXPIRY;
1567
1568        let session_id = Uuid::new_v4();
1569
1570        let scope = granted_scopes.clone();
1571
1572        let uuid = o2rs.uuid;
1573
1574        let access_token_raw = Oauth2TokenType::ClientAccess {
1575            scopes: granted_scopes,
1576            session_id,
1577            uuid,
1578            exp,
1579            iat,
1580            nbf: iat,
1581        };
1582
1583        let access_token_data = Jwe::into_json(&access_token_raw).map_err(|err| {
1584            error!(?err, "Unable to encode token data");
1585            Oauth2Error::ServerError(OperationError::SerdeJsonError)
1586        })?;
1587
1588        let access_token = o2rs
1589            .key_object
1590            .jwe_a128gcm_encrypt(&access_token_data, ct)
1591            .map(|jwe| jwe.to_string())
1592            .map_err(|err| {
1593                error!(?err, "Unable to encode token data");
1594                Oauth2Error::ServerError(OperationError::CryptographyError)
1595            })?;
1596
1597        // Write the session to the db
1598        let session = Value::Oauth2Session(
1599            session_id,
1600            Oauth2Session {
1601                parent: None,
1602                state: SessionState::ExpiresAt(odt_exp),
1603                issued_at: odt_ct,
1604                rs_uuid: o2rs.uuid,
1605            },
1606        );
1607
1608        // We need to create this session on the o2rs
1609        let modlist =
1610            ModifyList::new_list(vec![Modify::Present(Attribute::OAuth2Session, session)]);
1611
1612        self.qs_write
1613            .internal_modify(
1614                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(uuid))),
1615                &modlist,
1616            )
1617            .map_err(|e| {
1618                admin_error!("Failed to persist OAuth2 session record {:?}", e);
1619                Oauth2Error::ServerError(e)
1620            })?;
1621
1622        Ok(AccessTokenResponse {
1623            access_token,
1624            token_type: AccessTokenType::Bearer,
1625            expires_in,
1626            refresh_token: None,
1627            scope,
1628            id_token: None,
1629        })
1630    }
1631
1632    fn generate_access_token_response(
1633        &mut self,
1634        o2rs: &Oauth2RS,
1635        ct: Duration,
1636        //
1637        scopes: BTreeSet<String>,
1638        account_uuid: Uuid,
1639        parent_session_id: Uuid,
1640        session_id: Uuid,
1641        nonce: Option<String>,
1642    ) -> Result<AccessTokenResponse, Oauth2Error> {
1643        let odt_ct = OffsetDateTime::UNIX_EPOCH + ct;
1644        let iat = ct.as_secs() as i64;
1645
1646        // TODO: Configurable from the oauth2rs configuration?
1647
1648        // Disclaimer: It may seem odd here that we are ignoring our session when it comes to expiry
1649        // times. However, this actually is valid because when we take the initial code exchange
1650        // path we have already validate the expiry of the account/session in that process. For the
1651        // refresh path, we validate the session expiry and validity before we call this. So these
1652        // expiries are *purely* for the tokens we issue and are *not related* to the expiries of the
1653        // the session - these are enforced as above!
1654
1655        let expiry = odt_ct + Duration::from_secs(OAUTH2_ACCESS_TOKEN_EXPIRY as u64);
1656        let expires_in = OAUTH2_ACCESS_TOKEN_EXPIRY;
1657        let refresh_expiry = iat + OAUTH_REFRESH_TOKEN_EXPIRY as i64;
1658        let odt_refresh_expiry = odt_ct + Duration::from_secs(OAUTH_REFRESH_TOKEN_EXPIRY);
1659
1660        let scope = scopes.clone();
1661
1662        let iss = o2rs.iss.clone();
1663
1664        // Just reflect the access token expiry.
1665        let exp = expiry.unix_timestamp();
1666
1667        let aud = o2rs.name.clone();
1668
1669        let client_id = o2rs.name.clone();
1670
1671        let id_token = if scopes.contains(OAUTH2_SCOPE_OPENID) {
1672            // TODO: Scopes map to claims:
1673            //
1674            // * profile - (name, family\_name, given\_name, middle\_name, nickname, preferred\_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated\_at)
1675            // * email - (email, email\_verified)
1676            // * address - (address)
1677            // * phone - (phone\_number, phone\_number\_verified)
1678            //
1679            // https://openid.net/specs/openid-connect-basic-1_0.html#StandardClaims
1680
1681            // TODO: Can the user consent to which claims are released? Today as we don't support most
1682            // of them anyway, no, but in the future, we can stash these to the consent req.
1683
1684            // TODO: If max_age was requested in the request, we MUST provide auth_time.
1685
1686            // amr == auth method
1687            // We removed this from uat, and I think that it's okay here. AMR is a bit useless anyway
1688            // since there is no standard for what it should look like wrt to cred strength.
1689            let amr = None;
1690
1691            let entry = match self.qs_write.internal_search_uuid(account_uuid) {
1692                Ok(entry) => entry,
1693                Err(err) => return Err(Oauth2Error::ServerError(err)),
1694            };
1695
1696            let account = match Account::try_from_entry_rw(&entry, &mut self.qs_write) {
1697                Ok(account) => account,
1698                Err(err) => return Err(Oauth2Error::ServerError(err)),
1699            };
1700
1701            let s_claims = s_claims_for_account(o2rs, &account, &scopes);
1702            let extra_claims = extra_claims_for_account(&account, &o2rs.claim_map, &scopes);
1703
1704            let oidc = OidcToken {
1705                iss: iss.clone(),
1706                sub: OidcSubject::U(account_uuid),
1707                aud: aud.clone(),
1708                iat,
1709                nbf: Some(iat),
1710                exp,
1711                auth_time: None,
1712                nonce: nonce.clone(),
1713                at_hash: None,
1714                acr: None,
1715                amr,
1716                azp: Some(o2rs.name.clone()),
1717                jti: Some(session_id.to_string()),
1718                s_claims,
1719                claims: extra_claims,
1720            };
1721
1722            trace!(?oidc);
1723            let oidc = JwsBuilder::into_json(&oidc)
1724                .map(|builder| builder.build())
1725                .map_err(|err| {
1726                    admin_error!(?err, "Unable to encode access token data");
1727                    Oauth2Error::ServerError(OperationError::InvalidState)
1728                })?;
1729
1730            let jwt_signed = match o2rs.sign_alg {
1731                SignatureAlgo::Es256 => o2rs.key_object.jws_es256_sign(&oidc, ct),
1732                SignatureAlgo::Rs256 => o2rs.key_object.jws_rs256_sign(&oidc, ct),
1733            }
1734            .map_err(|err| {
1735                error!(?err, "Unable to encode oidc token data");
1736                Oauth2Error::ServerError(OperationError::InvalidState)
1737            })?;
1738
1739            Some(jwt_signed.to_string())
1740        } else {
1741            // id_token is not required in non-openid flows.
1742            None
1743        };
1744
1745        // We need to record this into the record? Delayed action?
1746        let access_token_data = OAuth2RFC9068Token {
1747            iss: iss.to_string(),
1748            sub: account_uuid,
1749            aud,
1750            exp,
1751            nbf: iat,
1752            iat,
1753            jti: session_id,
1754            client_id,
1755            extensions: OAuth2RFC9068TokenExtensions {
1756                auth_time: None,
1757                acr: None,
1758                amr: None,
1759                scope: scopes.clone(),
1760                nonce: nonce.clone(),
1761                session_id,
1762                parent_session_id: Some(parent_session_id),
1763            },
1764        };
1765
1766        let access_token_data = JwsBuilder::into_json(&access_token_data)
1767            .map(|builder| builder.set_typ(Some("at+jwt")).build())
1768            .map_err(|err| {
1769                error!(?err, "Unable to encode access token data");
1770                Oauth2Error::ServerError(OperationError::InvalidState)
1771            })?;
1772
1773        let access_token = match o2rs.sign_alg {
1774            SignatureAlgo::Es256 => o2rs.key_object.jws_es256_sign(&access_token_data, ct),
1775            SignatureAlgo::Rs256 => o2rs.key_object.jws_rs256_sign(&access_token_data, ct),
1776        }
1777        .map_err(|e| {
1778            admin_error!(err = ?e, "Unable to sign access token data");
1779            Oauth2Error::ServerError(OperationError::InvalidState)
1780        })?;
1781
1782        let refresh_token_raw = Oauth2TokenType::Refresh {
1783            scopes,
1784            parent_session_id,
1785            session_id,
1786            exp: refresh_expiry,
1787            uuid: account_uuid,
1788            iat,
1789            nbf: iat,
1790            nonce,
1791        };
1792
1793        let refresh_token_data = Jwe::into_json(&refresh_token_raw).map_err(|err| {
1794            error!(?err, "Unable to encode token data");
1795            Oauth2Error::ServerError(OperationError::SerdeJsonError)
1796        })?;
1797
1798        let refresh_token = o2rs
1799            .key_object
1800            .jwe_a128gcm_encrypt(&refresh_token_data, ct)
1801            .map(|jwe| jwe.to_string())
1802            .map_err(|err| {
1803                error!(?err, "Unable to encrypt token data");
1804                Oauth2Error::ServerError(OperationError::CryptographyError)
1805            })?;
1806
1807        // Write the session to the db even with the refresh path, we need to do
1808        // this to update the "not issued before" time.
1809        let session = Value::Oauth2Session(
1810            session_id,
1811            Oauth2Session {
1812                parent: Some(parent_session_id),
1813                state: SessionState::ExpiresAt(odt_refresh_expiry),
1814                issued_at: odt_ct,
1815                rs_uuid: o2rs.uuid,
1816            },
1817        );
1818
1819        // We need to update (replace) this session id if present.
1820        let modlist = ModifyList::new_list(vec![
1821            // NOTE: Oauth2_session has special handling that allows update in place without
1822            // the remove step needing to be carried out.
1823            // Modify::Removed("oauth2_session".into(), PartialValue::Refer(session_id)),
1824            Modify::Present(Attribute::OAuth2Session, session),
1825        ]);
1826
1827        self.qs_write
1828            .internal_modify(
1829                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(account_uuid))),
1830                &modlist,
1831            )
1832            .map_err(|e| {
1833                admin_error!("Failed to persist OAuth2 session record {:?}", e);
1834                Oauth2Error::ServerError(e)
1835            })?;
1836
1837        Ok(AccessTokenResponse {
1838            access_token: access_token.to_string(),
1839            token_type: AccessTokenType::Bearer,
1840            expires_in,
1841            refresh_token: Some(refresh_token),
1842            scope,
1843            id_token,
1844        })
1845    }
1846
1847    #[cfg(test)]
1848    fn reflect_oauth2_token(
1849        &mut self,
1850        client_auth_info: &ClientAuthInfo,
1851        token: &str,
1852    ) -> Result<Oauth2TokenType, OperationError> {
1853        let Some(client_authz) = client_auth_info.basic_authz.as_ref() else {
1854            warn!("OAuth2 client_id not provided by basic authz");
1855            return Err(OperationError::InvalidSessionState);
1856        };
1857
1858        let client_auth = parse_basic_authz(client_authz.as_str()).map_err(|_| {
1859            warn!("Invalid client_authz base64");
1860            OperationError::InvalidSessionState
1861        })?;
1862
1863        // Get the o2rs for the handle.
1864        let o2rs = self
1865            .oauth2rs
1866            .inner
1867            .rs_set_get(&client_auth.client_id)
1868            .ok_or_else(|| {
1869                warn!("Invalid OAuth2 client_id");
1870                OperationError::InvalidSessionState
1871            })?;
1872
1873        // check the secret.
1874        if let OauthRSType::Basic { authz_secret, .. } = &o2rs.type_ {
1875            if o2rs.is_basic() && Some(authz_secret) != client_auth.client_secret.as_ref() {
1876                info!(
1877                    "Invalid OAuth2 secret for client_id={}",
1878                    client_auth.client_id
1879                );
1880                return Err(OperationError::InvalidSessionState);
1881            }
1882        }
1883
1884        let jwe_compact = JweCompact::from_str(token).map_err(|err| {
1885            error!(?err, "Failed to deserialise a valid JWE");
1886            OperationError::InvalidSessionState
1887        })?;
1888
1889        o2rs.key_object
1890            .jwe_decrypt(&jwe_compact)
1891            .map_err(|err| {
1892                error!(?err, "Failed to decrypt token reflection request");
1893                OperationError::CryptographyError
1894            })
1895            .and_then(|jwe| {
1896                jwe.from_json().map_err(|err| {
1897                    error!(?err, "Failed to deserialise token for reflection");
1898                    OperationError::SerdeJsonError
1899                })
1900            })
1901    }
1902}
1903
1904impl IdmServerProxyReadTransaction<'_> {
1905    #[instrument(level = "debug", skip_all)]
1906    pub fn check_oauth2_authorisation(
1907        &self,
1908        maybe_ident: Option<&Identity>,
1909        auth_req: &AuthorisationRequest,
1910        ct: Duration,
1911    ) -> Result<AuthoriseResponse, Oauth2Error> {
1912        // due to identity processing we already know that:
1913        // * the session must be authenticated, and valid
1914        // * is within it's valid time window.
1915        trace!(?auth_req);
1916
1917        if auth_req.response_type != ResponseType::Code {
1918            admin_warn!("Unsupported OAuth2 response_type (should be 'code')");
1919            return Err(Oauth2Error::UnsupportedResponseType);
1920        }
1921
1922        let Some(response_mode) = auth_req.get_response_mode() else {
1923            warn!(
1924                "Invalid response_mode {:?} for response_type {:?}",
1925                auth_req.response_mode, auth_req.response_type
1926            );
1927            return Err(Oauth2Error::InvalidRequest);
1928        };
1929
1930        let response_mode = match response_mode {
1931            ResponseMode::Query => SupportedResponseMode::Query,
1932            ResponseMode::Fragment => SupportedResponseMode::Fragment,
1933            ResponseMode::FormPost => {
1934                warn!(
1935                    "Invalid response mode form_post requested - many clients request this incorrectly but proceed with response_mode=query. Remapping to query."
1936                );
1937                warn!("This behaviour WILL BE REMOVED in a future release.");
1938                SupportedResponseMode::Query
1939            }
1940            ResponseMode::Invalid => {
1941                warn!("Invalid response mode requested, unable to proceed");
1942                return Err(Oauth2Error::InvalidRequest);
1943            }
1944        };
1945
1946        /*
1947         * 4.1.2.1.  Error Response
1948         *
1949         * If the request fails due to a missing, invalid, or mismatching
1950         * redirection URI, or if the client identifier is missing or invalid,
1951         * the authorization server SHOULD inform the resource owner of the
1952         * error and MUST NOT automatically redirect the user-agent to the
1953         * invalid redirection URI.
1954         */
1955
1956        //
1957        let o2rs = self
1958            .oauth2rs
1959            .inner
1960            .rs_set_get(&auth_req.client_id)
1961            .ok_or_else(|| {
1962                warn!(
1963                    "Invalid OAuth2 client_id ({}) Have you configured the OAuth2 resource server?",
1964                    &auth_req.client_id
1965                );
1966                Oauth2Error::InvalidClientId
1967            })?;
1968
1969        // redirect_uri must be part of the client_id origins, unless the client is public and then it MAY
1970        // be a loopback address exempting it from this check and enforcement and we can carry on safely.
1971        if o2rs.type_.allow_localhost_redirect() && check_is_loopback(&auth_req.redirect_uri) {
1972            debug!("Loopback redirect_uri detected, allowing for localhost");
1973        } else {
1974            // The legacy origin match is in use.
1975            let origin_uri_matched =
1976                !o2rs.strict_redirect_uri && o2rs.origins.contains(&auth_req.redirect_uri.origin());
1977            // Strict uri validation is in use.
1978            let strict_redirect_uri_matched =
1979                o2rs.strict_redirect_uri && o2rs.redirect_uris.contains(&auth_req.redirect_uri);
1980            // Allow opaque origins such as app uris.
1981            let opaque_origin_matched = o2rs.opaque_origins.contains(&auth_req.redirect_uri);
1982
1983            // At least one of these conditions must hold true to proceed.
1984            if !(strict_redirect_uri_matched || origin_uri_matched || opaque_origin_matched) {
1985                if o2rs.strict_redirect_uri {
1986                    warn!(
1987                                "Invalid OAuth2 redirect_uri (must be an exact match to a redirect-url) - got {}",
1988                                auth_req.redirect_uri.as_str()
1989                            );
1990                } else {
1991                    warn!(
1992                        "Invalid OAuth2 redirect_uri (must be related to origin) - got {:?}",
1993                        auth_req.redirect_uri.origin()
1994                    );
1995                }
1996                return Err(Oauth2Error::InvalidOrigin);
1997            }
1998            // We have to specifically match on http here because non-http origins may be exempt from this
1999            // enforcement.
2000            if (o2rs.origin_https_required && auth_req.redirect_uri.scheme() != "https")
2001                && !opaque_origin_matched
2002            {
2003                admin_warn!(
2004                    "Invalid OAuth2 redirect_uri scheme (must be https for secure origin) - got {}",
2005                    auth_req.redirect_uri.to_string()
2006                );
2007                return Err(Oauth2Error::InvalidOrigin);
2008            }
2009        }
2010
2011        let code_challenge = if let Some(pkce_request) = &auth_req.pkce_request {
2012            if !o2rs.require_pkce() {
2013                security_info!(?o2rs.name, "Insecure OAuth2 client configuration - PKCE is not enforced, but client is requesting it!");
2014            }
2015            // CodeChallengeMethod must be S256
2016            if pkce_request.code_challenge_method != CodeChallengeMethod::S256 {
2017                admin_warn!("Invalid OAuth2 code_challenge_method (must be 'S256')");
2018                return Err(Oauth2Error::InvalidRequest);
2019            }
2020            Some(pkce_request.code_challenge.clone())
2021        } else if o2rs.require_pkce() {
2022            security_error!(?o2rs.name, "No PKCE code challenge was provided with client in enforced PKCE mode.");
2023            return Err(Oauth2Error::InvalidRequest);
2024        } else {
2025            security_info!(?o2rs.name, "Insecure client configuration - PKCE is not enforced.");
2026            None
2027        };
2028
2029        // =============================================================================
2030        // By this point, we have validated the majority of the security related
2031        // parameters of the request. We can now inspect the identity and decide
2032        // if we should ask the user to re-authenticate and proceed.
2033
2034        // TODO: https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters
2035        // Are we going to provide the functions for these? Most of these can be "later".
2036        // IF CHANGED: Update OidcDiscoveryResponse!!!
2037
2038        // TODO: https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters
2039        // prompt - if set to login, we need to force a re-auth. But we don't want to
2040        // if the user "only just" logged in, that's annoying. So we need a time window for
2041        // this, to detect when we should force it to the consent req.
2042
2043        // TODO: display = popup vs touch vs wap etc.
2044
2045        // TODO: max_age, pass through with consent req. If 0, force login.
2046        // Otherwise force a login re the uat timeout.
2047
2048        // TODO: ui_locales / claims_locales for the ui. Only if we don't have a Uat that
2049        // would provide this.
2050
2051        // TODO: id_token_hint - a past token which can be used as a hint.
2052
2053        let Some(ident) = maybe_ident else {
2054            debug!("No identity available, assume authentication required");
2055            return Ok(AuthoriseResponse::AuthenticationRequired {
2056                client_name: o2rs.displayname.clone(),
2057                login_hint: auth_req.oidc_ext.login_hint.clone(),
2058            });
2059        };
2060
2061        let Some(account_uuid) = ident.get_uuid() else {
2062            error!("Consent request ident does not have a valid UUID, unable to proceed");
2063            return Err(Oauth2Error::InvalidRequest);
2064        };
2065
2066        // Deny anonymous access to oauth2
2067        if account_uuid == UUID_ANONYMOUS {
2068            admin_error!(
2069                "Invalid OAuth2 request - refusing to allow user that authenticated with anonymous"
2070            );
2071            return Err(Oauth2Error::AccessDenied);
2072        }
2073
2074        // scopes - you need to have every requested scope or this auth_req is denied.
2075        let req_scopes: BTreeSet<String> = auth_req.scope.clone();
2076
2077        if req_scopes.is_empty() {
2078            admin_error!("Invalid OAuth2 request - must contain at least one requested scope");
2079            return Err(Oauth2Error::InvalidRequest);
2080        }
2081
2082        // Validate all request scopes have valid syntax.
2083        validate_scopes(&req_scopes)?;
2084
2085        let uat_scopes: BTreeSet<String> = o2rs
2086            .scope_maps
2087            .iter()
2088            .filter_map(|(u, m)| {
2089                if ident.is_memberof(*u) {
2090                    Some(m.iter())
2091                } else {
2092                    None
2093                }
2094            })
2095            .flatten()
2096            .cloned()
2097            .collect();
2098
2099        // Needs to use s.to_string due to &&str which can't use the str::to_string
2100        let avail_scopes: Vec<String> = req_scopes
2101            .intersection(&uat_scopes)
2102            .map(|s| s.to_string())
2103            .collect();
2104
2105        debug!(?o2rs.scope_maps);
2106
2107        // Due to the intersection above, this is correct because the equal len can only
2108        // occur if all terms were satisfied - effectively this check is that avail_scopes
2109        // and req_scopes are identical after intersection with the scopes defined by uat_scopes
2110        if avail_scopes.len() != req_scopes.len() {
2111            admin_warn!(
2112                %ident,
2113                requested_scopes = ?req_scopes,
2114                available_scopes = ?uat_scopes,
2115                "Identity does not have access to the requested scopes"
2116            );
2117            return Err(Oauth2Error::AccessDenied);
2118        }
2119
2120        drop(avail_scopes);
2121
2122        // ⚠️  At this point, per scopes we are *authorised*
2123
2124        // We now access the supplemental scopes that will be granted to this session. It is important
2125        // we DO NOT do this prior to the requested scope check, just in case we accidentally
2126        // confuse the two!
2127
2128        // The set of scopes that are being granted during this auth_request. This is a combination
2129        // of the scopes that were requested, and the scopes we supplement.
2130
2131        // MICRO OPTIMISATION = flag if we have openid first, so we can into_iter here rather than
2132        // cloning.
2133        let openid_requested = req_scopes.contains(OAUTH2_SCOPE_OPENID);
2134
2135        let granted_scopes: BTreeSet<String> = o2rs
2136            .sup_scope_maps
2137            .iter()
2138            .filter_map(|(u, m)| {
2139                if ident.is_memberof(*u) {
2140                    Some(m.iter())
2141                } else {
2142                    None
2143                }
2144            })
2145            .flatten()
2146            .cloned()
2147            .chain(req_scopes)
2148            .collect();
2149
2150        let consent_previously_granted =
2151            if let Some(consent_scopes) = ident.get_oauth2_consent_scopes(o2rs.uuid) {
2152                trace!(?granted_scopes);
2153                trace!(?consent_scopes);
2154                granted_scopes.eq(consent_scopes)
2155            } else {
2156                false
2157            };
2158
2159        let session_id = ident.get_session_id();
2160
2161        if consent_previously_granted {
2162            if event_enabled!(tracing::Level::DEBUG) {
2163                let pretty_scopes: Vec<String> =
2164                    granted_scopes.iter().map(|s| s.to_owned()).collect();
2165                debug!(
2166                    "User has previously consented, permitting with scopes: {}",
2167                    pretty_scopes.join(",")
2168                );
2169            }
2170
2171            // Xchg token expires in
2172            let expiry = ct.as_secs() + 60;
2173
2174            // Setup for the permit success
2175            let xchg_code = TokenExchangeCode {
2176                account_uuid,
2177                session_id,
2178                expiry,
2179                code_challenge,
2180                redirect_uri: auth_req.redirect_uri.clone(),
2181                scopes: granted_scopes.into_iter().collect(),
2182                nonce: auth_req.nonce.clone(),
2183            };
2184
2185            // Encrypt the exchange token with the key of the client
2186            let code_data_jwe = Jwe::into_json(&xchg_code).map_err(|err| {
2187                error!(?err, "Unable to encode xchg_code data");
2188                Oauth2Error::ServerError(OperationError::SerdeJsonError)
2189            })?;
2190
2191            let code = o2rs
2192                .key_object
2193                .jwe_a128gcm_encrypt(&code_data_jwe, ct)
2194                .map(|jwe| jwe.to_string())
2195                .map_err(|err| {
2196                    error!(?err, "Unable to encrypt xchg_code data");
2197                    Oauth2Error::ServerError(OperationError::CryptographyError)
2198                })?;
2199
2200            Ok(AuthoriseResponse::Permitted(AuthorisePermitSuccess {
2201                redirect_uri: auth_req.redirect_uri.clone(),
2202                state: auth_req.state.clone(),
2203                code,
2204                response_mode,
2205            }))
2206        } else {
2207            //  Check that the scopes are the same as a previous consent (if any)
2208            // If oidc, what PII is visible?
2209            // TODO: Scopes map to claims:
2210            //
2211            // * profile - (name, family\_name, given\_name, middle\_name, nickname, preferred\_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated\_at)
2212            // * email - (email, email\_verified)
2213            // * address - (address)
2214            // * phone - (phone\_number, phone\_number\_verified)
2215            //
2216            // https://openid.net/specs/openid-connect-basic-1_0.html#StandardClaims
2217
2218            // IMPORTANT DISTINCTION - Here req scopes must contain openid, but the PII can be supplemented
2219            // be the servers scopes!
2220            let mut pii_scopes = BTreeSet::default();
2221            if openid_requested {
2222                // Only mutate if things were requested under openid
2223                if granted_scopes.contains(OAUTH2_SCOPE_EMAIL) {
2224                    pii_scopes.insert(OAUTH2_SCOPE_EMAIL.to_string());
2225                    pii_scopes.insert("email_verified".to_string());
2226                }
2227            };
2228
2229            if granted_scopes.contains(OAUTH2_SCOPE_SSH_PUBLICKEYS) {
2230                pii_scopes.insert(OAUTH2_SCOPE_SSH_PUBLICKEYS.to_string());
2231            }
2232
2233            // Consent token expires in
2234            let expiry = ct.as_secs() + 300;
2235
2236            // Subsequent we then return an encrypted session handle which allows
2237            // the user to indicate their consent to this authorisation.
2238            //
2239            // This session handle is what we use in "permit" to generate the redirect.
2240
2241            let consent_req = ConsentToken {
2242                client_id: auth_req.client_id.clone(),
2243                ident_id: ident.get_event_origin_id(),
2244                expiry,
2245                session_id,
2246                state: auth_req.state.clone(),
2247                code_challenge,
2248                redirect_uri: auth_req.redirect_uri.clone(),
2249                scopes: granted_scopes.iter().cloned().collect(),
2250                nonce: auth_req.nonce.clone(),
2251                response_mode,
2252            };
2253
2254            let consent_jwe = Jwe::into_json(&consent_req).map_err(|err| {
2255                error!(?err, "Unable to encode consent data");
2256                Oauth2Error::ServerError(OperationError::SerdeJsonError)
2257            })?;
2258
2259            let consent_token = self
2260                .oauth2rs
2261                .inner
2262                .consent_key
2263                .encipher::<JweA128GCMEncipher>(&consent_jwe)
2264                .map(|jwe_compact| jwe_compact.to_string())
2265                .map_err(|err| {
2266                    error!(?err, "Unable to encrypt jwe");
2267                    Oauth2Error::ServerError(OperationError::CryptographyError)
2268                })?;
2269
2270            Ok(AuthoriseResponse::ConsentRequested {
2271                client_name: o2rs.displayname.clone(),
2272                scopes: granted_scopes.into_iter().collect(),
2273                pii_scopes,
2274                consent_token,
2275            })
2276        }
2277    }
2278
2279    #[instrument(level = "debug", skip_all)]
2280    pub fn check_oauth2_authorise_reject(
2281        &self,
2282        ident: &Identity,
2283        consent_token: &str,
2284        ct: Duration,
2285    ) -> Result<AuthoriseReject, OperationError> {
2286        let jwe_compact = JweCompact::from_str(consent_token).map_err(|_| {
2287            error!("Failed to deserialise a valid JWE");
2288            OperationError::CryptographyError
2289        })?;
2290
2291        // Decode the consent req with our system fernet key. Use a ttl of 5 minutes.
2292        let consent_req: ConsentToken = self
2293            .oauth2rs
2294            .inner
2295            .consent_key
2296            .decipher(&jwe_compact)
2297            .map_err(|_| {
2298                admin_error!("Failed to decrypt consent request");
2299                OperationError::CryptographyError
2300            })
2301            .and_then(|jwe| {
2302                jwe.from_json().map_err(|err| {
2303                    error!(?err, "Failed to deserialise consent request");
2304                    OperationError::SerdeJsonError
2305                })
2306            })?;
2307
2308        // Validate that the ident_id matches our current ident.
2309        if consent_req.ident_id != ident.get_event_origin_id() {
2310            security_info!("consent request ident id does not match the identity of our UAT.");
2311            return Err(OperationError::InvalidSessionState);
2312        }
2313
2314        // Validate that the session id matches our session
2315        if consent_req.session_id != ident.get_session_id() {
2316            security_info!("consent request sessien id does not match the session id of our UAT.");
2317            return Err(OperationError::InvalidSessionState);
2318        }
2319
2320        if consent_req.expiry <= ct.as_secs() {
2321            // Token is expired
2322            error!("Failed to decrypt consent request");
2323            return Err(OperationError::CryptographyError);
2324        }
2325
2326        // Get the resource server config based on this client_id.
2327        let _o2rs = self
2328            .oauth2rs
2329            .inner
2330            .rs_set_get(&consent_req.client_id)
2331            .ok_or_else(|| {
2332                admin_error!("Invalid consent request OAuth2 client_id");
2333                OperationError::InvalidRequestState
2334            })?;
2335
2336        // All good, now confirm the rejection to the client application.
2337        Ok(AuthoriseReject {
2338            redirect_uri: consent_req.redirect_uri,
2339            response_mode: consent_req.response_mode,
2340        })
2341    }
2342
2343    #[instrument(level = "debug", skip_all)]
2344    pub fn check_oauth2_token_introspect(
2345        &mut self,
2346        client_auth_info: &ClientAuthInfo,
2347        intr_req: &AccessTokenIntrospectRequest,
2348        ct: Duration,
2349    ) -> Result<AccessTokenIntrospectResponse, Oauth2Error> {
2350        let client_auth = get_client_auth(client_auth_info, &intr_req.client_post_auth)?;
2351
2352        // Get the o2rs for the handle.
2353        let o2rs = self
2354            .oauth2rs
2355            .inner
2356            .rs_set_get(&client_auth.client_id)
2357            .ok_or_else(|| {
2358                warn!("Invalid OAuth2 client_id");
2359                Oauth2Error::AuthenticationRequired
2360            })?;
2361
2362        // check the secret.
2363        match &o2rs.type_ {
2364            OauthRSType::Basic { authz_secret, .. } => {
2365                if Some(authz_secret) != client_auth.client_secret.as_ref() {
2366                    info!("Invalid OAuth2 client_id secret");
2367                    return Err(Oauth2Error::AuthenticationRequired);
2368                }
2369            }
2370            // Relies on the token to be valid.
2371            OauthRSType::Public { .. } => {}
2372        };
2373
2374        // We are authenticated! Yay! Now we can actually check things ...
2375
2376        let prefer_short_username = o2rs.prefer_short_username;
2377
2378        if let Ok(jwsc) = JwsCompact::from_str(&intr_req.token) {
2379            let access_token = o2rs
2380                .key_object
2381                .jws_verify(&jwsc)
2382                .map_err(|err| {
2383                    error!(?err, "Unable to verify access token");
2384                    Oauth2Error::InvalidRequest
2385                })
2386                .and_then(|jws| {
2387                    jws.from_json().map_err(|err| {
2388                        error!(?err, "Unable to deserialise access token");
2389                        Oauth2Error::InvalidRequest
2390                    })
2391                })?;
2392
2393            let OAuth2RFC9068Token::<_> {
2394                iss: _,
2395                sub,
2396                aud: _,
2397                exp,
2398                nbf,
2399                iat,
2400                jti,
2401                client_id: _,
2402                extensions:
2403                    OAuth2RFC9068TokenExtensions {
2404                        auth_time: _,
2405                        acr: _,
2406                        amr: _,
2407                        scope: scopes,
2408                        nonce: _,
2409                        session_id,
2410                        parent_session_id,
2411                    },
2412            } = access_token;
2413
2414            // Has this token expired?
2415            if exp <= ct.as_secs() as i64 {
2416                security_info!(?sub, "access token has expired, returning inactive");
2417                return Ok(AccessTokenIntrospectResponse::inactive(jti));
2418            }
2419
2420            // Is the user expired, or the OAuth2 session invalid?
2421            let valid = self
2422                .check_oauth2_account_uuid_valid(sub, session_id, parent_session_id, iat, ct)
2423                .map_err(|_| admin_error!("Account is not valid"));
2424
2425            let Ok(Some(entry)) = valid else {
2426                security_info!(
2427                    ?sub,
2428                    "access token account is not valid, returning inactive"
2429                );
2430                return Ok(AccessTokenIntrospectResponse::inactive(jti));
2431            };
2432
2433            let account = match Account::try_from_entry_ro(&entry, &mut self.qs_read) {
2434                Ok(account) => account,
2435                Err(err) => return Err(Oauth2Error::ServerError(err)),
2436            };
2437
2438            // ==== good to generate response ====
2439
2440            let scope = scopes.clone();
2441
2442            let preferred_username = if prefer_short_username {
2443                Some(account.name().into())
2444            } else {
2445                Some(account.spn().into())
2446            };
2447
2448            let token_type = Some(AccessTokenType::Bearer);
2449            Ok(AccessTokenIntrospectResponse {
2450                active: true,
2451                scope,
2452                client_id: Some(client_auth.client_id.clone()),
2453                username: preferred_username,
2454                token_type,
2455                iat: Some(iat),
2456                exp: Some(exp),
2457                nbf: Some(nbf),
2458                sub: Some(sub.to_string()),
2459                aud: Some(client_auth.client_id),
2460                iss: None,
2461                jti,
2462            })
2463        } else {
2464            let jwe_compact = JweCompact::from_str(&intr_req.token).map_err(|_| {
2465                error!("Failed to deserialise a valid JWE");
2466                Oauth2Error::InvalidRequest
2467            })?;
2468
2469            let token: Oauth2TokenType = o2rs
2470                .key_object
2471                .jwe_decrypt(&jwe_compact)
2472                .map_err(|_| {
2473                    admin_error!("Failed to decrypt token introspection request");
2474                    Oauth2Error::InvalidRequest
2475                })
2476                .and_then(|jwe| {
2477                    jwe.from_json().map_err(|err| {
2478                        error!(?err, "Failed to deserialise token");
2479                        Oauth2Error::InvalidRequest
2480                    })
2481                })?;
2482
2483            match token {
2484                Oauth2TokenType::ClientAccess {
2485                    scopes,
2486                    session_id,
2487                    uuid,
2488                    exp,
2489                    iat,
2490                    nbf,
2491                } => {
2492                    // Has this token expired?
2493                    if exp <= ct.as_secs() as i64 {
2494                        security_info!(?uuid, "access token has expired, returning inactive");
2495                        return Ok(AccessTokenIntrospectResponse::inactive(session_id));
2496                    }
2497
2498                    // We can't do the same validity check for the client as we do with an account
2499                    let valid = self
2500                        .check_oauth2_account_uuid_valid(uuid, session_id, None, iat, ct)
2501                        .map_err(|_| admin_error!("Account is not valid"));
2502
2503                    let Ok(Some(entry)) = valid else {
2504                        security_info!(
2505                            ?uuid,
2506                            "access token account is not valid, returning inactive"
2507                        );
2508                        return Ok(AccessTokenIntrospectResponse::inactive(session_id));
2509                    };
2510
2511                    let scope = scopes.clone();
2512
2513                    let token_type = Some(AccessTokenType::Bearer);
2514
2515                    let username = if prefer_short_username {
2516                        entry
2517                            .get_ava_single_iname(Attribute::Name)
2518                            .map(|s| s.to_string())
2519                    } else {
2520                        entry.get_ava_single_proto_string(Attribute::Spn)
2521                    };
2522
2523                    Ok(AccessTokenIntrospectResponse {
2524                        active: true,
2525                        scope,
2526                        client_id: Some(client_auth.client_id.clone()),
2527                        username,
2528                        token_type,
2529                        iat: Some(iat),
2530                        exp: Some(exp),
2531                        nbf: Some(nbf),
2532                        sub: Some(uuid.to_string()),
2533                        aud: Some(client_auth.client_id),
2534                        iss: None,
2535                        jti: session_id,
2536                    })
2537                }
2538                Oauth2TokenType::Refresh { session_id, .. } => {
2539                    Ok(AccessTokenIntrospectResponse::inactive(session_id))
2540                }
2541            }
2542        }
2543    }
2544
2545    #[instrument(level = "debug", skip_all)]
2546    pub fn oauth2_openid_userinfo(
2547        &mut self,
2548        client_id: &str,
2549        token: &JwsCompact,
2550        ct: Duration,
2551    ) -> Result<OidcToken, Oauth2Error> {
2552        // DANGER: Why do we have to do this? During the use of qs for internal search
2553        // and other operations we need qs to be mut. But when we borrow oauth2rs here we
2554        // cause multiple borrows to occur on struct members that freaks rust out. This *IS*
2555        // safe however because no element of the search or write process calls the oauth2rs
2556        // excepting for this idm layer within a single thread, meaning that stripping the
2557        // lifetime here is safe since we are the sole accessor.
2558        let o2rs: &Oauth2RS = unsafe {
2559            let s = self.oauth2rs.inner.rs_set_get(client_id).ok_or_else(|| {
2560                warn!("Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)");
2561                Oauth2Error::InvalidClientId
2562            })?;
2563            &*(s as *const _)
2564        };
2565
2566        let access_token = o2rs
2567            .key_object
2568            .jws_verify(token)
2569            .map_err(|err| {
2570                error!(?err, "Unable to verify access token");
2571                Oauth2Error::InvalidRequest
2572            })
2573            .and_then(|jws| {
2574                jws.from_json().map_err(|err| {
2575                    error!(?err, "Unable to deserialise access token");
2576                    Oauth2Error::InvalidRequest
2577                })
2578            })?;
2579
2580        let OAuth2RFC9068Token::<_> {
2581            iss: _,
2582            sub,
2583            aud: _,
2584            exp,
2585            nbf,
2586            iat,
2587            jti: _,
2588            client_id: _,
2589            extensions:
2590                OAuth2RFC9068TokenExtensions {
2591                    auth_time: _,
2592                    acr: _,
2593                    amr: _,
2594                    scope: scopes,
2595                    nonce,
2596                    session_id,
2597                    parent_session_id,
2598                },
2599        } = access_token;
2600        // Has this token expired?
2601        if exp <= ct.as_secs() as i64 {
2602            security_info!(?sub, "access token has expired, returning inactive");
2603            return Err(Oauth2Error::InvalidToken);
2604        }
2605
2606        // Is the user expired, or the OAuth2 session invalid?
2607        let valid = self
2608            .check_oauth2_account_uuid_valid(sub, session_id, parent_session_id, iat, ct)
2609            .map_err(|_| admin_error!("Account is not valid"));
2610
2611        let Ok(Some(entry)) = valid else {
2612            security_info!(
2613                ?sub,
2614                "access token has account not valid, returning inactive"
2615            );
2616            return Err(Oauth2Error::InvalidToken);
2617        };
2618
2619        let account = match Account::try_from_entry_ro(&entry, &mut self.qs_read) {
2620            Ok(account) => account,
2621            Err(err) => return Err(Oauth2Error::ServerError(err)),
2622        };
2623
2624        let amr = None;
2625
2626        let iss = o2rs.iss.clone();
2627
2628        let s_claims = s_claims_for_account(o2rs, &account, &scopes);
2629        let extra_claims = extra_claims_for_account(&account, &o2rs.claim_map, &scopes);
2630
2631        // ==== good to generate response ====
2632
2633        Ok(OidcToken {
2634            iss,
2635            sub: OidcSubject::U(sub),
2636            aud: client_id.to_string(),
2637            iat,
2638            nbf: Some(nbf),
2639            exp,
2640            auth_time: None,
2641            nonce,
2642            at_hash: None,
2643            acr: None,
2644            amr,
2645            azp: Some(client_id.to_string()),
2646            jti: Some(session_id.to_string()),
2647            s_claims,
2648            claims: extra_claims,
2649        })
2650    }
2651
2652    #[instrument(level = "debug", skip_all)]
2653    pub fn oauth2_rfc8414_metadata(
2654        &self,
2655        client_id: &str,
2656    ) -> Result<Oauth2Rfc8414MetadataResponse, OperationError> {
2657        let o2rs = self.oauth2rs.inner.rs_set_get(client_id).ok_or_else(|| {
2658            warn!("Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)");
2659            OperationError::NoMatchingEntries
2660        })?;
2661
2662        let issuer = o2rs.iss.clone();
2663        let authorization_endpoint = o2rs.authorization_endpoint.clone();
2664        let token_endpoint = o2rs.token_endpoint.clone();
2665        let revocation_endpoint = Some(o2rs.revocation_endpoint.clone());
2666        let introspection_endpoint = Some(o2rs.introspection_endpoint.clone());
2667        let jwks_uri = Some(o2rs.jwks_uri.clone());
2668        let scopes_supported = Some(o2rs.scopes_supported.iter().cloned().collect());
2669        let response_types_supported = vec![ResponseType::Code];
2670        let response_modes_supported = vec![ResponseMode::Query, ResponseMode::Fragment];
2671        let grant_types_supported = vec![GrantType::AuthorisationCode];
2672
2673        let token_endpoint_auth_methods_supported = vec![
2674            TokenEndpointAuthMethod::ClientSecretBasic,
2675            TokenEndpointAuthMethod::ClientSecretPost,
2676        ];
2677
2678        let revocation_endpoint_auth_methods_supported = vec![
2679            TokenEndpointAuthMethod::ClientSecretBasic,
2680            TokenEndpointAuthMethod::ClientSecretPost,
2681        ];
2682
2683        let introspection_endpoint_auth_methods_supported = vec![
2684            TokenEndpointAuthMethod::ClientSecretBasic,
2685            TokenEndpointAuthMethod::ClientSecretPost,
2686        ];
2687
2688        let service_documentation = Some(URL_SERVICE_DOCUMENTATION.clone());
2689
2690        let code_challenge_methods_supported = if o2rs.require_pkce() {
2691            vec![PkceAlg::S256]
2692        } else {
2693            Vec::with_capacity(0)
2694        };
2695
2696        Ok(Oauth2Rfc8414MetadataResponse {
2697            issuer,
2698            authorization_endpoint,
2699            token_endpoint,
2700            jwks_uri,
2701            registration_endpoint: None,
2702            scopes_supported,
2703            response_types_supported,
2704            response_modes_supported,
2705            grant_types_supported,
2706            token_endpoint_auth_methods_supported,
2707            token_endpoint_auth_signing_alg_values_supported: None,
2708            service_documentation,
2709            ui_locales_supported: None,
2710            op_policy_uri: None,
2711            op_tos_uri: None,
2712            revocation_endpoint,
2713            revocation_endpoint_auth_methods_supported,
2714            introspection_endpoint,
2715            introspection_endpoint_auth_methods_supported,
2716            introspection_endpoint_auth_signing_alg_values_supported: None,
2717            code_challenge_methods_supported,
2718        })
2719    }
2720
2721    #[instrument(level = "debug", skip_all)]
2722    pub fn oauth2_openid_discovery(
2723        &self,
2724        client_id: &str,
2725    ) -> Result<OidcDiscoveryResponse, OperationError> {
2726        let o2rs = self.oauth2rs.inner.rs_set_get(client_id).ok_or_else(|| {
2727            warn!("Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)");
2728            OperationError::NoMatchingEntries
2729        })?;
2730
2731        let issuer = o2rs.iss.clone();
2732
2733        let authorization_endpoint = o2rs.authorization_endpoint.clone();
2734        let token_endpoint = o2rs.token_endpoint.clone();
2735        let userinfo_endpoint = Some(o2rs.userinfo_endpoint.clone());
2736        let jwks_uri = o2rs.jwks_uri.clone();
2737        let scopes_supported = Some(o2rs.scopes_supported.iter().cloned().collect());
2738        let response_types_supported = vec![ResponseType::Code];
2739        let response_modes_supported = vec![ResponseMode::Query, ResponseMode::Fragment];
2740
2741        // TODO: add device code if the rs supports it per <https://www.rfc-editor.org/rfc/rfc8628#section-4>
2742        // `urn:ietf:params:oauth:grant-type:device_code`
2743        let grant_types_supported = vec![GrantType::AuthorisationCode];
2744
2745        let subject_types_supported = vec![SubjectType::Public];
2746
2747        let id_token_signing_alg_values_supported = match &o2rs.sign_alg {
2748            SignatureAlgo::Es256 => vec![IdTokenSignAlg::ES256],
2749            SignatureAlgo::Rs256 => vec![IdTokenSignAlg::RS256],
2750        };
2751
2752        let userinfo_signing_alg_values_supported = None;
2753        let token_endpoint_auth_methods_supported = vec![
2754            TokenEndpointAuthMethod::ClientSecretBasic,
2755            TokenEndpointAuthMethod::ClientSecretPost,
2756        ];
2757        let display_values_supported = Some(vec![DisplayValue::Page]);
2758        let claim_types_supported = vec![ClaimType::Normal];
2759        // What claims can we offer?
2760        let claims_supported = None;
2761        let service_documentation = Some(URL_SERVICE_DOCUMENTATION.clone());
2762
2763        let code_challenge_methods_supported = if o2rs.require_pkce() {
2764            vec![PkceAlg::S256]
2765        } else {
2766            Vec::with_capacity(0)
2767        };
2768
2769        // The following are extensions allowed by the oidc specification.
2770
2771        let revocation_endpoint = Some(o2rs.revocation_endpoint.clone());
2772        let revocation_endpoint_auth_methods_supported = vec![
2773            TokenEndpointAuthMethod::ClientSecretBasic,
2774            TokenEndpointAuthMethod::ClientSecretPost,
2775        ];
2776
2777        let introspection_endpoint = Some(o2rs.introspection_endpoint.clone());
2778        let introspection_endpoint_auth_methods_supported = vec![
2779            TokenEndpointAuthMethod::ClientSecretBasic,
2780            TokenEndpointAuthMethod::ClientSecretPost,
2781        ];
2782
2783        Ok(OidcDiscoveryResponse {
2784            issuer,
2785            authorization_endpoint,
2786            token_endpoint,
2787            userinfo_endpoint,
2788            jwks_uri,
2789            registration_endpoint: None,
2790            scopes_supported,
2791            response_types_supported,
2792            response_modes_supported,
2793            grant_types_supported,
2794            acr_values_supported: None,
2795            subject_types_supported,
2796            id_token_signing_alg_values_supported,
2797            id_token_encryption_alg_values_supported: None,
2798            id_token_encryption_enc_values_supported: None,
2799            userinfo_signing_alg_values_supported,
2800            userinfo_encryption_alg_values_supported: None,
2801            userinfo_encryption_enc_values_supported: None,
2802            request_object_signing_alg_values_supported: None,
2803            request_object_encryption_alg_values_supported: None,
2804            request_object_encryption_enc_values_supported: None,
2805            token_endpoint_auth_methods_supported,
2806            token_endpoint_auth_signing_alg_values_supported: None,
2807            display_values_supported,
2808            claim_types_supported,
2809            claims_supported,
2810            service_documentation,
2811            claims_locales_supported: None,
2812            ui_locales_supported: None,
2813            claims_parameter_supported: false,
2814            // TODO: once we support RFC9101 this can be true again
2815            request_parameter_supported: false,
2816            // TODO: if we support RFC9101 request_uri methods this can be true
2817            request_uri_parameter_supported: false,
2818            // TODO: if we support RFC9101 request_uri methods this should be true
2819            require_request_uri_registration: false,
2820            op_policy_uri: None,
2821            op_tos_uri: None,
2822            code_challenge_methods_supported,
2823            // Extensions
2824            revocation_endpoint,
2825            revocation_endpoint_auth_methods_supported,
2826            introspection_endpoint,
2827            introspection_endpoint_auth_methods_supported,
2828            introspection_endpoint_auth_signing_alg_values_supported: None,
2829            device_authorization_endpoint: o2rs.device_authorization_endpoint.clone(),
2830        })
2831    }
2832
2833    #[instrument(level = "debug", skip_all)]
2834    pub fn oauth2_openid_webfinger(
2835        &mut self,
2836        client_id: &str,
2837        resource_id: &str,
2838    ) -> Result<OidcWebfingerResponse, OperationError> {
2839        let o2rs = self.oauth2rs.inner.rs_set_get(client_id).ok_or_else(|| {
2840            warn!("Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)");
2841            OperationError::NoMatchingEntries
2842        })?;
2843
2844        let Some(spn) = PartialValue::new_spn_s(resource_id) else {
2845            return Err(OperationError::NoMatchingEntries);
2846        };
2847
2848        // Ensure that the account exists.
2849        if !self
2850            .qs_read
2851            .internal_exists(&Filter::new(f_eq(Attribute::Spn, spn)))?
2852        {
2853            return Err(OperationError::NoMatchingEntries);
2854        }
2855
2856        let issuer = o2rs.iss.clone();
2857
2858        Ok(OidcWebfingerResponse {
2859            // we set the subject to the resource_id to ensure we always send something valid back
2860            // but realistically this will be overwritten on at the API layer
2861            subject: resource_id.to_string(),
2862            links: vec![OidcWebfingerRel {
2863                rel: "http://openid.net/specs/connect/1.0/issuer".into(),
2864                href: issuer.into(),
2865            }],
2866        })
2867    }
2868
2869    #[instrument(level = "debug", skip_all)]
2870    pub fn oauth2_openid_publickey(&self, client_id: &str) -> Result<JwkKeySet, OperationError> {
2871        let o2rs = self.oauth2rs.inner.rs_set_get(client_id).ok_or_else(|| {
2872            warn!("Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)");
2873            OperationError::NoMatchingEntries
2874        })?;
2875
2876        trace!(sign_alg = ?o2rs.sign_alg);
2877
2878        match o2rs.sign_alg {
2879            SignatureAlgo::Es256 => o2rs.key_object.jws_es256_jwks(),
2880            SignatureAlgo::Rs256 => o2rs.key_object.jws_rs256_jwks(),
2881        }
2882        .ok_or_else(|| {
2883            error!(o2_client = ?o2rs.name, "Unable to retrieve public keys");
2884            OperationError::InvalidState
2885        })
2886    }
2887}
2888
2889fn parse_basic_authz(client_authz: &str) -> Result<ClientAuth, Oauth2Error> {
2890    // Check the client_authz
2891    let authz = general_purpose::STANDARD
2892        .decode(client_authz)
2893        .map_err(|_| {
2894            admin_error!("Basic authz invalid base64");
2895            Oauth2Error::AuthenticationRequired
2896        })
2897        .and_then(|data| {
2898            String::from_utf8(data).map_err(|_| {
2899                admin_error!("Basic authz invalid utf8");
2900                Oauth2Error::AuthenticationRequired
2901            })
2902        })?;
2903
2904    // Get the first :, it should be our delim.
2905    //
2906    let mut split_iter = authz.split(':');
2907
2908    let client_id = split_iter.next().ok_or_else(|| {
2909        admin_error!("Basic authz invalid format (corrupt input?)");
2910        Oauth2Error::AuthenticationRequired
2911    })?;
2912    let secret = split_iter.next().ok_or_else(|| {
2913        admin_error!("Basic authz invalid format (missing ':' separator?)");
2914        Oauth2Error::AuthenticationRequired
2915    })?;
2916
2917    Ok((client_id, Some(secret)).into())
2918}
2919
2920fn s_claims_for_account(
2921    o2rs: &Oauth2RS,
2922    account: &Account,
2923    scopes: &BTreeSet<String>,
2924) -> OidcClaims {
2925    let preferred_username = if o2rs.prefer_short_username {
2926        Some(account.name().into())
2927    } else {
2928        Some(account.spn().into())
2929    };
2930
2931    let (email, email_verified) = if scopes.contains(OAUTH2_SCOPE_EMAIL) {
2932        if let Some(mp) = &account.mail_primary {
2933            (Some(mp.clone()), Some(true))
2934        } else {
2935            (None, None)
2936        }
2937    } else {
2938        (None, None)
2939    };
2940
2941    OidcClaims {
2942        // Map from displayname
2943        name: Some(account.displayname.clone()),
2944        scopes: scopes.iter().cloned().collect(),
2945        preferred_username,
2946        email,
2947        email_verified,
2948        ..Default::default()
2949    }
2950}
2951
2952fn extra_claims_for_account(
2953    account: &Account,
2954
2955    claim_map: &BTreeMap<Uuid, Vec<(String, ClaimValue)>>,
2956
2957    scopes: &BTreeSet<String>,
2958) -> BTreeMap<String, serde_json::Value> {
2959    let mut extra_claims = BTreeMap::new();
2960
2961    let mut account_claims: BTreeMap<&str, ClaimValue> = BTreeMap::new();
2962
2963    // for each group
2964    for group_uuid in account.groups.iter().map(|g| g.uuid()) {
2965        // Does this group have any custom claims?
2966        if let Some(claim) = claim_map.get(group_uuid) {
2967            // If so, iterate over the set of claims and values.
2968            for (claim_name, claim_value) in claim.iter() {
2969                // Does this claim name already exist in our in-progress map?
2970                match account_claims.entry(claim_name.as_str()) {
2971                    BTreeEntry::Vacant(e) => {
2972                        e.insert(claim_value.clone());
2973                    }
2974                    BTreeEntry::Occupied(mut e) => {
2975                        let mut_claim_value = e.get_mut();
2976                        // Merge the extra details into this.
2977                        mut_claim_value.merge(claim_value);
2978                    }
2979                }
2980            }
2981        }
2982    }
2983
2984    // Now, flatten all these into the final structure.
2985    for (claim_name, claim_value) in account_claims {
2986        extra_claims.insert(claim_name.to_string(), claim_value.to_json_value());
2987    }
2988
2989    // Now perform our custom claim's from scopes. We do these second so that
2990    // a user can't stomp our claim names.
2991
2992    if scopes.contains(OAUTH2_SCOPE_SSH_PUBLICKEYS) {
2993        extra_claims.insert(
2994            OAUTH2_SCOPE_SSH_PUBLICKEYS.to_string(),
2995            account
2996                .sshkeys()
2997                .values()
2998                .map(|pub_key| serde_json::Value::String(pub_key.to_string()))
2999                .collect(),
3000        );
3001    }
3002
3003    let wants_groups = scopes.contains(OAUTH2_SCOPE_GROUPS);
3004    // groups implies uuid + spn to match current behaviour.
3005    let wants_groups_uuid = wants_groups || scopes.contains(OAUTH2_SCOPE_GROUPS_UUID);
3006    let wants_groups_spn = wants_groups || scopes.contains(OAUTH2_SCOPE_GROUPS_SPN);
3007    let wants_groups_name = scopes.contains(OAUTH2_SCOPE_GROUPS_NAME);
3008
3009    if wants_groups_uuid || wants_groups_name || wants_groups_spn {
3010        extra_claims.insert(
3011            OAUTH2_SCOPE_GROUPS.to_string(),
3012            account
3013                .groups
3014                .iter()
3015                .flat_map(|group| {
3016                    let mut attrs = Vec::with_capacity(3);
3017
3018                    if wants_groups_uuid {
3019                        attrs.push(group.uuid().as_hyphenated().to_string())
3020                    }
3021
3022                    if wants_groups_spn {
3023                        attrs.push(group.spn().clone())
3024                    }
3025
3026                    if wants_groups_name {
3027                        if let Some(name) = group.name() {
3028                            attrs.push(name.into())
3029                        }
3030                    }
3031
3032                    attrs
3033                })
3034                .collect(),
3035        );
3036    }
3037
3038    trace!(?extra_claims);
3039
3040    extra_claims
3041}
3042
3043fn validate_scopes(req_scopes: &BTreeSet<String>) -> Result<(), Oauth2Error> {
3044    let failed_scopes = req_scopes
3045        .iter()
3046        .filter(|&s| !OAUTHSCOPE_RE.is_match(s))
3047        .cloned()
3048        .collect::<Vec<String>>();
3049
3050    if !failed_scopes.is_empty() {
3051        let requested_scopes_string = req_scopes
3052            .iter()
3053            .cloned()
3054            .collect::<Vec<String>>()
3055            .join(",");
3056        admin_error!(
3057                "Invalid OAuth2 request - requested scopes ({}) but ({}) failed to pass validation rules - all must match the regex {}",
3058                    requested_scopes_string,
3059                    failed_scopes.join(","),
3060                    OAUTHSCOPE_RE.as_str()
3061            );
3062        return Err(Oauth2Error::InvalidScope);
3063    }
3064    Ok(())
3065}
3066
3067/// device code is a random bucket of bytes used in the device flow
3068#[inline]
3069#[cfg(any(feature = "dev-oauth2-device-flow", test))]
3070#[allow(dead_code)]
3071fn gen_device_code() -> Result<[u8; 16], Oauth2Error> {
3072    use rand::TryRngCore;
3073
3074    let mut rng = rand::rng();
3075    let mut result = [0u8; 16];
3076    // doing it here because of feature-shenanigans.
3077    if let Err(err) = rng.try_fill_bytes(&mut result) {
3078        error!("Failed to generate device code! {:?}", err);
3079        return Err(Oauth2Error::ServerError(OperationError::Backend));
3080    }
3081    Ok(result)
3082}
3083
3084#[inline]
3085#[cfg(any(feature = "dev-oauth2-device-flow", test))]
3086#[allow(dead_code)]
3087/// Returns (xxx-yyy-zzz, digits) where one's the human-facing code, the other is what we store in the DB.
3088fn gen_user_code() -> (String, u32) {
3089    use rand::Rng;
3090    let mut rng = rand::rng();
3091    let num: u32 = rng.random_range(0..=999999999);
3092    let result = format!("{num:09}");
3093    (
3094        format!("{}-{}-{}", &result[0..3], &result[3..6], &result[6..9]),
3095        num,
3096    )
3097}
3098
3099/// Take the supplied user code and check it's a valid u32
3100#[allow(dead_code)]
3101fn parse_user_code(val: &str) -> Result<u32, Oauth2Error> {
3102    let mut val = val.to_string();
3103    val.retain(|c| c.is_ascii_digit());
3104    val.parse().map_err(|err| {
3105        debug!("Failed to parse value={} as u32: {:?}", val, err);
3106        Oauth2Error::InvalidRequest
3107    })
3108}
3109
3110/// Check if a host is local (loopback or localhost)
3111fn host_is_local(host: &Host<&str>) -> bool {
3112    match host {
3113        Host::Ipv4(ip) => ip.is_loopback(),
3114        Host::Ipv6(ip) => ip.is_loopback(),
3115        Host::Domain(domain) => *domain == "localhost",
3116    }
3117}
3118
3119/// Ensure that the redirect URI is a loopback/localhost address
3120fn check_is_loopback(redirect_uri: &Url) -> bool {
3121    redirect_uri.host().is_some_and(|host| {
3122        // Check if the host is a loopback/localhost address.
3123        host_is_local(&host)
3124    })
3125}
3126
3127#[cfg(test)]
3128mod tests {
3129    use base64::{engine::general_purpose, Engine as _};
3130    use std::collections::{BTreeMap, BTreeSet};
3131    use std::convert::TryFrom;
3132    use std::str::FromStr;
3133    use std::time::Duration;
3134    use uri::{OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT};
3135
3136    use compact_jwt::{
3137        compact::JwkUse, crypto::JwsRs256Verifier, dangernoverify::JwsDangerReleaseWithoutVerify,
3138        JwaAlg, Jwk, JwsCompact, JwsEs256Verifier, JwsVerifier, OidcSubject, OidcToken,
3139        OidcUnverified,
3140    };
3141    use kanidm_proto::constants::*;
3142    use kanidm_proto::internal::{SshPublicKey, UserAuthToken};
3143    use kanidm_proto::oauth2::*;
3144    use openssl::sha;
3145
3146    use crate::idm::accountpolicy::ResolvedAccountPolicy;
3147    use crate::idm::oauth2::{host_is_local, AuthoriseResponse, Oauth2Error, OauthRSType};
3148    use crate::idm::server::{IdmServer, IdmServerTransaction};
3149    use crate::prelude::*;
3150    use crate::value::{AuthType, OauthClaimMapJoin, SessionState};
3151    use crate::valueset::{ValueSetOauthScopeMap, ValueSetSshKey};
3152
3153    use crate::credential::Credential;
3154    use kanidm_lib_crypto::CryptoPolicy;
3155
3156    use super::Oauth2TokenType;
3157
3158    const TEST_CURRENT_TIME: u64 = 6000;
3159    const UAT_EXPIRE: u64 = 5;
3160    const TOKEN_EXPIRE: u64 = 900;
3161
3162    const UUID_TESTGROUP: Uuid = uuid!("a3028223-bf20-47d5-8b65-967b5d2bb3eb");
3163
3164    macro_rules! create_code_verifier {
3165        ($key:expr) => {{
3166            let code_verifier = $key.to_string();
3167            let mut hasher = sha::Sha256::new();
3168            hasher.update(code_verifier.as_bytes());
3169            let code_challenge: Vec<u8> = hasher.finish().iter().copied().collect();
3170            (Some(code_verifier), code_challenge)
3171        }};
3172    }
3173
3174    macro_rules! good_authorisation_request {
3175        (
3176            $idms_prox_read:expr,
3177            $ident:expr,
3178            $ct:expr,
3179            $code_challenge:expr,
3180            $scope:expr
3181        ) => {{
3182            #[allow(clippy::unnecessary_to_owned)]
3183            let scope: BTreeSet<String> = $scope.split(" ").map(|s| s.to_string()).collect();
3184
3185            let auth_req = AuthorisationRequest {
3186                response_type: ResponseType::Code,
3187                response_mode: None,
3188                client_id: "test_resource_server".to_string(),
3189                state: Some("123".to_string()),
3190                pkce_request: Some(PkceRequest {
3191                    code_challenge: $code_challenge.into(),
3192                    code_challenge_method: CodeChallengeMethod::S256,
3193                }),
3194                redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
3195                scope,
3196                nonce: Some("abcdef".to_string()),
3197                oidc_ext: Default::default(),
3198                max_age: None,
3199                unknown_keys: Default::default(),
3200            };
3201
3202            $idms_prox_read
3203                .check_oauth2_authorisation(Some($ident), &auth_req, $ct)
3204                .expect("OAuth2 authorisation failed")
3205        }};
3206    }
3207
3208    // setup an OAuth2 instance.
3209    async fn setup_oauth2_resource_server_basic(
3210        idms: &IdmServer,
3211        ct: Duration,
3212        enable_pkce: bool,
3213        enable_legacy_crypto: bool,
3214        prefer_short_username: bool,
3215    ) -> (String, UserAuthToken, Identity, Uuid) {
3216        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3217
3218        let rs_uuid = Uuid::new_v4();
3219
3220        let entry_group: Entry<EntryInit, EntryNew> = entry_init!(
3221            (Attribute::Class, EntryClass::Group.to_value()),
3222            (Attribute::Name, Value::new_iname("testgroup")),
3223            (Attribute::Description, Value::new_utf8s("testgroup")),
3224            (Attribute::Uuid, Value::Uuid(UUID_TESTGROUP)),
3225            (Attribute::Member, Value::Refer(UUID_TESTPERSON_1),)
3226        );
3227
3228        let entry_rs: Entry<EntryInit, EntryNew> = entry_init!(
3229            (Attribute::Class, EntryClass::Object.to_value()),
3230            (Attribute::Class, EntryClass::Account.to_value()),
3231            (
3232                Attribute::Class,
3233                EntryClass::OAuth2ResourceServer.to_value()
3234            ),
3235            (
3236                Attribute::Class,
3237                EntryClass::OAuth2ResourceServerBasic.to_value()
3238            ),
3239            (Attribute::Uuid, Value::Uuid(rs_uuid)),
3240            (Attribute::Name, Value::new_iname("test_resource_server")),
3241            (
3242                Attribute::DisplayName,
3243                Value::new_utf8s("test_resource_server")
3244            ),
3245            (
3246                Attribute::OAuth2RsOriginLanding,
3247                Value::new_url_s("https://demo.example.com").unwrap()
3248            ),
3249            // Supplemental origins
3250            (
3251                Attribute::OAuth2RsOrigin,
3252                Value::new_url_s("https://demo.example.com/oauth2/result").unwrap()
3253            ),
3254            (
3255                Attribute::OAuth2RsOrigin,
3256                Value::new_url_s("https://portal.example.com/?custom=foo").unwrap()
3257            ),
3258            (
3259                Attribute::OAuth2RsOrigin,
3260                Value::new_url_s("app://cheese").unwrap()
3261            ),
3262            // System admins
3263            (
3264                Attribute::OAuth2RsScopeMap,
3265                Value::new_oauthscopemap(
3266                    UUID_TESTGROUP,
3267                    btreeset![OAUTH2_SCOPE_GROUPS.to_string()]
3268                )
3269                .expect("invalid oauthscope")
3270            ),
3271            (
3272                Attribute::OAuth2RsScopeMap,
3273                Value::new_oauthscopemap(
3274                    UUID_IDM_ALL_ACCOUNTS,
3275                    btreeset![OAUTH2_SCOPE_OPENID.to_string()]
3276                )
3277                .expect("invalid oauthscope")
3278            ),
3279            (
3280                Attribute::OAuth2RsSupScopeMap,
3281                Value::new_oauthscopemap(
3282                    UUID_IDM_ALL_ACCOUNTS,
3283                    btreeset!["supplement".to_string()]
3284                )
3285                .expect("invalid oauthscope")
3286            ),
3287            (
3288                Attribute::OAuth2AllowInsecureClientDisablePkce,
3289                Value::new_bool(!enable_pkce)
3290            ),
3291            (
3292                Attribute::OAuth2JwtLegacyCryptoEnable,
3293                Value::new_bool(enable_legacy_crypto)
3294            ),
3295            (
3296                Attribute::OAuth2PreferShortUsername,
3297                Value::new_bool(prefer_short_username)
3298            )
3299        );
3300
3301        let ce = CreateEvent::new_internal(vec![entry_rs, entry_group, E_TESTPERSON_1.clone()]);
3302        assert!(idms_prox_write.qs_write.create(&ce).is_ok());
3303
3304        let entry = idms_prox_write
3305            .qs_write
3306            .internal_search_uuid(rs_uuid)
3307            .expect("Failed to retrieve OAuth2 resource entry ");
3308        let secret = entry
3309            .get_ava_single_secret(Attribute::OAuth2RsBasicSecret)
3310            .map(str::to_string)
3311            .expect("No oauth2_rs_basic_secret found");
3312
3313        // Setup the uat we'll be using - note for these tests they *require*
3314        // the parent session to be valid and present!
3315        let session_id = uuid::Uuid::new_v4();
3316
3317        let account = idms_prox_write
3318            .target_to_account(UUID_TESTPERSON_1)
3319            .expect("account must exist");
3320
3321        let uat = account
3322            .to_userauthtoken(
3323                session_id,
3324                SessionScope::ReadWrite,
3325                ct,
3326                &ResolvedAccountPolicy::test_policy(),
3327            )
3328            .expect("Unable to create uat");
3329
3330        // Need the uat first for expiry.
3331        let state = uat
3332            .expiry
3333            .map(SessionState::ExpiresAt)
3334            .unwrap_or(SessionState::NeverExpires);
3335
3336        let p = CryptoPolicy::minimum();
3337        let cred = Credential::new_password_only(&p, "test_password").unwrap();
3338        let cred_id = cred.uuid;
3339
3340        let session = Value::Session(
3341            session_id,
3342            crate::value::Session {
3343                label: "label".to_string(),
3344                state,
3345                issued_at: time::OffsetDateTime::UNIX_EPOCH + ct,
3346                issued_by: IdentityId::Internal,
3347                cred_id,
3348                scope: SessionScope::ReadWrite,
3349                type_: AuthType::Passkey,
3350            },
3351        );
3352
3353        // Mod the user
3354        let modlist = ModifyList::new_list(vec![
3355            Modify::Present(Attribute::UserAuthTokenSession, session),
3356            Modify::Present(
3357                Attribute::PrimaryCredential,
3358                Value::Cred("primary".to_string(), cred),
3359            ),
3360        ]);
3361
3362        idms_prox_write
3363            .qs_write
3364            .internal_modify(
3365                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
3366                &modlist,
3367            )
3368            .expect("Failed to modify user");
3369
3370        let ident = idms_prox_write
3371            .process_uat_to_identity(&uat, ct, Source::Internal)
3372            .expect("Unable to process uat");
3373
3374        idms_prox_write.commit().expect("failed to commit");
3375
3376        (secret, uat, ident, rs_uuid)
3377    }
3378
3379    async fn setup_oauth2_resource_server_public(
3380        idms: &IdmServer,
3381        ct: Duration,
3382    ) -> (UserAuthToken, Identity, Uuid) {
3383        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3384
3385        let rs_uuid = Uuid::new_v4();
3386
3387        let entry_group: Entry<EntryInit, EntryNew> = entry_init!(
3388            (Attribute::Class, EntryClass::Group.to_value()),
3389            (Attribute::Name, Value::new_iname("testgroup")),
3390            (Attribute::Description, Value::new_utf8s("testgroup")),
3391            (Attribute::Uuid, Value::Uuid(UUID_TESTGROUP)),
3392            (Attribute::Member, Value::Refer(UUID_TESTPERSON_1),)
3393        );
3394
3395        let entry_rs: Entry<EntryInit, EntryNew> = entry_init!(
3396            (Attribute::Class, EntryClass::Object.to_value()),
3397            (Attribute::Class, EntryClass::Account.to_value()),
3398            (
3399                Attribute::Class,
3400                EntryClass::OAuth2ResourceServer.to_value()
3401            ),
3402            (
3403                Attribute::Class,
3404                EntryClass::OAuth2ResourceServerPublic.to_value()
3405            ),
3406            (Attribute::Uuid, Value::Uuid(rs_uuid)),
3407            (Attribute::Name, Value::new_iname("test_resource_server")),
3408            (
3409                Attribute::DisplayName,
3410                Value::new_utf8s("test_resource_server")
3411            ),
3412            (
3413                Attribute::OAuth2RsOriginLanding,
3414                Value::new_url_s("https://demo.example.com").unwrap()
3415            ),
3416            (
3417                Attribute::OAuth2RsOrigin,
3418                Value::new_url_s("https://demo.example.com/oauth2/result").unwrap()
3419            ),
3420            // System admins
3421            (
3422                Attribute::OAuth2RsScopeMap,
3423                Value::new_oauthscopemap(
3424                    UUID_TESTGROUP,
3425                    btreeset![OAUTH2_SCOPE_GROUPS.to_string()]
3426                )
3427                .expect("invalid oauthscope")
3428            ),
3429            (
3430                Attribute::OAuth2RsScopeMap,
3431                Value::new_oauthscopemap(
3432                    UUID_IDM_ALL_ACCOUNTS,
3433                    btreeset![OAUTH2_SCOPE_OPENID.to_string()]
3434                )
3435                .expect("invalid oauthscope")
3436            ),
3437            (
3438                Attribute::OAuth2RsSupScopeMap,
3439                Value::new_oauthscopemap(
3440                    UUID_IDM_ALL_ACCOUNTS,
3441                    btreeset!["supplement".to_string()]
3442                )
3443                .expect("invalid oauthscope")
3444            )
3445        );
3446        let ce = CreateEvent::new_internal(vec![entry_rs, entry_group, E_TESTPERSON_1.clone()]);
3447        assert!(idms_prox_write.qs_write.create(&ce).is_ok());
3448
3449        // Setup the uat we'll be using - note for these tests they *require*
3450        // the parent session to be valid and present!
3451
3452        let session_id = uuid::Uuid::new_v4();
3453
3454        let account = idms_prox_write
3455            .target_to_account(UUID_TESTPERSON_1)
3456            .expect("account must exist");
3457        let uat = account
3458            .to_userauthtoken(
3459                session_id,
3460                SessionScope::ReadWrite,
3461                ct,
3462                &ResolvedAccountPolicy::test_policy(),
3463            )
3464            .expect("Unable to create uat");
3465
3466        // Need the uat first for expiry.
3467        let state = uat
3468            .expiry
3469            .map(SessionState::ExpiresAt)
3470            .unwrap_or(SessionState::NeverExpires);
3471
3472        let p = CryptoPolicy::minimum();
3473        let cred = Credential::new_password_only(&p, "test_password").unwrap();
3474        let cred_id = cred.uuid;
3475
3476        let session = Value::Session(
3477            session_id,
3478            crate::value::Session {
3479                label: "label".to_string(),
3480                state,
3481                issued_at: time::OffsetDateTime::UNIX_EPOCH + ct,
3482                issued_by: IdentityId::Internal,
3483                cred_id,
3484                scope: SessionScope::ReadWrite,
3485                type_: AuthType::Passkey,
3486            },
3487        );
3488
3489        // Mod the user
3490        let modlist = ModifyList::new_list(vec![
3491            Modify::Present(Attribute::UserAuthTokenSession, session),
3492            Modify::Present(
3493                Attribute::PrimaryCredential,
3494                Value::Cred("primary".to_string(), cred),
3495            ),
3496        ]);
3497
3498        idms_prox_write
3499            .qs_write
3500            .internal_modify(
3501                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
3502                &modlist,
3503            )
3504            .expect("Failed to modify user");
3505
3506        let ident = idms_prox_write
3507            .process_uat_to_identity(&uat, ct, Source::Internal)
3508            .expect("Unable to process uat");
3509
3510        idms_prox_write.commit().expect("failed to commit");
3511
3512        (uat, ident, rs_uuid)
3513    }
3514
3515    /// Perform an oauth2 exchange, assuming that it will succeed.
3516    async fn perform_oauth2_exchange(
3517        idms: &IdmServer,
3518        ident: &Identity,
3519        ct: Duration,
3520        client_authz: ClientAuthInfo,
3521        scopes: String,
3522    ) -> AccessTokenResponse {
3523        let idms_prox_read = idms.proxy_read().await.unwrap();
3524
3525        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
3526
3527        let consent_request =
3528            good_authorisation_request!(idms_prox_read, ident, ct, code_challenge, scopes);
3529
3530        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
3531            unreachable!();
3532        };
3533
3534        // == Manually submit the consent token to the permit for the permit_success
3535        drop(idms_prox_read);
3536        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3537
3538        let permit_success = idms_prox_write
3539            .check_oauth2_authorise_permit(ident, &consent_token, ct)
3540            .expect("Failed to perform OAuth2 permit");
3541
3542        // == Submit the token exchange code.
3543        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
3544            code: permit_success.code,
3545            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
3546            // From the first step.
3547            code_verifier,
3548        }
3549        .into();
3550
3551        let token_response = idms_prox_write
3552            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
3553            .expect("Failed to perform OAuth2 token exchange");
3554
3555        assert!(idms_prox_write.commit().is_ok());
3556
3557        token_response
3558    }
3559
3560    async fn validate_id_token(idms: &IdmServer, ct: Duration, id_token: &str) -> OidcToken {
3561        let idms_prox_read = idms.proxy_read().await.unwrap();
3562
3563        let mut jwkset = idms_prox_read
3564            .oauth2_openid_publickey("test_resource_server")
3565            .expect("Failed to get public key");
3566        let public_jwk = jwkset.keys.pop().expect("no such jwk");
3567
3568        let jws_validator =
3569            JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
3570
3571        let oidc_unverified = OidcUnverified::from_str(id_token).expect("Failed to parse id_token");
3572
3573        let iat = ct.as_secs() as i64;
3574
3575        jws_validator
3576            .verify(&oidc_unverified)
3577            .unwrap()
3578            .verify_exp(iat)
3579            .expect("Failed to verify oidc")
3580    }
3581
3582    async fn setup_idm_admin(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) {
3583        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3584        let account = idms_prox_write
3585            .target_to_account(UUID_IDM_ADMIN)
3586            .expect("account must exist");
3587        let session_id = uuid::Uuid::new_v4();
3588        let uat = account
3589            .to_userauthtoken(
3590                session_id,
3591                SessionScope::ReadWrite,
3592                ct,
3593                &ResolvedAccountPolicy::test_policy(),
3594            )
3595            .expect("Unable to create uat");
3596        let ident = idms_prox_write
3597            .process_uat_to_identity(&uat, ct, Source::Internal)
3598            .expect("Unable to process uat");
3599
3600        idms_prox_write.commit().expect("failed to commit");
3601
3602        (uat, ident)
3603    }
3604
3605    #[idm_test]
3606    async fn test_idm_oauth2_basic_function(
3607        idms: &IdmServer,
3608        _idms_delayed: &mut IdmServerDelayed,
3609    ) {
3610        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3611        let (secret, _uat, ident, _) =
3612            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
3613
3614        let idms_prox_read = idms.proxy_read().await.unwrap();
3615
3616        // == Setup the authorisation request
3617        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
3618
3619        let consent_request = good_authorisation_request!(
3620            idms_prox_read,
3621            &ident,
3622            ct,
3623            code_challenge,
3624            OAUTH2_SCOPE_OPENID.to_string()
3625        );
3626
3627        // Should be in the consent phase;
3628        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
3629            unreachable!();
3630        };
3631
3632        // == Manually submit the consent token to the permit for the permit_success
3633        drop(idms_prox_read);
3634        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3635
3636        let permit_success = idms_prox_write
3637            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
3638            .expect("Failed to perform OAuth2 permit");
3639
3640        // Check we are reflecting the CSRF properly.
3641        assert_eq!(permit_success.state.as_deref(), Some("123"));
3642
3643        // == Submit the token exchange code.
3644
3645        let token_req = AccessTokenRequest {
3646            grant_type: GrantTypeReq::AuthorizationCode {
3647                code: permit_success.code,
3648                redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
3649                // From the first step.
3650                code_verifier,
3651            },
3652            client_post_auth: ClientPostAuth {
3653                client_id: Some("test_resource_server".to_string()),
3654                client_secret: Some(secret),
3655            },
3656        };
3657
3658        let token_response = idms_prox_write
3659            .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
3660            .expect("Failed to perform OAuth2 token exchange");
3661
3662        // 🎉 We got a token! In the future we can then check introspection from this point.
3663        assert_eq!(token_response.token_type, AccessTokenType::Bearer);
3664
3665        assert!(idms_prox_write.commit().is_ok());
3666    }
3667
3668    #[idm_test]
3669    async fn test_idm_oauth2_public_function(
3670        idms: &IdmServer,
3671        _idms_delayed: &mut IdmServerDelayed,
3672    ) {
3673        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3674        let (_uat, ident, _) = setup_oauth2_resource_server_public(idms, ct).await;
3675
3676        let idms_prox_read = idms.proxy_read().await.unwrap();
3677
3678        // Get an ident/uat for now.
3679
3680        // == Setup the authorisation request
3681        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
3682
3683        let consent_request = good_authorisation_request!(
3684            idms_prox_read,
3685            &ident,
3686            ct,
3687            code_challenge,
3688            OAUTH2_SCOPE_OPENID.to_string()
3689        );
3690
3691        // Should be in the consent phase;
3692        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
3693            unreachable!();
3694        };
3695
3696        // == Manually submit the consent token to the permit for the permit_success
3697        drop(idms_prox_read);
3698        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
3699
3700        let permit_success = idms_prox_write
3701            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
3702            .expect("Failed to perform OAuth2 permit");
3703
3704        // Check we are reflecting the CSRF properly.
3705        assert_eq!(permit_success.state.as_deref(), Some("123"));
3706
3707        // == Submit the token exchange code.
3708
3709        let token_req = AccessTokenRequest {
3710            grant_type: GrantTypeReq::AuthorizationCode {
3711                code: permit_success.code,
3712                redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
3713                // From the first step.
3714                code_verifier,
3715            },
3716
3717            client_post_auth: ClientPostAuth {
3718                client_id: Some("Test_Resource_Server".to_string()),
3719                client_secret: None,
3720            },
3721        };
3722
3723        let token_response = idms_prox_write
3724            .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
3725            .expect("Failed to perform OAuth2 token exchange");
3726
3727        // 🎉 We got a token! In the future we can then check introspection from this point.
3728        assert_eq!(token_response.token_type, AccessTokenType::Bearer);
3729
3730        assert!(idms_prox_write.commit().is_ok());
3731    }
3732
3733    #[idm_test]
3734    async fn test_idm_oauth2_invalid_authorisation_requests(
3735        idms: &IdmServer,
3736        _idms_delayed: &mut IdmServerDelayed,
3737    ) {
3738        // Test invalid OAuth2 authorisation states/requests.
3739        let ct = Duration::from_secs(TEST_CURRENT_TIME);
3740        let (_secret, _uat, ident, _) =
3741            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
3742
3743        let (_anon_uat, anon_ident) = setup_idm_admin(idms, ct).await;
3744        let (_idm_admin_uat, idm_admin_ident) = setup_idm_admin(idms, ct).await;
3745
3746        // Need a uat from a user not in the group. Probs anonymous.
3747        let idms_prox_read = idms.proxy_read().await.unwrap();
3748
3749        let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
3750
3751        let pkce_request = Some(PkceRequest {
3752            code_challenge,
3753            code_challenge_method: CodeChallengeMethod::S256,
3754        });
3755
3756        //  * response type != code.
3757        let auth_req = AuthorisationRequest {
3758            // We're unlikely to support Implicit Grant
3759            response_type: ResponseType::Token,
3760            response_mode: None,
3761            client_id: "test_resource_server".to_string(),
3762            state: Some("123".to_string()),
3763            pkce_request: pkce_request.clone(),
3764            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
3765            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
3766            nonce: None,
3767            oidc_ext: Default::default(),
3768            max_age: None,
3769            unknown_keys: Default::default(),
3770        };
3771
3772        assert!(
3773            idms_prox_read
3774                .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
3775                .unwrap_err()
3776                == Oauth2Error::UnsupportedResponseType
3777        );
3778
3779        // * No pkce in pkce enforced mode.
3780        let auth_req = AuthorisationRequest {
3781            response_type: ResponseType::Code,
3782            response_mode: None,
3783            client_id: "test_resource_server".to_string(),
3784            state: Some("123".to_string()),
3785            pkce_request: None,
3786            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
3787            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
3788            nonce: None,
3789            oidc_ext: Default::default(),
3790            max_age: None,
3791            unknown_keys: Default::default(),
3792        };
3793
3794        assert!(
3795            idms_prox_read
3796                .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
3797                .unwrap_err()
3798                == Oauth2Error::InvalidRequest
3799        );
3800
3801        //  * invalid rs name
3802        let auth_req = AuthorisationRequest {
3803            response_type: ResponseType::Code,
3804            response_mode: None,
3805            client_id: "NOT A REAL RESOURCE SERVER".to_string(),
3806            state: Some("123".to_string()),
3807            pkce_request: pkce_request.clone(),
3808            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
3809            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
3810            nonce: None,
3811            oidc_ext: Default::default(),
3812            max_age: None,
3813            unknown_keys: Default::default(),
3814        };
3815
3816        assert!(
3817            idms_prox_read
3818                .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
3819                .unwrap_err()
3820                == Oauth2Error::InvalidClientId
3821        );
3822
3823        //  * mismatched origin in the redirect.
3824        let auth_req = AuthorisationRequest {
3825            response_type: ResponseType::Code,
3826            response_mode: None,
3827            client_id: "test_resource_server".to_string(),
3828            state: Some("123".to_string()),
3829            pkce_request: pkce_request.clone(),
3830            redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(),
3831            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
3832            nonce: None,
3833            oidc_ext: Default::default(),
3834            max_age: None,
3835            unknown_keys: Default::default(),
3836        };
3837
3838        assert!(
3839            idms_prox_read
3840                .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
3841                .unwrap_err()
3842                == Oauth2Error::InvalidOrigin
3843        );
3844
3845        // * invalid uri in the redirect
3846        let auth_req = AuthorisationRequest {
3847            response_type: ResponseType::Code,
3848            response_mode: None,
3849            client_id: "test_resource_server".to_string(),
3850            state: Some("123".to_string()),
3851            pkce_request: pkce_request.clone(),
3852            redirect_uri: Url::parse("https://demo.example.com/oauth2/wrong_place").unwrap(),
3853            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
3854            nonce: None,
3855            oidc_ext: Default::default(),
3856            max_age: None,
3857            unknown_keys: Default::default(),
3858        };
3859
3860        assert!(
3861            idms_prox_read
3862                .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
3863                .unwrap_err()
3864                == Oauth2Error::InvalidOrigin
3865        );
3866
3867        // * invalid uri (doesn't match query params)
3868        let auth_req = AuthorisationRequest {
3869            response_type: ResponseType::Code,
3870            response_mode: None,
3871            client_id: "test_resource_server".to_string(),
3872            state: Some("123".to_string()),
3873            pkce_request: pkce_request.clone(),
3874            redirect_uri: Url::parse("https://portal.example.com/?custom=foo&too=many").unwrap(),
3875            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
3876            nonce: None,
3877            oidc_ext: Default::default(),
3878            max_age: None,
3879            unknown_keys: Default::default(),
3880        };
3881
3882        assert!(
3883            idms_prox_read
3884                .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
3885                .unwrap_err()
3886                == Oauth2Error::InvalidOrigin
3887        );
3888
3889        let auth_req = AuthorisationRequest {
3890            response_type: ResponseType::Code,
3891            response_mode: None,
3892            client_id: "test_resource_server".to_string(),
3893            state: Some("123".to_string()),
3894            pkce_request: pkce_request.clone(),
3895            redirect_uri: Url::parse("https://portal.example.com").unwrap(),
3896            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
3897            nonce: None,
3898            oidc_ext: Default::default(),
3899            max_age: None,
3900            unknown_keys: Default::default(),
3901        };
3902
3903        assert!(
3904            idms_prox_read
3905                .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
3906                .unwrap_err()
3907                == Oauth2Error::InvalidOrigin
3908        );
3909
3910        let auth_req = AuthorisationRequest {
3911            response_type: ResponseType::Code,
3912            response_mode: None,
3913            client_id: "test_resource_server".to_string(),
3914            state: Some("123".to_string()),
3915            pkce_request: pkce_request.clone(),
3916            redirect_uri: Url::parse("https://portal.example.com/?wrong=queryparam").unwrap(),
3917            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
3918            nonce: None,
3919            oidc_ext: Default::default(),
3920            max_age: None,
3921            unknown_keys: Default::default(),
3922        };
3923
3924        assert!(
3925            idms_prox_read
3926                .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
3927                .unwrap_err()
3928                == Oauth2Error::InvalidOrigin
3929        );
3930
3931        // Not Authenticated
3932        let auth_req = AuthorisationRequest {
3933            response_type: ResponseType::Code,
3934            response_mode: None,
3935            client_id: "test_resource_server".to_string(),
3936            state: Some("123".to_string()),
3937            pkce_request: pkce_request.clone(),
3938            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
3939            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
3940            nonce: None,
3941            oidc_ext: Default::default(),
3942            max_age: None,
3943            unknown_keys: Default::default(),
3944        };
3945
3946        let req = idms_prox_read
3947            .check_oauth2_authorisation(None, &auth_req, ct)
3948            .unwrap();
3949
3950        assert!(matches!(
3951            req,
3952            AuthoriseResponse::AuthenticationRequired { .. }
3953        ));
3954
3955        // Requested scope is not available
3956        let auth_req = AuthorisationRequest {
3957            response_type: ResponseType::Code,
3958            response_mode: None,
3959            client_id: "test_resource_server".to_string(),
3960            state: Some("123".to_string()),
3961            pkce_request: pkce_request.clone(),
3962            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
3963            scope: btreeset!["invalid_scope".to_string(), "read".to_string()],
3964            nonce: None,
3965            oidc_ext: Default::default(),
3966            max_age: None,
3967            unknown_keys: Default::default(),
3968        };
3969
3970        assert!(
3971            idms_prox_read
3972                .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
3973                .unwrap_err()
3974                == Oauth2Error::AccessDenied
3975        );
3976
3977        // Not a member of the group.
3978        let auth_req = AuthorisationRequest {
3979            response_type: ResponseType::Code,
3980            response_mode: None,
3981            client_id: "test_resource_server".to_string(),
3982            state: Some("123".to_string()),
3983            pkce_request: pkce_request.clone(),
3984            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
3985            scope: btreeset!["openid".to_string(), "read".to_string()],
3986            nonce: None,
3987            oidc_ext: Default::default(),
3988            max_age: None,
3989            unknown_keys: Default::default(),
3990        };
3991
3992        assert!(
3993            idms_prox_read
3994                .check_oauth2_authorisation(Some(&idm_admin_ident), &auth_req, ct)
3995                .unwrap_err()
3996                == Oauth2Error::AccessDenied
3997        );
3998
3999        // Deny Anonymous auth methods
4000        let auth_req = AuthorisationRequest {
4001            response_type: ResponseType::Code,
4002            response_mode: None,
4003            client_id: "test_resource_server".to_string(),
4004            state: Some("123".to_string()),
4005            pkce_request,
4006            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
4007            scope: btreeset!["openid".to_string(), "read".to_string()],
4008            nonce: None,
4009            oidc_ext: Default::default(),
4010            max_age: None,
4011            unknown_keys: Default::default(),
4012        };
4013
4014        assert!(
4015            idms_prox_read
4016                .check_oauth2_authorisation(Some(&anon_ident), &auth_req, ct)
4017                .unwrap_err()
4018                == Oauth2Error::AccessDenied
4019        );
4020    }
4021
4022    #[idm_test]
4023    async fn test_idm_oauth2_invalid_authorisation_permit_requests(
4024        idms: &IdmServer,
4025        _idms_delayed: &mut IdmServerDelayed,
4026    ) {
4027        // Test invalid OAuth2 authorisation states/requests.
4028        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4029        let (_secret, uat, ident, _) =
4030            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
4031
4032        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4033
4034        let mut uat_wrong_session_id = uat.clone();
4035        uat_wrong_session_id.session_id = uuid::Uuid::new_v4();
4036        let ident_wrong_session_id = idms_prox_write
4037            .process_uat_to_identity(&uat_wrong_session_id, ct, Source::Internal)
4038            .expect("Unable to process uat");
4039
4040        let account = idms_prox_write
4041            .target_to_account(UUID_IDM_ADMIN)
4042            .expect("account must exist");
4043        let session_id = uuid::Uuid::new_v4();
4044        let uat2 = account
4045            .to_userauthtoken(
4046                session_id,
4047                SessionScope::ReadWrite,
4048                ct,
4049                &ResolvedAccountPolicy::test_policy(),
4050            )
4051            .expect("Unable to create uat");
4052        let ident2 = idms_prox_write
4053            .process_uat_to_identity(&uat2, ct, Source::Internal)
4054            .expect("Unable to process uat");
4055
4056        assert!(idms_prox_write.commit().is_ok());
4057
4058        // Now start the test
4059
4060        let idms_prox_read = idms.proxy_read().await.unwrap();
4061
4062        let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
4063
4064        let consent_request = good_authorisation_request!(
4065            idms_prox_read,
4066            &ident,
4067            ct,
4068            code_challenge,
4069            OAUTH2_SCOPE_OPENID.to_string()
4070        );
4071
4072        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
4073            unreachable!();
4074        };
4075
4076        drop(idms_prox_read);
4077        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4078
4079        // Invalid permits
4080        //  * expired token, aka past ttl.
4081        assert!(
4082            idms_prox_write
4083                .check_oauth2_authorise_permit(
4084                    &ident,
4085                    &consent_token,
4086                    ct + Duration::from_secs(TOKEN_EXPIRE),
4087                )
4088                .unwrap_err()
4089                == OperationError::CryptographyError
4090        );
4091
4092        //  * incorrect ident
4093        // We get another uat, but for a different user, and we'll introduce these
4094        // inconsistently to cause confusion.
4095
4096        assert!(
4097            idms_prox_write
4098                .check_oauth2_authorise_permit(&ident2, &consent_token, ct,)
4099                .unwrap_err()
4100                == OperationError::InvalidSessionState
4101        );
4102
4103        //  * incorrect session id
4104        assert!(
4105            idms_prox_write
4106                .check_oauth2_authorise_permit(&ident_wrong_session_id, &consent_token, ct,)
4107                .unwrap_err()
4108                == OperationError::InvalidSessionState
4109        );
4110
4111        assert!(idms_prox_write.commit().is_ok());
4112    }
4113
4114    #[idm_test]
4115    async fn test_idm_oauth2_invalid_token_exchange_requests(
4116        idms: &IdmServer,
4117        _idms_delayed: &mut IdmServerDelayed,
4118    ) {
4119        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4120        let (secret, mut uat, ident, _) =
4121            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
4122
4123        // ⚠️  We set the uat expiry time to 5 seconds from TEST_CURRENT_TIME. This
4124        // allows all our other tests to pass, but it means when we specifically put the
4125        // clock forward a fraction, the fernet tokens are still valid, but the uat
4126        // is not.
4127        // IE
4128        //   |---------------------|------------------|
4129        //   TEST_CURRENT_TIME     UAT_EXPIRE         TOKEN_EXPIRE
4130        //
4131        // This lets us check a variety of time based cases.
4132        uat.expiry = Some(
4133            time::OffsetDateTime::UNIX_EPOCH
4134                + Duration::from_secs(TEST_CURRENT_TIME + UAT_EXPIRE - 1),
4135        );
4136
4137        let idms_prox_read = idms.proxy_read().await.unwrap();
4138
4139        // == Setup the authorisation request
4140        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
4141        let consent_request = good_authorisation_request!(
4142            idms_prox_read,
4143            &ident,
4144            ct,
4145            code_challenge,
4146            OAUTH2_SCOPE_OPENID.to_string()
4147        );
4148
4149        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
4150            unreachable!();
4151        };
4152
4153        drop(idms_prox_read);
4154        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4155
4156        // == Manually submit the consent token to the permit for the permit_success
4157        let permit_success = idms_prox_write
4158            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
4159            .expect("Failed to perform OAuth2 permit");
4160
4161        // == Submit the token exchange code.
4162
4163        // Invalid token exchange
4164        //  * invalid client_authz (not base64)
4165        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
4166            code: permit_success.code.clone(),
4167            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
4168            code_verifier: code_verifier.clone(),
4169        }
4170        .into();
4171
4172        let client_authz = ClientAuthInfo::from("not base64");
4173
4174        assert!(
4175            idms_prox_write
4176                .check_oauth2_token_exchange(&client_authz, &token_req, ct)
4177                .unwrap_err()
4178                == Oauth2Error::AuthenticationRequired
4179        );
4180
4181        //  * doesn't have ':'
4182        let client_authz =
4183            general_purpose::STANDARD.encode(format!("test_resource_server {secret}"));
4184        let client_authz = ClientAuthInfo::from(client_authz.as_str());
4185
4186        assert!(
4187            idms_prox_write
4188                .check_oauth2_token_exchange(&client_authz, &token_req, ct)
4189                .unwrap_err()
4190                == Oauth2Error::AuthenticationRequired
4191        );
4192
4193        //  * invalid client_id
4194        let client_authz = ClientAuthInfo::encode_basic("NOT A REAL SERVER", secret.as_str());
4195
4196        assert!(
4197            idms_prox_write
4198                .check_oauth2_token_exchange(&client_authz, &token_req, ct)
4199                .unwrap_err()
4200                == Oauth2Error::AuthenticationRequired
4201        );
4202
4203        //  * valid client_id, but invalid secret
4204        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", "12345");
4205
4206        assert!(
4207            idms_prox_write
4208                .check_oauth2_token_exchange(&client_authz, &token_req, ct)
4209                .unwrap_err()
4210                == Oauth2Error::AuthenticationRequired
4211        );
4212
4213        // ✅ Now the valid client_authz is in place.
4214        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
4215
4216        //  * expired exchange code (took too long)
4217        assert!(
4218            idms_prox_write
4219                .check_oauth2_token_exchange(
4220                    &client_authz,
4221                    &token_req,
4222                    ct + Duration::from_secs(TOKEN_EXPIRE)
4223                )
4224                .unwrap_err()
4225                == Oauth2Error::InvalidRequest
4226        );
4227
4228        /*
4229        //  * incorrect grant_type
4230        // No longer possible due to changes in json api
4231        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
4232            grant_type: "INCORRECT GRANT TYPE".to_string(),
4233            code: permit_success.code.clone(),
4234            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
4235            client_id: None,
4236            client_secret: None,
4237            code_verifier: code_verifier.clone(),
4238        };
4239        assert!(
4240            idms_prox_read
4241                .check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct)
4242                .unwrap_err()
4243                == Oauth2Error::InvalidRequest
4244        );
4245        */
4246
4247        //  * Incorrect redirect uri
4248        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
4249            code: permit_success.code.clone(),
4250            redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(),
4251            code_verifier,
4252        }
4253        .into();
4254        assert!(
4255            idms_prox_write
4256                .check_oauth2_token_exchange(&client_authz, &token_req, ct)
4257                .unwrap_err()
4258                == Oauth2Error::InvalidOrigin
4259        );
4260
4261        //  * code verifier incorrect
4262        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
4263            code: permit_success.code,
4264            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
4265            code_verifier: Some("12345".to_string()),
4266        }
4267        .into();
4268        assert!(
4269            idms_prox_write
4270                .check_oauth2_token_exchange(&client_authz, &token_req, ct)
4271                .unwrap_err()
4272                == Oauth2Error::InvalidRequest
4273        );
4274
4275        assert!(idms_prox_write.commit().is_ok());
4276    }
4277
4278    #[idm_test]
4279    async fn test_idm_oauth2_supplemental_origin_redirect(
4280        idms: &IdmServer,
4281        _idms_delayed: &mut IdmServerDelayed,
4282    ) {
4283        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4284        let (secret, uat, ident, _) =
4285            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
4286
4287        let idms_prox_read = idms.proxy_read().await.unwrap();
4288
4289        // == Setup the authorisation request
4290        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
4291
4292        let redirect_uri = Url::parse("https://portal.example.com/?custom=foo").unwrap();
4293
4294        let auth_req = AuthorisationRequest {
4295            response_type: ResponseType::Code,
4296            response_mode: None,
4297            client_id: "test_resource_server".to_string(),
4298            state: None,
4299            pkce_request: Some(PkceRequest {
4300                code_challenge: code_challenge.clone(),
4301                code_challenge_method: CodeChallengeMethod::S256,
4302            }),
4303            redirect_uri: redirect_uri.clone(),
4304            scope: btreeset![OAUTH2_SCOPE_GROUPS.to_string()],
4305            nonce: Some("abcdef".to_string()),
4306            oidc_ext: Default::default(),
4307            max_age: None,
4308            unknown_keys: Default::default(),
4309        };
4310
4311        let consent_request = idms_prox_read
4312            .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
4313            .expect("OAuth2 authorisation failed");
4314
4315        trace!(?consent_request);
4316
4317        // Should be in the consent phase;
4318        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
4319            unreachable!();
4320        };
4321
4322        // == Manually submit the consent token to the permit for the permit_success
4323        drop(idms_prox_read);
4324        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4325
4326        let permit_success = idms_prox_write
4327            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
4328            .expect("Failed to perform OAuth2 permit");
4329
4330        // Check we are reflecting the CSRF properly.
4331        assert_eq!(permit_success.state.as_deref(), None);
4332
4333        // Assert we followed the redirect uri including the query elements
4334        // we have in the url.
4335        let permit_redirect_uri = permit_success.build_redirect_uri();
4336
4337        assert_eq!(permit_redirect_uri.origin(), redirect_uri.origin());
4338        assert_eq!(permit_redirect_uri.path(), redirect_uri.path());
4339        let query = BTreeMap::from_iter(permit_redirect_uri.query_pairs().into_owned());
4340        // Assert the query pair wasn't changed
4341        assert_eq!(query.get("custom").map(|s| s.as_str()), Some("foo"));
4342
4343        // == Submit the token exchange code.
4344        // ⚠️  This is where we submit a different origin!
4345        let token_req = AccessTokenRequest {
4346            grant_type: GrantTypeReq::AuthorizationCode {
4347                code: permit_success.code,
4348                redirect_uri,
4349                // From the first step.
4350                code_verifier: code_verifier.clone(),
4351            },
4352
4353            client_post_auth: ClientPostAuth {
4354                client_id: Some("test_resource_server".to_string()),
4355                client_secret: Some(secret.clone()),
4356            },
4357        };
4358
4359        let token_response = idms_prox_write
4360            .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
4361            .expect("Failed to perform OAuth2 token exchange");
4362
4363        // 🎉 We got a token! In the future we can then check introspection from this point.
4364        assert_eq!(token_response.token_type, AccessTokenType::Bearer);
4365
4366        assert!(idms_prox_write.commit().is_ok());
4367
4368        // ============================================================================
4369        // Now repeat the test with the app url.
4370
4371        let mut idms_prox_read = idms.proxy_read().await.unwrap();
4372
4373        // Reload the ident since it pins an entry in memory.
4374        let ident = idms_prox_read
4375            .process_uat_to_identity(&uat, ct, Source::Internal)
4376            .expect("Unable to process uat");
4377
4378        let auth_req = AuthorisationRequest {
4379            response_type: ResponseType::Code,
4380            response_mode: None,
4381            client_id: "test_resource_server".to_string(),
4382            state: Some("123".to_string()),
4383            pkce_request: Some(PkceRequest {
4384                code_challenge,
4385                code_challenge_method: CodeChallengeMethod::S256,
4386            }),
4387            redirect_uri: Url::parse("app://cheese").unwrap(),
4388            scope: btreeset![OAUTH2_SCOPE_GROUPS.to_string()],
4389            nonce: Some("abcdef".to_string()),
4390            oidc_ext: Default::default(),
4391            max_age: None,
4392            unknown_keys: Default::default(),
4393        };
4394
4395        let consent_request = idms_prox_read
4396            .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
4397            .expect("OAuth2 authorisation failed");
4398
4399        trace!(?consent_request);
4400
4401        let AuthoriseResponse::Permitted(permit_success) = consent_request else {
4402            unreachable!();
4403        };
4404
4405        // == Manually submit the consent token to the permit for the permit_success
4406        // Check we are reflecting the CSRF properly.
4407        assert_eq!(permit_success.state.as_deref(), Some("123"));
4408
4409        // == Submit the token exchange code.
4410        // ⚠️  This is where we submit a different origin!
4411        let token_req = AccessTokenRequest {
4412            grant_type: GrantTypeReq::AuthorizationCode {
4413                code: permit_success.code,
4414                redirect_uri: Url::parse("app://cheese").unwrap(),
4415                // From the first step.
4416                code_verifier,
4417            },
4418
4419            client_post_auth: ClientPostAuth {
4420                client_id: Some("test_resource_server".to_string()),
4421                client_secret: Some(secret),
4422            },
4423        };
4424
4425        drop(idms_prox_read);
4426        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4427
4428        let token_response = idms_prox_write
4429            .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
4430            .expect("Failed to perform OAuth2 token exchange");
4431
4432        // 🎉 We got a token! In the future we can then check introspection from this point.
4433        assert_eq!(token_response.token_type, AccessTokenType::Bearer);
4434    }
4435
4436    #[idm_test]
4437    async fn test_idm_oauth2_token_introspect(
4438        idms: &IdmServer,
4439        _idms_delayed: &mut IdmServerDelayed,
4440    ) {
4441        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4442        let (secret, _uat, ident, _) =
4443            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
4444        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
4445
4446        let idms_prox_read = idms.proxy_read().await.unwrap();
4447
4448        // == Setup the authorisation request
4449        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
4450        let consent_request = good_authorisation_request!(
4451            idms_prox_read,
4452            &ident,
4453            ct,
4454            code_challenge,
4455            OAUTH2_SCOPE_OPENID.to_string()
4456        );
4457
4458        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
4459            unreachable!();
4460        };
4461
4462        // == Manually submit the consent token to the permit for the permit_success
4463        drop(idms_prox_read);
4464        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4465
4466        let permit_success = idms_prox_write
4467            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
4468            .expect("Failed to perform OAuth2 permit");
4469
4470        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
4471            code: permit_success.code,
4472            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
4473            code_verifier,
4474        }
4475        .into();
4476        let oauth2_token = idms_prox_write
4477            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
4478            .expect("Unable to exchange for OAuth2 token");
4479
4480        assert!(idms_prox_write.commit().is_ok());
4481
4482        // Okay, now we have the token, we can check it works with introspect.
4483        let mut idms_prox_read = idms.proxy_read().await.unwrap();
4484
4485        let intr_request = AccessTokenIntrospectRequest {
4486            token: oauth2_token.access_token,
4487            token_type_hint: None,
4488
4489            client_post_auth: ClientPostAuth::default(),
4490        };
4491        let intr_response = idms_prox_read
4492            .check_oauth2_token_introspect(&client_authz, &intr_request, ct)
4493            .expect("Failed to inspect token");
4494
4495        eprintln!("👉  {intr_response:?}");
4496        assert!(intr_response.active);
4497        assert_eq!(
4498            intr_response.scope,
4499            btreeset!["openid".to_string(), "supplement".to_string()]
4500        );
4501        assert_eq!(
4502            intr_response.client_id.as_deref(),
4503            Some("test_resource_server")
4504        );
4505        assert_eq!(
4506            intr_response.username.as_deref(),
4507            Some("testperson1@example.com")
4508        );
4509        assert_eq!(intr_response.token_type, Some(AccessTokenType::Bearer));
4510        assert_eq!(intr_response.iat, Some(ct.as_secs() as i64));
4511        assert_eq!(intr_response.nbf, Some(ct.as_secs() as i64));
4512
4513        drop(idms_prox_read);
4514        // start a write,
4515
4516        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4517        // Expire the account, should cause introspect to return inactive.
4518        let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_CURRENT_TIME - 1));
4519        let me_inv_m = ModifyEvent::new_internal_invalid(
4520            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
4521            ModifyList::new_list(vec![Modify::Present(Attribute::AccountExpire, v_expire)]),
4522        );
4523        // go!
4524        assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok());
4525        assert!(idms_prox_write.commit().is_ok());
4526
4527        // start a new read
4528        // check again.
4529        let mut idms_prox_read = idms.proxy_read().await.unwrap();
4530        let intr_response = idms_prox_read
4531            .check_oauth2_token_introspect(&client_authz, &intr_request, ct)
4532            .expect("Failed to inspect token");
4533
4534        assert!(!intr_response.active);
4535    }
4536
4537    #[idm_test]
4538    async fn test_idm_oauth2_token_revoke(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
4539        // First, setup to get a token.
4540        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4541        let (secret, _uat, ident, _) =
4542            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
4543        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
4544
4545        let idms_prox_read = idms.proxy_read().await.unwrap();
4546
4547        // == Setup the authorisation request
4548        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
4549        let consent_request = good_authorisation_request!(
4550            idms_prox_read,
4551            &ident,
4552            ct,
4553            code_challenge,
4554            OAUTH2_SCOPE_OPENID.to_string()
4555        );
4556
4557        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
4558            unreachable!();
4559        };
4560
4561        // == Manually submit the consent token to the permit for the permit_success
4562        drop(idms_prox_read);
4563        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4564
4565        let permit_success = idms_prox_write
4566            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
4567            .expect("Failed to perform OAuth2 permit");
4568
4569        // Assert that the consent was submitted
4570        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
4571            code: permit_success.code,
4572            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
4573            code_verifier,
4574        }
4575        .into();
4576        let oauth2_token = idms_prox_write
4577            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
4578            .expect("Unable to exchange for OAuth2 token");
4579
4580        assert!(idms_prox_write.commit().is_ok());
4581
4582        // Okay, now we have the token, we can check behaviours with the revoke interface.
4583
4584        // First, assert it is valid, similar to the introspect api.
4585        let mut idms_prox_read = idms.proxy_read().await.unwrap();
4586        let intr_request = AccessTokenIntrospectRequest {
4587            token: oauth2_token.access_token.clone(),
4588            token_type_hint: None,
4589            client_post_auth: ClientPostAuth::default(),
4590        };
4591        let intr_response = idms_prox_read
4592            .check_oauth2_token_introspect(&client_authz, &intr_request, ct)
4593            .expect("Failed to inspect token");
4594        eprintln!("👉  {intr_response:?}");
4595        assert!(intr_response.active);
4596        drop(idms_prox_read);
4597
4598        // First, the revoke needs basic auth. Provide incorrect auth, and we fail.
4599        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4600
4601        let bad_client_authz = ClientAuthInfo::encode_basic("test_resource_server", "12345");
4602
4603        let revoke_request = TokenRevokeRequest {
4604            token: oauth2_token.access_token.clone(),
4605            token_type_hint: None,
4606            client_post_auth: ClientPostAuth::default(),
4607        };
4608        let e = idms_prox_write
4609            .oauth2_token_revoke(&bad_client_authz, &revoke_request, ct)
4610            .unwrap_err();
4611        assert!(matches!(e, Oauth2Error::AuthenticationRequired));
4612        assert!(idms_prox_write.commit().is_ok());
4613
4614        // Now submit a non-existent/invalid token. Does not affect our tokens validity.
4615        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4616        let revoke_request = TokenRevokeRequest {
4617            token: "this is an invalid token, nothing will happen!".to_string(),
4618            token_type_hint: None,
4619            client_post_auth: ClientPostAuth::default(),
4620        };
4621        let e = idms_prox_write
4622            .oauth2_token_revoke(&client_authz, &revoke_request, ct)
4623            .unwrap_err();
4624        assert!(matches!(e, Oauth2Error::InvalidRequest));
4625        assert!(idms_prox_write.commit().is_ok());
4626
4627        // Check our token is still valid.
4628        let mut idms_prox_read = idms.proxy_read().await.unwrap();
4629        let intr_response = idms_prox_read
4630            .check_oauth2_token_introspect(&client_authz, &intr_request, ct)
4631            .expect("Failed to inspect token");
4632        assert!(intr_response.active);
4633        drop(idms_prox_read);
4634
4635        // Finally revoke it.
4636        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4637        let revoke_request = TokenRevokeRequest {
4638            token: oauth2_token.access_token.clone(),
4639            token_type_hint: None,
4640            client_post_auth: ClientPostAuth::default(),
4641        };
4642        assert!(idms_prox_write
4643            .oauth2_token_revoke(&client_authz, &revoke_request, ct,)
4644            .is_ok());
4645        assert!(idms_prox_write.commit().is_ok());
4646
4647        // Assert it is now invalid.
4648        let mut idms_prox_read = idms.proxy_read().await.unwrap();
4649        let intr_response = idms_prox_read
4650            .check_oauth2_token_introspect(&client_authz, &intr_request, ct)
4651            .expect("Failed to inspect token");
4652
4653        assert!(!intr_response.active);
4654        drop(idms_prox_read);
4655
4656        // Force trim the session and wait for the grace window to pass. The token will be invalidated
4657        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4658        let filt = filter!(f_eq(
4659            Attribute::Uuid,
4660            PartialValue::Uuid(ident.get_uuid().unwrap())
4661        ));
4662        let mut work_set = idms_prox_write
4663            .qs_write
4664            .internal_search_writeable(&filt)
4665            .expect("Failed to perform internal search writeable");
4666        for (_, entry) in work_set.iter_mut() {
4667            let _ = entry.force_trim_ava(Attribute::OAuth2Session);
4668        }
4669        assert!(idms_prox_write
4670            .qs_write
4671            .internal_apply_writable(work_set)
4672            .is_ok());
4673
4674        assert!(idms_prox_write.commit().is_ok());
4675
4676        let mut idms_prox_read = idms.proxy_read().await.unwrap();
4677        // Grace window in effect.
4678        let intr_response = idms_prox_read
4679            .check_oauth2_token_introspect(&client_authz, &intr_request, ct)
4680            .expect("Failed to inspect token");
4681        assert!(intr_response.active);
4682
4683        // Grace window passed, it will now be invalid.
4684        let ct = ct + AUTH_TOKEN_GRACE_WINDOW;
4685        let intr_response = idms_prox_read
4686            .check_oauth2_token_introspect(&client_authz, &intr_request, ct)
4687            .expect("Failed to inspect token");
4688        assert!(!intr_response.active);
4689
4690        drop(idms_prox_read);
4691
4692        // A second invalidation of the token "does nothing".
4693        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4694        let revoke_request = TokenRevokeRequest {
4695            token: oauth2_token.access_token,
4696            token_type_hint: None,
4697            client_post_auth: ClientPostAuth::default(),
4698        };
4699        assert!(idms_prox_write
4700            .oauth2_token_revoke(&client_authz, &revoke_request, ct,)
4701            .is_ok());
4702        assert!(idms_prox_write.commit().is_ok());
4703    }
4704
4705    #[idm_test]
4706    async fn test_idm_oauth2_session_cleanup_post_rs_delete(
4707        idms: &IdmServer,
4708        _idms_delayed: &mut IdmServerDelayed,
4709    ) {
4710        // First, setup to get a token.
4711        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4712        let (secret, _uat, ident, _) =
4713            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
4714        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
4715
4716        let idms_prox_read = idms.proxy_read().await.unwrap();
4717
4718        // == Setup the authorisation request
4719        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
4720        let consent_request = good_authorisation_request!(
4721            idms_prox_read,
4722            &ident,
4723            ct,
4724            code_challenge,
4725            OAUTH2_SCOPE_OPENID.to_string()
4726        );
4727
4728        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
4729            unreachable!();
4730        };
4731
4732        // == Manually submit the consent token to the permit for the permit_success
4733        drop(idms_prox_read);
4734        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4735
4736        let permit_success = idms_prox_write
4737            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
4738            .expect("Failed to perform OAuth2 permit");
4739
4740        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
4741            code: permit_success.code,
4742            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
4743            code_verifier,
4744        }
4745        .into();
4746
4747        let oauth2_token = idms_prox_write
4748            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
4749            .expect("Unable to exchange for OAuth2 token");
4750
4751        let access_token =
4752            JwsCompact::from_str(&oauth2_token.access_token).expect("Invalid Access Token");
4753
4754        let jws_verifier = JwsDangerReleaseWithoutVerify::default();
4755
4756        let reflected_token = jws_verifier
4757            .verify(&access_token)
4758            .unwrap()
4759            .from_json::<OAuth2RFC9068Token<OAuth2RFC9068TokenExtensions>>()
4760            .expect("Failed to access internals of the refresh token");
4761
4762        let session_id = reflected_token.extensions.session_id;
4763
4764        assert!(idms_prox_write.commit().is_ok());
4765
4766        // Process it to ensure the record exists.
4767        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4768
4769        // Check it is now there
4770        let entry = idms_prox_write
4771            .qs_write
4772            .internal_search_uuid(UUID_TESTPERSON_1)
4773            .expect("failed");
4774        let valid = entry
4775            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
4776            .map(|map| map.get(&session_id).is_some())
4777            .unwrap_or(false);
4778        assert!(valid);
4779
4780        // Delete the resource server.
4781
4782        let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
4783            Attribute::Name,
4784            PartialValue::new_iname("test_resource_server")
4785        )));
4786
4787        assert!(idms_prox_write.qs_write.delete(&de).is_ok());
4788
4789        // Assert the session is revoked. This is cleaned up as an artifact of the referential
4790        // integrity plugin. Remember, refint doesn't consider revoked sessions once they are
4791        // revoked.
4792        let entry = idms_prox_write
4793            .qs_write
4794            .internal_search_uuid(UUID_TESTPERSON_1)
4795            .expect("failed");
4796        let revoked = entry
4797            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
4798            .and_then(|sessions| sessions.get(&session_id))
4799            .map(|session| matches!(session.state, SessionState::RevokedAt(_)))
4800            .unwrap_or(false);
4801        assert!(revoked);
4802
4803        assert!(idms_prox_write.commit().is_ok());
4804    }
4805
4806    #[idm_test]
4807    async fn test_idm_oauth2_authorisation_reject(
4808        idms: &IdmServer,
4809        _idms_delayed: &mut IdmServerDelayed,
4810    ) {
4811        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4812        let (_secret, _uat, ident, _) =
4813            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
4814
4815        let ident2 = {
4816            let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
4817            let account = idms_prox_write
4818                .target_to_account(UUID_IDM_ADMIN)
4819                .expect("account must exist");
4820            let session_id = uuid::Uuid::new_v4();
4821            let uat2 = account
4822                .to_userauthtoken(
4823                    session_id,
4824                    SessionScope::ReadWrite,
4825                    ct,
4826                    &ResolvedAccountPolicy::test_policy(),
4827                )
4828                .expect("Unable to create uat");
4829
4830            idms_prox_write
4831                .process_uat_to_identity(&uat2, ct, Source::Internal)
4832                .expect("Unable to process uat")
4833        };
4834
4835        let idms_prox_read = idms.proxy_read().await.unwrap();
4836        let redirect_uri = Url::parse("https://demo.example.com/oauth2/result").unwrap();
4837        let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
4838
4839        // Check reject behaviour
4840        let consent_request = good_authorisation_request!(
4841            idms_prox_read,
4842            &ident,
4843            ct,
4844            code_challenge,
4845            OAUTH2_SCOPE_OPENID.to_string()
4846        );
4847
4848        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
4849            unreachable!();
4850        };
4851
4852        let reject_success = idms_prox_read
4853            .check_oauth2_authorise_reject(&ident, &consent_token, ct)
4854            .expect("Failed to perform OAuth2 reject");
4855
4856        assert_eq!(reject_success.redirect_uri, redirect_uri);
4857
4858        // Too much time past to reject
4859        let past_ct = Duration::from_secs(TEST_CURRENT_TIME + 301);
4860        assert!(
4861            idms_prox_read
4862                .check_oauth2_authorise_reject(&ident, &consent_token, past_ct)
4863                .unwrap_err()
4864                == OperationError::CryptographyError
4865        );
4866
4867        // Invalid consent token
4868        assert_eq!(
4869            idms_prox_read
4870                .check_oauth2_authorise_reject(&ident, "not a token", ct)
4871                .unwrap_err(),
4872            OperationError::CryptographyError
4873        );
4874
4875        // Wrong ident
4876        assert!(
4877            idms_prox_read
4878                .check_oauth2_authorise_reject(&ident2, &consent_token, ct)
4879                .unwrap_err()
4880                == OperationError::InvalidSessionState
4881        );
4882    }
4883
4884    #[idm_test]
4885    async fn test_idm_oauth2_rfc8414_metadata(
4886        idms: &IdmServer,
4887        _idms_delayed: &mut IdmServerDelayed,
4888    ) {
4889        let ct = Duration::from_secs(TEST_CURRENT_TIME);
4890        let (_secret, _uat, _ident, _) =
4891            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
4892
4893        let idms_prox_read = idms.proxy_read().await.unwrap();
4894
4895        // check the discovery end point works as we expect
4896        assert!(
4897            idms_prox_read
4898                .oauth2_rfc8414_metadata("nosuchclient")
4899                .unwrap_err()
4900                == OperationError::NoMatchingEntries
4901        );
4902
4903        let discovery = idms_prox_read
4904            .oauth2_rfc8414_metadata("test_resource_server")
4905            .expect("Failed to get discovery");
4906
4907        assert!(
4908            discovery.issuer
4909                == Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
4910                    .unwrap()
4911        );
4912
4913        assert!(
4914            discovery.authorization_endpoint
4915                == Url::parse("https://idm.example.com/ui/oauth2").unwrap()
4916        );
4917
4918        assert!(
4919            discovery.token_endpoint
4920                == Url::parse(&format!(
4921                    "https://idm.example.com{}",
4922                    uri::OAUTH2_TOKEN_ENDPOINT
4923                ))
4924                .unwrap()
4925        );
4926
4927        assert!(
4928            discovery.jwks_uri
4929                == Some(
4930                    Url::parse(
4931                        "https://idm.example.com/oauth2/openid/test_resource_server/public_key.jwk"
4932                    )
4933                    .unwrap()
4934                )
4935        );
4936
4937        assert!(discovery.registration_endpoint.is_none());
4938
4939        assert!(
4940            discovery.scopes_supported
4941                == Some(vec![
4942                    OAUTH2_SCOPE_GROUPS.to_string(),
4943                    OAUTH2_SCOPE_OPENID.to_string(),
4944                    "supplement".to_string(),
4945                ])
4946        );
4947
4948        assert_eq!(discovery.response_types_supported, vec![ResponseType::Code]);
4949        assert_eq!(
4950            discovery.response_modes_supported,
4951            vec![ResponseMode::Query, ResponseMode::Fragment]
4952        );
4953        assert_eq!(
4954            discovery.grant_types_supported,
4955            vec![GrantType::AuthorisationCode]
4956        );
4957        assert!(
4958            discovery.token_endpoint_auth_methods_supported
4959                == vec![
4960                    TokenEndpointAuthMethod::ClientSecretBasic,
4961                    TokenEndpointAuthMethod::ClientSecretPost
4962                ]
4963        );
4964        assert!(discovery.service_documentation.is_some());
4965
4966        assert!(discovery.ui_locales_supported.is_none());
4967        assert!(discovery.op_policy_uri.is_none());
4968        assert!(discovery.op_tos_uri.is_none());
4969
4970        assert!(
4971            discovery.revocation_endpoint
4972                == Some(
4973                    Url::parse(&format!(
4974                        "https://idm.example.com{OAUTH2_TOKEN_REVOKE_ENDPOINT}"
4975                    ))
4976                    .unwrap()
4977                )
4978        );
4979        assert!(
4980            discovery.revocation_endpoint_auth_methods_supported
4981                == vec![
4982                    TokenEndpointAuthMethod::ClientSecretBasic,
4983                    TokenEndpointAuthMethod::ClientSecretPost
4984                ]
4985        );
4986
4987        assert!(
4988            discovery.introspection_endpoint
4989                == Some(
4990                    Url::parse(&format!(
4991                        "https://idm.example.com{}",
4992                        kanidm_proto::constants::uri::OAUTH2_TOKEN_INTROSPECT_ENDPOINT
4993                    ))
4994                    .unwrap()
4995                )
4996        );
4997        assert!(
4998            discovery.introspection_endpoint_auth_methods_supported
4999                == vec![
5000                    TokenEndpointAuthMethod::ClientSecretBasic,
5001                    TokenEndpointAuthMethod::ClientSecretPost
5002                ]
5003        );
5004        assert!(discovery
5005            .introspection_endpoint_auth_signing_alg_values_supported
5006            .is_none());
5007
5008        assert_eq!(
5009            discovery.code_challenge_methods_supported,
5010            vec![PkceAlg::S256]
5011        )
5012    }
5013
5014    #[idm_test]
5015    async fn test_idm_oauth2_openid_discovery(
5016        idms: &IdmServer,
5017        _idms_delayed: &mut IdmServerDelayed,
5018    ) {
5019        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5020        let (_secret, _uat, _ident, _) =
5021            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
5022
5023        let idms_prox_read = idms.proxy_read().await.unwrap();
5024
5025        // check the discovery end point works as we expect
5026        assert!(
5027            idms_prox_read
5028                .oauth2_openid_discovery("nosuchclient")
5029                .unwrap_err()
5030                == OperationError::NoMatchingEntries
5031        );
5032
5033        assert!(
5034            idms_prox_read
5035                .oauth2_openid_publickey("nosuchclient")
5036                .unwrap_err()
5037                == OperationError::NoMatchingEntries
5038        );
5039
5040        let discovery = idms_prox_read
5041            .oauth2_openid_discovery("test_resource_server")
5042            .expect("Failed to get discovery");
5043
5044        let mut jwkset = idms_prox_read
5045            .oauth2_openid_publickey("test_resource_server")
5046            .expect("Failed to get public key");
5047
5048        let jwk = jwkset.keys.pop().expect("no such jwk");
5049
5050        match jwk {
5051            Jwk::EC { alg, use_, kid, .. } => {
5052                match (
5053                    alg.unwrap(),
5054                    &discovery.id_token_signing_alg_values_supported[0],
5055                ) {
5056                    (JwaAlg::ES256, IdTokenSignAlg::ES256) => {}
5057                    _ => panic!(),
5058                };
5059                assert_eq!(use_.unwrap(), JwkUse::Sig);
5060                assert!(kid.is_some())
5061            }
5062            _ => panic!(),
5063        };
5064
5065        assert!(
5066            discovery.issuer
5067                == Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
5068                    .unwrap()
5069        );
5070
5071        assert!(
5072            discovery.authorization_endpoint
5073                == Url::parse("https://idm.example.com/ui/oauth2").unwrap()
5074        );
5075
5076        assert!(
5077            discovery.token_endpoint == Url::parse("https://idm.example.com/oauth2/token").unwrap()
5078        );
5079
5080        assert!(
5081            discovery.userinfo_endpoint
5082                == Some(
5083                    Url::parse(
5084                        "https://idm.example.com/oauth2/openid/test_resource_server/userinfo"
5085                    )
5086                    .unwrap()
5087                )
5088        );
5089
5090        assert!(
5091            discovery.jwks_uri
5092                == Url::parse(
5093                    "https://idm.example.com/oauth2/openid/test_resource_server/public_key.jwk"
5094                )
5095                .unwrap()
5096        );
5097
5098        assert!(
5099            discovery.scopes_supported
5100                == Some(vec![
5101                    OAUTH2_SCOPE_GROUPS.to_string(),
5102                    OAUTH2_SCOPE_OPENID.to_string(),
5103                    "supplement".to_string(),
5104                ])
5105        );
5106
5107        assert_eq!(discovery.response_types_supported, vec![ResponseType::Code]);
5108        assert_eq!(
5109            discovery.response_modes_supported,
5110            vec![ResponseMode::Query, ResponseMode::Fragment]
5111        );
5112        assert_eq!(
5113            discovery.grant_types_supported,
5114            vec![GrantType::AuthorisationCode]
5115        );
5116        assert_eq!(discovery.subject_types_supported, vec![SubjectType::Public]);
5117        assert_eq!(
5118            discovery.id_token_signing_alg_values_supported,
5119            vec![IdTokenSignAlg::ES256]
5120        );
5121        assert!(discovery.userinfo_signing_alg_values_supported.is_none());
5122        assert!(
5123            discovery.token_endpoint_auth_methods_supported
5124                == vec![
5125                    TokenEndpointAuthMethod::ClientSecretBasic,
5126                    TokenEndpointAuthMethod::ClientSecretPost
5127                ]
5128        );
5129        assert_eq!(
5130            discovery.display_values_supported,
5131            Some(vec![DisplayValue::Page])
5132        );
5133        assert_eq!(discovery.claim_types_supported, vec![ClaimType::Normal]);
5134        assert!(discovery.claims_supported.is_none());
5135        assert!(discovery.service_documentation.is_some());
5136
5137        assert!(discovery.registration_endpoint.is_none());
5138        assert!(discovery.acr_values_supported.is_none());
5139        assert!(discovery.id_token_encryption_alg_values_supported.is_none());
5140        assert!(discovery.id_token_encryption_enc_values_supported.is_none());
5141        assert!(discovery.userinfo_encryption_alg_values_supported.is_none());
5142        assert!(discovery.userinfo_encryption_enc_values_supported.is_none());
5143        assert!(discovery
5144            .request_object_signing_alg_values_supported
5145            .is_none());
5146        assert!(discovery
5147            .request_object_encryption_alg_values_supported
5148            .is_none());
5149        assert!(discovery
5150            .request_object_encryption_enc_values_supported
5151            .is_none());
5152        assert!(discovery
5153            .token_endpoint_auth_signing_alg_values_supported
5154            .is_none());
5155        assert!(discovery.claims_locales_supported.is_none());
5156        assert!(discovery.ui_locales_supported.is_none());
5157        assert!(discovery.op_policy_uri.is_none());
5158        assert!(discovery.op_tos_uri.is_none());
5159        assert!(!discovery.claims_parameter_supported);
5160        assert!(!discovery.request_uri_parameter_supported);
5161        assert!(!discovery.require_request_uri_registration);
5162        assert!(!discovery.request_parameter_supported);
5163        assert_eq!(
5164            discovery.code_challenge_methods_supported,
5165            vec![PkceAlg::S256]
5166        );
5167
5168        // Extensions
5169        assert!(
5170            discovery.revocation_endpoint
5171                == Some(
5172                    Url::parse(&format!(
5173                        "https://idm.example.com{OAUTH2_TOKEN_REVOKE_ENDPOINT}"
5174                    ))
5175                    .unwrap()
5176                )
5177        );
5178        assert!(
5179            discovery.revocation_endpoint_auth_methods_supported
5180                == vec![
5181                    TokenEndpointAuthMethod::ClientSecretBasic,
5182                    TokenEndpointAuthMethod::ClientSecretPost
5183                ]
5184        );
5185
5186        assert!(
5187            discovery.introspection_endpoint
5188                == Some(
5189                    Url::parse(&format!(
5190                        "https://idm.example.com{OAUTH2_TOKEN_INTROSPECT_ENDPOINT}"
5191                    ))
5192                    .unwrap()
5193                )
5194        );
5195        assert!(
5196            discovery.introspection_endpoint_auth_methods_supported
5197                == vec![
5198                    TokenEndpointAuthMethod::ClientSecretBasic,
5199                    TokenEndpointAuthMethod::ClientSecretPost
5200                ]
5201        );
5202        assert!(discovery
5203            .introspection_endpoint_auth_signing_alg_values_supported
5204            .is_none());
5205    }
5206
5207    #[idm_test]
5208    async fn test_idm_oauth2_openid_extensions(
5209        idms: &IdmServer,
5210        _idms_delayed: &mut IdmServerDelayed,
5211    ) {
5212        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5213        let (secret, _uat, ident, _) =
5214            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
5215        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
5216
5217        let idms_prox_read = idms.proxy_read().await.unwrap();
5218
5219        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
5220
5221        let consent_request = good_authorisation_request!(
5222            idms_prox_read,
5223            &ident,
5224            ct,
5225            code_challenge,
5226            OAUTH2_SCOPE_OPENID.to_string()
5227        );
5228
5229        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
5230            unreachable!();
5231        };
5232
5233        // == Manually submit the consent token to the permit for the permit_success
5234        drop(idms_prox_read);
5235        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5236
5237        let permit_success = idms_prox_write
5238            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
5239            .expect("Failed to perform OAuth2 permit");
5240
5241        // == Submit the token exchange code.
5242        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
5243            code: permit_success.code,
5244            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
5245            // From the first step.
5246            code_verifier,
5247        }
5248        .into();
5249
5250        let token_response = idms_prox_write
5251            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
5252            .expect("Failed to perform OAuth2 token exchange");
5253
5254        // 🎉 We got a token!
5255        assert_eq!(token_response.token_type, AccessTokenType::Bearer);
5256
5257        let id_token = token_response.id_token.expect("No id_token in response!");
5258
5259        let access_token =
5260            JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
5261
5262        let refresh_token = token_response
5263            .refresh_token
5264            .as_ref()
5265            .expect("no refresh token was issued")
5266            .clone();
5267
5268        // Get the read txn for inspecting the tokens
5269        assert!(idms_prox_write.commit().is_ok());
5270
5271        let mut idms_prox_read = idms.proxy_read().await.unwrap();
5272
5273        let mut jwkset = idms_prox_read
5274            .oauth2_openid_publickey("test_resource_server")
5275            .expect("Failed to get public key");
5276
5277        let public_jwk = jwkset.keys.pop().expect("no such jwk");
5278
5279        let jws_validator =
5280            JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
5281
5282        let oidc_unverified =
5283            OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
5284
5285        let iat = ct.as_secs() as i64;
5286
5287        let oidc = jws_validator
5288            .verify(&oidc_unverified)
5289            .unwrap()
5290            .verify_exp(iat)
5291            .expect("Failed to verify oidc");
5292
5293        // Are the id_token values what we expect?
5294        assert!(
5295            oidc.iss
5296                == Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
5297                    .unwrap()
5298        );
5299        assert_eq!(oidc.sub, OidcSubject::U(UUID_TESTPERSON_1));
5300        assert_eq!(oidc.aud, "test_resource_server");
5301        assert_eq!(oidc.iat, iat);
5302        assert_eq!(oidc.nbf, Some(iat));
5303        // Previously this was the auth session but it's now inline with the access token expiry.
5304        assert_eq!(oidc.exp, iat + (OAUTH2_ACCESS_TOKEN_EXPIRY as i64));
5305        assert!(oidc.auth_time.is_none());
5306        // Is nonce correctly passed through?
5307        assert_eq!(oidc.nonce, Some("abcdef".to_string()));
5308        assert!(oidc.at_hash.is_none());
5309        assert!(oidc.acr.is_none());
5310        assert!(oidc.amr.is_none());
5311        assert_eq!(oidc.azp, Some("test_resource_server".to_string()));
5312        assert!(oidc.jti.is_some());
5313        if let Some(jti) = &oidc.jti {
5314            assert!(Uuid::from_str(jti).is_ok());
5315        }
5316        assert_eq!(oidc.s_claims.name, Some("Test Person 1".to_string()));
5317        assert_eq!(
5318            oidc.s_claims.preferred_username,
5319            Some("testperson1@example.com".to_string())
5320        );
5321        assert!(
5322            oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
5323        );
5324        assert!(oidc.claims.is_empty());
5325        // Does our access token work with the userinfo endpoint?
5326        // Do the id_token details line up to the userinfo?
5327        let userinfo = idms_prox_read
5328            .oauth2_openid_userinfo("test_resource_server", &access_token, ct)
5329            .expect("failed to get userinfo");
5330
5331        assert_eq!(oidc.iss, userinfo.iss);
5332        assert_eq!(oidc.sub, userinfo.sub);
5333        assert_eq!(oidc.aud, userinfo.aud);
5334        assert_eq!(oidc.iat, userinfo.iat);
5335        assert_eq!(oidc.nbf, userinfo.nbf);
5336        assert_eq!(oidc.exp, userinfo.exp);
5337        assert!(userinfo.auth_time.is_none());
5338        assert_eq!(userinfo.nonce, Some("abcdef".to_string()));
5339        assert!(userinfo.at_hash.is_none());
5340        assert!(userinfo.acr.is_none());
5341        assert_eq!(oidc.amr, userinfo.amr);
5342        assert_eq!(oidc.azp, userinfo.azp);
5343        assert!(userinfo.jti.is_some());
5344        if let Some(jti) = &userinfo.jti {
5345            assert!(Uuid::from_str(jti).is_ok());
5346        }
5347        assert_eq!(oidc.s_claims, userinfo.s_claims);
5348        assert!(userinfo.claims.is_empty());
5349
5350        drop(idms_prox_read);
5351
5352        // Importantly, we need to persist the nonce through access/refresh token operations
5353        // because some clients like the rust openidconnect library require it always for claim
5354        // verification.
5355        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5356
5357        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
5358            refresh_token,
5359            scope: None,
5360        }
5361        .into();
5362
5363        let token_response = idms_prox_write
5364            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
5365            .expect("Unable to exchange for OAuth2 token");
5366
5367        let access_token =
5368            JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
5369
5370        assert!(idms_prox_write.commit().is_ok());
5371
5372        // Okay, refresh done, lets check it.
5373        let mut idms_prox_read = idms.proxy_read().await.unwrap();
5374
5375        let userinfo = idms_prox_read
5376            .oauth2_openid_userinfo("test_resource_server", &access_token, ct)
5377            .expect("failed to get userinfo");
5378
5379        assert_eq!(oidc.iss, userinfo.iss);
5380        assert_eq!(oidc.sub, userinfo.sub);
5381        assert_eq!(oidc.aud, userinfo.aud);
5382        assert_eq!(oidc.iat, userinfo.iat);
5383        assert_eq!(oidc.nbf, userinfo.nbf);
5384        assert_eq!(oidc.exp, userinfo.exp);
5385        assert!(userinfo.auth_time.is_none());
5386        assert_eq!(userinfo.nonce, Some("abcdef".to_string()));
5387        assert!(userinfo.at_hash.is_none());
5388        assert!(userinfo.acr.is_none());
5389        assert_eq!(oidc.amr, userinfo.amr);
5390        assert_eq!(oidc.azp, userinfo.azp);
5391        assert!(userinfo.jti.is_some());
5392        if let Some(jti) = &userinfo.jti {
5393            assert!(Uuid::from_str(jti).is_ok());
5394        }
5395        assert_eq!(oidc.s_claims, userinfo.s_claims);
5396        assert!(userinfo.claims.is_empty());
5397    }
5398
5399    #[idm_test]
5400    async fn test_idm_oauth2_openid_short_username(
5401        idms: &IdmServer,
5402        _idms_delayed: &mut IdmServerDelayed,
5403    ) {
5404        // we run the same test as test_idm_oauth2_openid_extensions()
5405        // but change the preferred_username setting on the RS
5406        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5407        let (secret, _uat, ident, _) =
5408            setup_oauth2_resource_server_basic(idms, ct, true, false, true).await;
5409        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
5410
5411        let idms_prox_read = idms.proxy_read().await.unwrap();
5412
5413        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
5414
5415        let consent_request = good_authorisation_request!(
5416            idms_prox_read,
5417            &ident,
5418            ct,
5419            code_challenge,
5420            OAUTH2_SCOPE_OPENID.to_string()
5421        );
5422
5423        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
5424            unreachable!();
5425        };
5426
5427        // == Manually submit the consent token to the permit for the permit_success
5428        drop(idms_prox_read);
5429        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5430
5431        let permit_success = idms_prox_write
5432            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
5433            .expect("Failed to perform OAuth2 permit");
5434
5435        // == Submit the token exchange code.
5436        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
5437            code: permit_success.code,
5438            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
5439            // From the first step.
5440            code_verifier,
5441        }
5442        .into();
5443
5444        let token_response = idms_prox_write
5445            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
5446            .expect("Failed to perform OAuth2 token exchange");
5447
5448        let id_token = token_response.id_token.expect("No id_token in response!");
5449        let access_token =
5450            JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
5451
5452        assert!(idms_prox_write.commit().is_ok());
5453        let mut idms_prox_read = idms.proxy_read().await.unwrap();
5454
5455        let mut jwkset = idms_prox_read
5456            .oauth2_openid_publickey("test_resource_server")
5457            .expect("Failed to get public key");
5458        let public_jwk = jwkset.keys.pop().expect("no such jwk");
5459
5460        let jws_validator =
5461            JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
5462
5463        let oidc_unverified =
5464            OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
5465
5466        let iat = ct.as_secs() as i64;
5467
5468        let oidc = jws_validator
5469            .verify(&oidc_unverified)
5470            .unwrap()
5471            .verify_exp(iat)
5472            .expect("Failed to verify oidc");
5473
5474        // Do we have the short username in the token claims?
5475        assert_eq!(
5476            oidc.s_claims.preferred_username,
5477            Some("testperson1".to_string())
5478        );
5479        // Do the id_token details line up to the userinfo?
5480        let userinfo = idms_prox_read
5481            .oauth2_openid_userinfo("test_resource_server", &access_token, ct)
5482            .expect("failed to get userinfo");
5483
5484        assert_eq!(oidc.s_claims, userinfo.s_claims);
5485    }
5486
5487    #[idm_test]
5488    async fn test_idm_oauth2_openid_group_claims(
5489        idms: &IdmServer,
5490        _idms_delayed: &mut IdmServerDelayed,
5491    ) {
5492        // we run the same test as test_idm_oauth2_openid_extensions()
5493        // but change the preferred_username setting on the RS
5494        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5495        let (secret, _uat, ident, _) =
5496            setup_oauth2_resource_server_basic(idms, ct, true, false, true).await;
5497
5498        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
5499
5500        let token_response = perform_oauth2_exchange(
5501            idms,
5502            &ident,
5503            ct,
5504            client_authz,
5505            format!("{OAUTH2_SCOPE_OPENID} {OAUTH2_SCOPE_GROUPS}"),
5506        )
5507        .await;
5508
5509        let id_token = token_response.id_token.expect("No id_token in response!");
5510        let access_token =
5511            JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
5512
5513        let oidc = validate_id_token(idms, ct, &id_token).await;
5514
5515        // does our id_token contain the expected groups?
5516        assert!(oidc.claims.contains_key("groups"));
5517
5518        assert!(oidc
5519            .claims
5520            .get("groups")
5521            .expect("unable to find key")
5522            .as_array()
5523            .unwrap()
5524            .contains(&serde_json::json!(STR_UUID_IDM_ALL_ACCOUNTS)));
5525
5526        let mut idms_prox_read = idms.proxy_read().await.unwrap();
5527
5528        // Do the id_token details line up to the userinfo?
5529        let userinfo = idms_prox_read
5530            .oauth2_openid_userinfo("test_resource_server", &access_token, ct)
5531            .expect("failed to get userinfo");
5532
5533        // does the userinfo endpoint provide the same groups?
5534        assert_eq!(oidc.claims.get("groups"), userinfo.claims.get("groups"));
5535    }
5536
5537    #[idm_test]
5538    async fn test_idm_oauth2_openid_group_extended_claims(
5539        idms: &IdmServer,
5540        _idms_delayed: &mut IdmServerDelayed,
5541    ) {
5542        // we run the same test as test_idm_oauth2_openid_extensions()
5543        // but change the preferred_username setting on the RS
5544        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5545        let (secret, _uat, ident, oauth2_client_uuid) =
5546            setup_oauth2_resource_server_basic(idms, ct, true, false, true).await;
5547
5548        // Modify the oauth2 client to have different scope maps.
5549        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5550
5551        let modlist = ModifyList::new_list(vec![
5552            Modify::Removed(
5553                Attribute::OAuth2RsScopeMap,
5554                PartialValue::Refer(UUID_TESTGROUP),
5555            ),
5556            Modify::Present(
5557                Attribute::OAuth2RsScopeMap,
5558                Value::new_oauthscopemap(
5559                    UUID_TESTGROUP,
5560                    btreeset![OAUTH2_SCOPE_GROUPS_NAME.to_string()],
5561                )
5562                .expect("invalid oauthscope"),
5563            ),
5564        ]);
5565
5566        idms_prox_write
5567            .qs_write
5568            .internal_modify_uuid(oauth2_client_uuid, &modlist)
5569            .expect("Failed to modify scopes");
5570
5571        idms_prox_write.commit().expect("failed to commit");
5572
5573        // Now actually do the test.
5574        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
5575
5576        let token_response = perform_oauth2_exchange(
5577            idms,
5578            &ident,
5579            ct,
5580            client_authz,
5581            format!("{OAUTH2_SCOPE_OPENID} {OAUTH2_SCOPE_GROUPS_NAME}"),
5582        )
5583        .await;
5584
5585        let id_token = token_response.id_token.expect("No id_token in response!");
5586        let access_token =
5587            JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
5588
5589        let oidc = validate_id_token(idms, ct, &id_token).await;
5590
5591        // does our id_token contain the expected groups?
5592        assert!(oidc.claims.contains_key("groups"));
5593
5594        assert!(oidc
5595            .claims
5596            .get("groups")
5597            .expect("unable to find key")
5598            .as_array()
5599            .unwrap()
5600            .contains(&serde_json::json!("testgroup")));
5601
5602        let mut idms_prox_read = idms.proxy_read().await.unwrap();
5603
5604        // Do the id_token details line up to the userinfo?
5605        let userinfo = idms_prox_read
5606            .oauth2_openid_userinfo("test_resource_server", &access_token, ct)
5607            .expect("failed to get userinfo");
5608
5609        // does the userinfo endpoint provide the same groups?
5610        assert_eq!(oidc.claims.get("groups"), userinfo.claims.get("groups"));
5611    }
5612
5613    #[idm_test]
5614    async fn test_idm_oauth2_openid_ssh_publickey_claim(
5615        idms: &IdmServer,
5616        _idms_delayed: &mut IdmServerDelayed,
5617    ) {
5618        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5619        let (secret, _uat, ident, client_uuid) =
5620            setup_oauth2_resource_server_basic(idms, ct, true, false, true).await;
5621
5622        // Extra setup for our test - add the correct claim and give an ssh publickey
5623        // to our testperson
5624        const ECDSA_SSH_PUBLIC_KEY: &str = "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGyIY7o3BtOzRiJ9vvjj96bRImwmyy5GvFSIUPlK00HitiAWGhiO1jGZKmK7220Oe4rqU3uAwA00a0758UODs+0OQHLMDRtl81lzPrVSdrYEDldxH9+a86dBZhdm0e15+ODDts2LHUknsJCRRldO4o9R9VrohlF7cbyBlnhJQrR4S+Oag== william@amethyst";
5625        let ssh_pubkey = SshPublicKey::from_string(ECDSA_SSH_PUBLIC_KEY).unwrap();
5626
5627        let scope_set = BTreeSet::from([OAUTH2_SCOPE_SSH_PUBLICKEYS.to_string()]);
5628
5629        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5630
5631        idms_prox_write
5632            .qs_write
5633            .internal_batch_modify(
5634                [
5635                    (
5636                        UUID_TESTPERSON_1,
5637                        ModifyList::new_set(
5638                            Attribute::SshPublicKey,
5639                            ValueSetSshKey::new("label".to_string(), ssh_pubkey),
5640                        ),
5641                    ),
5642                    (
5643                        client_uuid,
5644                        ModifyList::new_set(
5645                            Attribute::OAuth2RsSupScopeMap,
5646                            ValueSetOauthScopeMap::new(UUID_IDM_ALL_ACCOUNTS, scope_set),
5647                        ),
5648                    ),
5649                ]
5650                .into_iter(),
5651            )
5652            .expect("Failed to modify test entries");
5653
5654        assert!(idms_prox_write.commit().is_ok());
5655
5656        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
5657
5658        let idms_prox_read = idms.proxy_read().await.unwrap();
5659
5660        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
5661
5662        let consent_request = good_authorisation_request!(
5663            idms_prox_read,
5664            &ident,
5665            ct,
5666            code_challenge,
5667            "openid groups".to_string()
5668        );
5669
5670        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
5671            unreachable!();
5672        };
5673
5674        // == Manually submit the consent token to the permit for the permit_success
5675        drop(idms_prox_read);
5676        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5677
5678        let permit_success = idms_prox_write
5679            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
5680            .expect("Failed to perform OAuth2 permit");
5681
5682        // == Submit the token exchange code.
5683        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
5684            code: permit_success.code,
5685            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
5686            // From the first step.
5687            code_verifier,
5688        }
5689        .into();
5690
5691        let token_response = idms_prox_write
5692            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
5693            .expect("Failed to perform OAuth2 token exchange");
5694
5695        let id_token = token_response.id_token.expect("No id_token in response!");
5696        let access_token =
5697            JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
5698
5699        assert!(idms_prox_write.commit().is_ok());
5700        let mut idms_prox_read = idms.proxy_read().await.unwrap();
5701
5702        let mut jwkset = idms_prox_read
5703            .oauth2_openid_publickey("test_resource_server")
5704            .expect("Failed to get public key");
5705        let public_jwk = jwkset.keys.pop().expect("no such jwk");
5706
5707        let jws_validator =
5708            JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
5709
5710        let oidc_unverified =
5711            OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
5712
5713        let iat = ct.as_secs() as i64;
5714
5715        let oidc = jws_validator
5716            .verify(&oidc_unverified)
5717            .unwrap()
5718            .verify_exp(iat)
5719            .expect("Failed to verify oidc");
5720
5721        // does our id_token contain the expected groups?
5722        assert!(oidc.claims.contains_key(OAUTH2_SCOPE_SSH_PUBLICKEYS));
5723
5724        assert!(oidc
5725            .claims
5726            .get(OAUTH2_SCOPE_SSH_PUBLICKEYS)
5727            .expect("unable to find key")
5728            .as_array()
5729            .unwrap()
5730            .contains(&serde_json::json!(ECDSA_SSH_PUBLIC_KEY)));
5731
5732        // Do the id_token details line up to the userinfo?
5733        let userinfo = idms_prox_read
5734            .oauth2_openid_userinfo("test_resource_server", &access_token, ct)
5735            .expect("failed to get userinfo");
5736
5737        // does the userinfo endpoint provide the same groups?
5738        assert_eq!(
5739            oidc.claims.get(OAUTH2_SCOPE_SSH_PUBLICKEYS),
5740            userinfo.claims.get(OAUTH2_SCOPE_SSH_PUBLICKEYS)
5741        );
5742    }
5743
5744    //  Check insecure pkce behaviour.
5745    #[idm_test]
5746    async fn test_idm_oauth2_insecure_pkce(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
5747        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5748        let (_secret, _uat, ident, _) =
5749            setup_oauth2_resource_server_basic(idms, ct, false, false, false).await;
5750
5751        let idms_prox_read = idms.proxy_read().await.unwrap();
5752
5753        // == Setup the authorisation request
5754        let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
5755
5756        // Even in disable pkce mode, we will allow pkce
5757        let _consent_request = good_authorisation_request!(
5758            idms_prox_read,
5759            &ident,
5760            ct,
5761            code_challenge,
5762            OAUTH2_SCOPE_OPENID.to_string()
5763        );
5764
5765        // Check we allow none.
5766        let auth_req = AuthorisationRequest {
5767            response_type: ResponseType::Code,
5768            response_mode: None,
5769            client_id: "test_resource_server".to_string(),
5770            state: Some("123".to_string()),
5771            pkce_request: None,
5772            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
5773            scope: btreeset![OAUTH2_SCOPE_GROUPS.to_string()],
5774            nonce: Some("abcdef".to_string()),
5775            oidc_ext: Default::default(),
5776            max_age: None,
5777            unknown_keys: Default::default(),
5778        };
5779
5780        idms_prox_read
5781            .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
5782            .expect("Oauth2 authorisation failed");
5783    }
5784
5785    #[idm_test]
5786    async fn test_idm_oauth2_webfinger(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
5787        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5788        let (_secret, _uat, _ident, _) =
5789            setup_oauth2_resource_server_basic(idms, ct, true, false, true).await;
5790        let mut idms_prox_read = idms.proxy_read().await.unwrap();
5791
5792        let user = "testperson1@example.com";
5793
5794        let webfinger = idms_prox_read
5795            .oauth2_openid_webfinger("test_resource_server", user)
5796            .expect("Failed to get webfinger");
5797
5798        assert_eq!(webfinger.subject, user);
5799        assert_eq!(webfinger.links.len(), 1);
5800
5801        let link = &webfinger.links[0];
5802        assert_eq!(link.rel, "http://openid.net/specs/connect/1.0/issuer");
5803        assert_eq!(
5804            link.href,
5805            "https://idm.example.com/oauth2/openid/test_resource_server"
5806        );
5807
5808        let failed_webfinger = idms_prox_read
5809            .oauth2_openid_webfinger("test_resource_server", "someone@another.domain");
5810        assert!(failed_webfinger.is_err());
5811    }
5812
5813    #[idm_test]
5814    async fn test_idm_oauth2_openid_legacy_crypto(
5815        idms: &IdmServer,
5816        _idms_delayed: &mut IdmServerDelayed,
5817    ) {
5818        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5819        let (secret, _uat, ident, _) =
5820            setup_oauth2_resource_server_basic(idms, ct, false, true, false).await;
5821        let idms_prox_read = idms.proxy_read().await.unwrap();
5822        // The public key url should offer an rs key
5823        // discovery should offer RS256
5824        let discovery = idms_prox_read
5825            .oauth2_openid_discovery("test_resource_server")
5826            .expect("Failed to get discovery");
5827
5828        let mut jwkset = idms_prox_read
5829            .oauth2_openid_publickey("test_resource_server")
5830            .expect("Failed to get public key");
5831
5832        let jwk = jwkset.keys.pop().expect("no such jwk");
5833        let public_jwk = jwk.clone();
5834
5835        match jwk {
5836            Jwk::RSA { alg, use_, kid, .. } => {
5837                match (
5838                    alg.unwrap(),
5839                    &discovery.id_token_signing_alg_values_supported[0],
5840                ) {
5841                    (JwaAlg::RS256, IdTokenSignAlg::RS256) => {}
5842                    _ => panic!(),
5843                };
5844                assert_eq!(use_.unwrap(), JwkUse::Sig);
5845                assert!(kid.is_some());
5846            }
5847            _ => panic!(),
5848        };
5849
5850        // Check that the id_token is signed with the correct key.
5851        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
5852
5853        let consent_request = good_authorisation_request!(
5854            idms_prox_read,
5855            &ident,
5856            ct,
5857            code_challenge,
5858            OAUTH2_SCOPE_OPENID.to_string()
5859        );
5860
5861        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
5862            unreachable!();
5863        };
5864
5865        // == Manually submit the consent token to the permit for the permit_success
5866        drop(idms_prox_read);
5867        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5868
5869        let permit_success = idms_prox_write
5870            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
5871            .expect("Failed to perform OAuth2 permit");
5872
5873        // == Submit the token exchange code.
5874        let token_req = AccessTokenRequest {
5875            grant_type: GrantTypeReq::AuthorizationCode {
5876                code: permit_success.code,
5877                redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
5878                // From the first step.
5879                code_verifier,
5880            },
5881
5882            client_post_auth: ClientPostAuth {
5883                client_id: Some("test_resource_server".to_string()),
5884                client_secret: Some(secret),
5885            },
5886        };
5887
5888        let token_response = idms_prox_write
5889            .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
5890            .expect("Failed to perform OAuth2 token exchange");
5891
5892        // 🎉 We got a token!
5893        assert_eq!(token_response.token_type, AccessTokenType::Bearer);
5894        let id_token = token_response.id_token.expect("No id_token in response!");
5895
5896        let jws_validator =
5897            JwsRs256Verifier::try_from(&public_jwk).expect("failed to build validator");
5898
5899        let oidc_unverified =
5900            OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
5901
5902        let iat = ct.as_secs() as i64;
5903
5904        let oidc = jws_validator
5905            .verify(&oidc_unverified)
5906            .unwrap()
5907            .verify_exp(iat)
5908            .expect("Failed to verify oidc");
5909
5910        assert_eq!(oidc.sub, OidcSubject::U(UUID_TESTPERSON_1));
5911
5912        assert!(idms_prox_write.commit().is_ok());
5913    }
5914
5915    #[idm_test]
5916    async fn test_idm_oauth2_consent_granted_and_changed_workflow(
5917        idms: &IdmServer,
5918        _idms_delayed: &mut IdmServerDelayed,
5919    ) {
5920        let ct = Duration::from_secs(TEST_CURRENT_TIME);
5921        let (_secret, uat, ident, _) =
5922            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
5923
5924        let idms_prox_read = idms.proxy_read().await.unwrap();
5925
5926        let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
5927        let consent_request = good_authorisation_request!(
5928            idms_prox_read,
5929            &ident,
5930            ct,
5931            code_challenge,
5932            OAUTH2_SCOPE_OPENID.to_string()
5933        );
5934
5935        // Should be in the consent phase;
5936        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
5937            unreachable!();
5938        };
5939
5940        // == Manually submit the consent token to the permit for the permit_success
5941        drop(idms_prox_read);
5942        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5943
5944        let _permit_success = idms_prox_write
5945            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
5946            .expect("Failed to perform OAuth2 permit");
5947
5948        assert!(idms_prox_write.commit().is_ok());
5949
5950        // == Now try the authorise again, should be in the permitted state.
5951        let mut idms_prox_read = idms.proxy_read().await.unwrap();
5952
5953        // We need to reload our identity
5954        let ident = idms_prox_read
5955            .process_uat_to_identity(&uat, ct, Source::Internal)
5956            .expect("Unable to process uat");
5957
5958        let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
5959        let consent_request = good_authorisation_request!(
5960            idms_prox_read,
5961            &ident,
5962            ct,
5963            code_challenge,
5964            OAUTH2_SCOPE_OPENID.to_string()
5965        );
5966
5967        // Should be in the consent phase;
5968        let AuthoriseResponse::Permitted(_permit_success) = consent_request else {
5969            unreachable!();
5970        };
5971
5972        drop(idms_prox_read);
5973
5974        // Great! Now change the scopes on the OAuth2 instance, this revokes the permit.
5975        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
5976
5977        let me_extend_scopes = ModifyEvent::new_internal_invalid(
5978            filter!(f_eq(
5979                Attribute::Name,
5980                PartialValue::new_iname("test_resource_server")
5981            )),
5982            ModifyList::new_list(vec![Modify::Present(
5983                Attribute::OAuth2RsScopeMap,
5984                Value::new_oauthscopemap(
5985                    UUID_IDM_ALL_ACCOUNTS,
5986                    btreeset![
5987                        OAUTH2_SCOPE_EMAIL.to_string(),
5988                        OAUTH2_SCOPE_OPENID.to_string()
5989                    ],
5990                )
5991                .expect("invalid oauthscope"),
5992            )]),
5993        );
5994
5995        assert!(idms_prox_write.qs_write.modify(&me_extend_scopes).is_ok());
5996        assert!(idms_prox_write.commit().is_ok());
5997
5998        // And do the workflow once more to see if we need to consent again.
5999
6000        let mut idms_prox_read = idms.proxy_read().await.unwrap();
6001
6002        // We need to reload our identity
6003        let ident = idms_prox_read
6004            .process_uat_to_identity(&uat, ct, Source::Internal)
6005            .expect("Unable to process uat");
6006
6007        let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
6008
6009        let auth_req = AuthorisationRequest {
6010            response_type: ResponseType::Code,
6011            response_mode: None,
6012            client_id: "test_resource_server".to_string(),
6013            state: Some("123".to_string()),
6014            pkce_request: Some(PkceRequest {
6015                code_challenge,
6016                code_challenge_method: CodeChallengeMethod::S256,
6017            }),
6018            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
6019            scope: btreeset!["openid".to_string(), "email".to_string()],
6020            nonce: Some("abcdef".to_string()),
6021            oidc_ext: Default::default(),
6022            max_age: None,
6023            unknown_keys: Default::default(),
6024        };
6025
6026        let consent_request = idms_prox_read
6027            .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
6028            .expect("Oauth2 authorisation failed");
6029
6030        // Should be in the consent phase;
6031        let AuthoriseResponse::ConsentRequested { .. } = consent_request else {
6032            unreachable!();
6033        };
6034
6035        drop(idms_prox_read);
6036
6037        // Success! We had to consent again due to the change :)
6038
6039        // Now change the supplemental scopes on the OAuth2 instance, this revokes the permit.
6040        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6041
6042        let me_extend_scopes = ModifyEvent::new_internal_invalid(
6043            filter!(f_eq(
6044                Attribute::Name,
6045                PartialValue::new_iname("test_resource_server")
6046            )),
6047            ModifyList::new_list(vec![Modify::Present(
6048                Attribute::OAuth2RsSupScopeMap,
6049                Value::new_oauthscopemap(UUID_IDM_ALL_ACCOUNTS, btreeset!["newscope".to_string()])
6050                    .expect("invalid oauthscope"),
6051            )]),
6052        );
6053
6054        assert!(idms_prox_write.qs_write.modify(&me_extend_scopes).is_ok());
6055        assert!(idms_prox_write.commit().is_ok());
6056
6057        // And do the workflow once more to see if we need to consent again.
6058
6059        let mut idms_prox_read = idms.proxy_read().await.unwrap();
6060
6061        // We need to reload our identity
6062        let ident = idms_prox_read
6063            .process_uat_to_identity(&uat, ct, Source::Internal)
6064            .expect("Unable to process uat");
6065
6066        let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
6067
6068        let auth_req = AuthorisationRequest {
6069            response_type: ResponseType::Code,
6070            response_mode: None,
6071            client_id: "test_resource_server".to_string(),
6072            state: Some("123".to_string()),
6073            pkce_request: Some(PkceRequest {
6074                code_challenge,
6075                code_challenge_method: CodeChallengeMethod::S256,
6076            }),
6077            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
6078            // Note the scope isn't requested here!
6079            scope: btreeset!["openid".to_string(), "email".to_string()],
6080            nonce: Some("abcdef".to_string()),
6081            oidc_ext: Default::default(),
6082            max_age: None,
6083            unknown_keys: Default::default(),
6084        };
6085
6086        let consent_request = idms_prox_read
6087            .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
6088            .expect("Oauth2 authorisation failed");
6089
6090        // Should be present in the consent phase however!
6091        let _consent_token = if let AuthoriseResponse::ConsentRequested {
6092            consent_token,
6093            scopes,
6094            ..
6095        } = consent_request
6096        {
6097            assert!(scopes.contains("newscope"));
6098            consent_token
6099        } else {
6100            unreachable!();
6101        };
6102    }
6103
6104    #[idm_test]
6105    async fn test_idm_oauth2_consent_granted_refint_cleanup_on_delete(
6106        idms: &IdmServer,
6107        _idms_delayed: &mut IdmServerDelayed,
6108    ) {
6109        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6110        let (_secret, uat, ident, o2rs_uuid) =
6111            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
6112
6113        // Assert there are no consent maps yet.
6114        assert!(ident.get_oauth2_consent_scopes(o2rs_uuid).is_none());
6115
6116        let idms_prox_read = idms.proxy_read().await.unwrap();
6117
6118        let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
6119        let consent_request = good_authorisation_request!(
6120            idms_prox_read,
6121            &ident,
6122            ct,
6123            code_challenge,
6124            OAUTH2_SCOPE_OPENID.to_string()
6125        );
6126
6127        // Should be in the consent phase;
6128        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
6129            unreachable!();
6130        };
6131
6132        // == Manually submit the consent token to the permit for the permit_success
6133        drop(idms_prox_read);
6134        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6135
6136        let _permit_success = idms_prox_write
6137            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
6138            .expect("Failed to perform OAuth2 permit");
6139
6140        let ident = idms_prox_write
6141            .process_uat_to_identity(&uat, ct, Source::Internal)
6142            .expect("Unable to process uat");
6143
6144        // Assert that the ident now has the consents.
6145        assert!(
6146            ident.get_oauth2_consent_scopes(o2rs_uuid)
6147                == Some(&btreeset![
6148                    OAUTH2_SCOPE_OPENID.to_string(),
6149                    "supplement".to_string()
6150                ])
6151        );
6152
6153        // Now trigger the delete of the RS
6154        let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
6155            Attribute::Name,
6156            PartialValue::new_iname("test_resource_server")
6157        )));
6158
6159        assert!(idms_prox_write.qs_write.delete(&de).is_ok());
6160        // Assert the consent maps are gone.
6161        let ident = idms_prox_write
6162            .process_uat_to_identity(&uat, ct, Source::Internal)
6163            .expect("Unable to process uat");
6164        dbg!(&o2rs_uuid);
6165        dbg!(&ident);
6166        let consent_scopes = ident.get_oauth2_consent_scopes(o2rs_uuid);
6167        dbg!(consent_scopes);
6168        assert!(consent_scopes.is_none());
6169
6170        assert!(idms_prox_write.commit().is_ok());
6171    }
6172
6173    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.8
6174    //
6175    // It was reported we were vulnerable to this attack, but that isn't the case. First
6176    // this attack relies on stripping the *code_challenge* from the internals of the returned
6177    // code exchange token. This isn't possible due to our use of encryption of the code exchange
6178    // token. If that code challenge *could* be removed, then the attacker could use the code exchange
6179    // with no verifier or an incorrect verifier.
6180    //
6181    // Due to the logic in our server, if a code exchange contains a code challenge we always enforce
6182    // it is correctly used!
6183    //
6184    // This left a single odd case where if a client did an authorisation request without a pkce
6185    // verifier, but then a verifier was submitted during the code exchange, that the server would
6186    // *ignore* the verifier parameter. In this case, no stripping of the code challenge was done,
6187    // and the client could have simply also submitted *no* verifier anyway. It could be that
6188    // an attacker could gain a code exchange with no code challenge and then force a victim to
6189    // exchange that code exchange with out the verifier, but I'm not sure what damage that would
6190    // lead to? Regardless, we test for and close off that possible hole in this test.
6191    //
6192    #[idm_test]
6193    async fn test_idm_oauth2_1076_pkce_downgrade(
6194        idms: &IdmServer,
6195        _idms_delayed: &mut IdmServerDelayed,
6196    ) {
6197        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6198        // Enable pkce is set to FALSE
6199        let (secret, _uat, ident, _) =
6200            setup_oauth2_resource_server_basic(idms, ct, false, false, false).await;
6201
6202        let idms_prox_read = idms.proxy_read().await.unwrap();
6203
6204        // Get an ident/uat for now.
6205
6206        // == Setup the authorisation request
6207        // We attempt pkce even though the rs is set to not support pkce.
6208        let (code_verifier, _code_challenge) = create_code_verifier!("Whar Garble");
6209
6210        // First, the user does not request pkce in their exchange.
6211        let auth_req = AuthorisationRequest {
6212            response_type: ResponseType::Code,
6213            response_mode: None,
6214            client_id: "test_resource_server".to_string(),
6215            state: Some("123".to_string()),
6216            pkce_request: None,
6217            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
6218            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
6219            nonce: None,
6220            oidc_ext: Default::default(),
6221            max_age: None,
6222            unknown_keys: Default::default(),
6223        };
6224
6225        let consent_request = idms_prox_read
6226            .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
6227            .expect("Failed to perform OAuth2 authorisation request.");
6228
6229        // Should be in the consent phase;
6230        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
6231            unreachable!();
6232        };
6233
6234        // == Manually submit the consent token to the permit for the permit_success
6235        drop(idms_prox_read);
6236        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6237
6238        let permit_success = idms_prox_write
6239            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
6240            .expect("Failed to perform OAuth2 permit");
6241
6242        // == Submit the token exchange code.
6243        // This exchange failed because we submitted a verifier when the code exchange
6244        // has NO code challenge present.
6245        let token_req = AccessTokenRequest {
6246            grant_type: GrantTypeReq::AuthorizationCode {
6247                code: permit_success.code,
6248                redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
6249                // Note the code verifier is set to "something else"
6250                code_verifier,
6251            },
6252            client_post_auth: ClientPostAuth {
6253                client_id: Some("test_resource_server".to_string()),
6254                client_secret: Some(secret),
6255            },
6256        };
6257
6258        // Assert the exchange fails.
6259        assert!(matches!(
6260            idms_prox_write.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct),
6261            Err(Oauth2Error::InvalidRequest)
6262        ));
6263
6264        assert!(idms_prox_write.commit().is_ok());
6265    }
6266
6267    #[idm_test]
6268    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.1
6269    //
6270    // If the origin configured is https, do not allow downgrading to http on redirect
6271    async fn test_idm_oauth2_redir_http_downgrade(
6272        idms: &IdmServer,
6273        _idms_delayed: &mut IdmServerDelayed,
6274    ) {
6275        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6276        // Enable pkce is set to FALSE
6277        let (secret, _uat, ident, _) =
6278            setup_oauth2_resource_server_basic(idms, ct, false, false, false).await;
6279
6280        let idms_prox_read = idms.proxy_read().await.unwrap();
6281
6282        // Get an ident/uat for now.
6283
6284        // == Setup the authorisation request
6285        // We attempt pkce even though the rs is set to not support pkce.
6286        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
6287
6288        // First, NOTE the lack of https on the redir uri.
6289        let auth_req = AuthorisationRequest {
6290            response_type: ResponseType::Code,
6291            response_mode: None,
6292            client_id: "test_resource_server".to_string(),
6293            state: Some("123".to_string()),
6294            pkce_request: Some(PkceRequest {
6295                code_challenge: code_challenge.clone(),
6296                code_challenge_method: CodeChallengeMethod::S256,
6297            }),
6298            redirect_uri: Url::parse("http://demo.example.com/oauth2/result").unwrap(),
6299            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
6300            nonce: None,
6301            oidc_ext: Default::default(),
6302            max_age: None,
6303            unknown_keys: Default::default(),
6304        };
6305
6306        assert!(
6307            idms_prox_read
6308                .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
6309                .unwrap_err()
6310                == Oauth2Error::InvalidOrigin
6311        );
6312
6313        // This does have https
6314        let consent_request = good_authorisation_request!(
6315            idms_prox_read,
6316            &ident,
6317            ct,
6318            code_challenge,
6319            OAUTH2_SCOPE_OPENID.to_string()
6320        );
6321
6322        // Should be in the consent phase;
6323        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
6324            unreachable!();
6325        };
6326
6327        // == Manually submit the consent token to the permit for the permit_success
6328        drop(idms_prox_read);
6329        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6330
6331        let permit_success = idms_prox_write
6332            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
6333            .expect("Failed to perform OAuth2 permit");
6334
6335        // == Submit the token exchange code.
6336        // NOTE the url is http again
6337        let token_req = AccessTokenRequest {
6338            grant_type: GrantTypeReq::AuthorizationCode {
6339                code: permit_success.code,
6340                redirect_uri: Url::parse("http://demo.example.com/oauth2/result").unwrap(),
6341                // Note the code verifier is set to "something else"
6342                code_verifier,
6343            },
6344
6345            client_post_auth: ClientPostAuth {
6346                client_id: Some("test_resource_server".to_string()),
6347                client_secret: Some(secret),
6348            },
6349        };
6350
6351        // Assert the exchange fails.
6352        assert!(matches!(
6353            idms_prox_write.check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct),
6354            Err(Oauth2Error::InvalidOrigin)
6355        ));
6356
6357        assert!(idms_prox_write.commit().is_ok());
6358    }
6359
6360    async fn setup_refresh_token(
6361        idms: &IdmServer,
6362        _idms_delayed: &mut IdmServerDelayed,
6363        ct: Duration,
6364    ) -> (AccessTokenResponse, ClientAuthInfo) {
6365        // First, setup to get a token.
6366        let (secret, _uat, ident, _) =
6367            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
6368        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
6369
6370        let idms_prox_read = idms.proxy_read().await.unwrap();
6371
6372        // == Setup the authorisation request
6373        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
6374        let consent_request = good_authorisation_request!(
6375            idms_prox_read,
6376            &ident,
6377            ct,
6378            code_challenge,
6379            OAUTH2_SCOPE_OPENID.to_string()
6380        );
6381
6382        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
6383            unreachable!();
6384        };
6385
6386        // == Manually submit the consent token to the permit for the permit_success
6387        drop(idms_prox_read);
6388        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6389
6390        let permit_success = idms_prox_write
6391            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
6392            .expect("Failed to perform OAuth2 permit");
6393
6394        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
6395            code: permit_success.code,
6396            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
6397            code_verifier,
6398        }
6399        .into();
6400        let access_token_response_1 = idms_prox_write
6401            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6402            .expect("Unable to exchange for OAuth2 token");
6403
6404        assert!(idms_prox_write.commit().is_ok());
6405
6406        trace!(?access_token_response_1);
6407
6408        (access_token_response_1, client_authz)
6409    }
6410
6411    #[idm_test]
6412    async fn test_idm_oauth2_refresh_token_basic(
6413        idms: &IdmServer,
6414        idms_delayed: &mut IdmServerDelayed,
6415    ) {
6416        // First, setup to get a token.
6417        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6418
6419        let (access_token_response_1, client_authz) =
6420            setup_refresh_token(idms, idms_delayed, ct).await;
6421
6422        // ============================================
6423        // test basic refresh while access still valid.
6424
6425        let ct = Duration::from_secs(TEST_CURRENT_TIME + 10);
6426        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6427
6428        let refresh_token = access_token_response_1
6429            .refresh_token
6430            .as_ref()
6431            .expect("no refresh token was issued")
6432            .clone();
6433
6434        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6435            refresh_token,
6436            scope: None,
6437        }
6438        .into();
6439
6440        let access_token_response_2 = idms_prox_write
6441            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6442            .expect("Unable to exchange for OAuth2 token");
6443
6444        assert!(idms_prox_write.commit().is_ok());
6445
6446        trace!(?access_token_response_2);
6447
6448        assert!(access_token_response_1.access_token != access_token_response_2.access_token);
6449        assert!(access_token_response_1.refresh_token != access_token_response_2.refresh_token);
6450        assert!(access_token_response_1.id_token != access_token_response_2.id_token);
6451
6452        // ============================================
6453        // test basic refresh after access exp
6454        let ct =
6455            Duration::from_secs(TEST_CURRENT_TIME + 20 + access_token_response_2.expires_in as u64);
6456
6457        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6458
6459        let refresh_token = access_token_response_2
6460            .refresh_token
6461            .as_ref()
6462            .expect("no refresh token was issued")
6463            .clone();
6464
6465        // get the refresh token expiry now before we use it.
6466        let reflected_token = idms_prox_write
6467            .reflect_oauth2_token(&client_authz, &refresh_token)
6468            .expect("Failed to access internals of the refresh token");
6469
6470        let refresh_exp = match reflected_token {
6471            Oauth2TokenType::Refresh { exp, .. } => exp,
6472            // Oauth2TokenType::Access { .. } |
6473            Oauth2TokenType::ClientAccess { .. } => unreachable!(),
6474        };
6475
6476        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6477            refresh_token,
6478            scope: None,
6479        }
6480        .into();
6481
6482        let access_token_response_3 = idms_prox_write
6483            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6484            .expect("Unable to exchange for OAuth2 token");
6485
6486        // Get the user entry to check the session life was extended.
6487
6488        let entry = idms_prox_write
6489            .qs_write
6490            .internal_search_uuid(UUID_TESTPERSON_1)
6491            .expect("failed");
6492        let session = entry
6493            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
6494            .and_then(|sessions| sessions.first_key_value())
6495            // If there is no map, then something is wrong.
6496            .unwrap();
6497
6498        trace!(?session);
6499        // The Oauth2 Session must be updated with a newer session time.
6500        assert_eq!(
6501            SessionState::ExpiresAt(
6502                time::OffsetDateTime::UNIX_EPOCH
6503                    + ct
6504                    + Duration::from_secs(OAUTH_REFRESH_TOKEN_EXPIRY)
6505            ),
6506            session.1.state
6507        );
6508
6509        assert!(idms_prox_write.commit().is_ok());
6510
6511        trace!(?access_token_response_3);
6512
6513        assert!(access_token_response_3.access_token != access_token_response_2.access_token);
6514        assert!(access_token_response_3.refresh_token != access_token_response_2.refresh_token);
6515        assert!(access_token_response_3.id_token != access_token_response_2.id_token);
6516
6517        // refresh after refresh has expired.
6518        // Refresh tokens have a max time limit - the session time limit still bounds it though, but
6519        // so does the refresh token limit. We check both, but the refresh time is checked first so
6520        // we can guarantee this in this test.
6521
6522        let ct = Duration::from_secs(
6523            TEST_CURRENT_TIME + refresh_exp as u64 + access_token_response_3.expires_in as u64,
6524        );
6525
6526        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6527
6528        let refresh_token = access_token_response_3
6529            .refresh_token
6530            .as_ref()
6531            .expect("no refresh token was issued")
6532            .clone();
6533
6534        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6535            refresh_token,
6536            scope: None,
6537        }
6538        .into();
6539        let access_token_response_4 = idms_prox_write
6540            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6541            .unwrap_err();
6542
6543        assert_eq!(access_token_response_4, Oauth2Error::InvalidGrant);
6544
6545        assert!(idms_prox_write.commit().is_ok());
6546    }
6547
6548    // refresh when OAuth2 parent session exp / missing.
6549    #[idm_test]
6550    async fn test_idm_oauth2_refresh_token_oauth2_session_expired(
6551        idms: &IdmServer,
6552        idms_delayed: &mut IdmServerDelayed,
6553    ) {
6554        // First, setup to get a token.
6555        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6556
6557        let (access_token_response_1, client_authz) =
6558            setup_refresh_token(idms, idms_delayed, ct).await;
6559
6560        // ============================================
6561        // Revoke the OAuth2 session
6562
6563        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6564        let revoke_request = TokenRevokeRequest {
6565            token: access_token_response_1.access_token.clone(),
6566            token_type_hint: None,
6567            client_post_auth: ClientPostAuth::default(),
6568        };
6569        assert!(idms_prox_write
6570            .oauth2_token_revoke(&client_authz, &revoke_request, ct,)
6571            .is_ok());
6572        assert!(idms_prox_write.commit().is_ok());
6573
6574        // ============================================
6575        // then attempt a refresh.
6576        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6577
6578        let refresh_token = access_token_response_1
6579            .refresh_token
6580            .as_ref()
6581            .expect("no refresh token was issued")
6582            .clone();
6583
6584        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6585            refresh_token,
6586            scope: None,
6587        }
6588        .into();
6589        let access_token_response_2 = idms_prox_write
6590            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6591            // Should be unable to exchange.
6592            .unwrap_err();
6593
6594        assert_eq!(access_token_response_2, Oauth2Error::InvalidGrant);
6595
6596        assert!(idms_prox_write.commit().is_ok());
6597    }
6598
6599    // refresh with wrong client id/authz
6600    #[idm_test]
6601    async fn test_idm_oauth2_refresh_token_invalid_client_authz(
6602        idms: &IdmServer,
6603        idms_delayed: &mut IdmServerDelayed,
6604    ) {
6605        // First, setup to get a token.
6606        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6607
6608        let (access_token_response_1, _client_authz) =
6609            setup_refresh_token(idms, idms_delayed, ct).await;
6610
6611        let bad_client_authz = ClientAuthInfo::encode_basic("test_resource_server", "12345");
6612
6613        // ============================================
6614        // Refresh with invalid client authz
6615
6616        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6617
6618        let refresh_token = access_token_response_1
6619            .refresh_token
6620            .as_ref()
6621            .expect("no refresh token was issued")
6622            .clone();
6623
6624        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6625            refresh_token,
6626            scope: None,
6627        }
6628        .into();
6629        let access_token_response_2 = idms_prox_write
6630            .check_oauth2_token_exchange(&bad_client_authz, &token_req, ct)
6631            .unwrap_err();
6632
6633        assert_eq!(access_token_response_2, Oauth2Error::AuthenticationRequired);
6634
6635        assert!(idms_prox_write.commit().is_ok());
6636    }
6637
6638    // Incorrect scopes re-requested
6639    #[idm_test]
6640    async fn test_idm_oauth2_refresh_token_inconsistent_scopes(
6641        idms: &IdmServer,
6642        idms_delayed: &mut IdmServerDelayed,
6643    ) {
6644        // First, setup to get a token.
6645        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6646
6647        let (access_token_response_1, client_authz) =
6648            setup_refresh_token(idms, idms_delayed, ct).await;
6649
6650        // ============================================
6651        // Refresh with different scopes
6652
6653        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6654
6655        let refresh_token = access_token_response_1
6656            .refresh_token
6657            .as_ref()
6658            .expect("no refresh token was issued")
6659            .clone();
6660
6661        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6662            refresh_token,
6663            scope: Some(btreeset!["invalid_scope".to_string()]),
6664        }
6665        .into();
6666        let access_token_response_2 = idms_prox_write
6667            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6668            .unwrap_err();
6669
6670        assert_eq!(access_token_response_2, Oauth2Error::InvalidScope);
6671
6672        assert!(idms_prox_write.commit().is_ok());
6673    }
6674
6675    // Test that reuse of a refresh token is denied + terminates the session.
6676    //
6677    // https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-18.html#refresh_token_protection
6678    #[idm_test]
6679    async fn test_idm_oauth2_refresh_token_reuse_invalidates_session(
6680        idms: &IdmServer,
6681        idms_delayed: &mut IdmServerDelayed,
6682    ) {
6683        // First, setup to get a token.
6684        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6685
6686        let (access_token_response_1, client_authz) =
6687            setup_refresh_token(idms, idms_delayed, ct).await;
6688
6689        // ============================================
6690        // Use the refresh token once
6691        let ct = Duration::from_secs(TEST_CURRENT_TIME + 1);
6692        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6693
6694        let refresh_token = access_token_response_1
6695            .refresh_token
6696            .as_ref()
6697            .expect("no refresh token was issued")
6698            .clone();
6699
6700        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6701            refresh_token,
6702            scope: None,
6703        }
6704        .into();
6705
6706        let _access_token_response_2 = idms_prox_write
6707            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6708            .expect("Unable to exchange for OAuth2 token");
6709
6710        assert!(idms_prox_write.commit().is_ok());
6711
6712        // Now use it again. - this will cause an error and the session to be terminated.
6713        let ct = Duration::from_secs(TEST_CURRENT_TIME + 2);
6714        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6715
6716        let refresh_token = access_token_response_1
6717            .refresh_token
6718            .as_ref()
6719            .expect("no refresh token was issued")
6720            .clone();
6721
6722        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6723            refresh_token,
6724            scope: None,
6725        }
6726        .into();
6727
6728        let access_token_response_3 = idms_prox_write
6729            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6730            .unwrap_err();
6731
6732        assert_eq!(access_token_response_3, Oauth2Error::InvalidGrant);
6733
6734        let entry = idms_prox_write
6735            .qs_write
6736            .internal_search_uuid(UUID_TESTPERSON_1)
6737            .expect("failed");
6738        let valid = entry
6739            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
6740            .and_then(|sessions| sessions.first_key_value())
6741            .map(|(_, session)| !matches!(session.state, SessionState::RevokedAt(_)))
6742            // If there is no map, then something is wrong.
6743            .unwrap();
6744        // The session should be invalid at this point.
6745        assert!(!valid);
6746
6747        assert!(idms_prox_write.commit().is_ok());
6748    }
6749
6750    // Test session divergence. This means that we have to:
6751    // access + refresh 1
6752    // use refresh 1 -> access + refresh 2 // don't commit this txn.
6753    // use refresh 2 -> access + refresh 3
6754    //    check the session state.
6755
6756    #[idm_test]
6757    async fn test_idm_oauth2_refresh_token_divergence(
6758        idms: &IdmServer,
6759        idms_delayed: &mut IdmServerDelayed,
6760    ) {
6761        // First, setup to get a token.
6762        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6763
6764        let (access_token_response_1, client_authz) =
6765            setup_refresh_token(idms, idms_delayed, ct).await;
6766
6767        // ============================================
6768        // Use the refresh token once
6769        let ct = Duration::from_secs(TEST_CURRENT_TIME + 1);
6770        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6771
6772        let refresh_token = access_token_response_1
6773            .refresh_token
6774            .as_ref()
6775            .expect("no refresh token was issued")
6776            .clone();
6777
6778        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6779            refresh_token,
6780            scope: None,
6781        }
6782        .into();
6783
6784        let access_token_response_2 = idms_prox_write
6785            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6786            .expect("Unable to exchange for OAuth2 token");
6787
6788        // DO NOT COMMIT HERE - this is what forces the session issued_at
6789        // time to stay at the original time!
6790        drop(idms_prox_write);
6791
6792        // ============================================
6793        let ct = Duration::from_secs(TEST_CURRENT_TIME + 2);
6794        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6795
6796        let refresh_token = access_token_response_2
6797            .refresh_token
6798            .as_ref()
6799            .expect("no refresh token was issued")
6800            .clone();
6801
6802        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6803            refresh_token,
6804            scope: None,
6805        }
6806        .into();
6807
6808        let _access_token_response_3 = idms_prox_write
6809            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6810            .expect("Unable to exchange for OAuth2 token");
6811
6812        assert!(idms_prox_write.commit().is_ok());
6813
6814        // Success!
6815    }
6816
6817    #[idm_test]
6818    async fn test_idm_oauth2_refresh_token_scope_constraints(
6819        idms: &IdmServer,
6820        idms_delayed: &mut IdmServerDelayed,
6821    ) {
6822        // First, setup to get a token.
6823        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6824
6825        let (access_token_response_1, client_authz) =
6826            setup_refresh_token(idms, idms_delayed, ct).await;
6827
6828        // https://www.rfc-editor.org/rfc/rfc6749#section-1.5
6829        // Refresh tokens are issued to the client by the authorization
6830        // server and are used to obtain a new access token when the
6831        // current access token becomes invalid or expires, or to obtain
6832        // additional access tokens with identical or narrower scope
6833        // (access tokens may have a shorter lifetime and fewer
6834        // permissions than authorized by the resource owner).
6835
6836        let ct = Duration::from_secs(TEST_CURRENT_TIME + 10);
6837        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6838
6839        let refresh_token = access_token_response_1
6840            .refresh_token
6841            .as_ref()
6842            .expect("no refresh token was issued")
6843            .clone();
6844
6845        // Get the initial scopes.
6846        let jws_verifier = JwsDangerReleaseWithoutVerify::default();
6847
6848        let access_token_unverified = JwsCompact::from_str(&access_token_response_1.access_token)
6849            .expect("Invalid Access Token");
6850
6851        let reflected_token = jws_verifier
6852            .verify(&access_token_unverified)
6853            .unwrap()
6854            .from_json::<OAuth2RFC9068Token<OAuth2RFC9068TokenExtensions>>()
6855            .expect("Failed to access internals of the refresh token");
6856
6857        trace!(?reflected_token);
6858        let initial_scopes = reflected_token.extensions.scope;
6859        trace!(?initial_scopes);
6860
6861        // Should be the same scopes as initial.
6862        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6863            refresh_token,
6864            scope: None,
6865        }
6866        .into();
6867
6868        let access_token_response_2 = idms_prox_write
6869            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6870            .expect("Unable to exchange for OAuth2 token");
6871
6872        let access_token_unverified = JwsCompact::from_str(&access_token_response_2.access_token)
6873            .expect("Invalid Access Token");
6874
6875        let reflected_token = jws_verifier
6876            .verify(&access_token_unverified)
6877            .unwrap()
6878            .from_json::<OAuth2RFC9068Token<OAuth2RFC9068TokenExtensions>>()
6879            .expect("Failed to access internals of the refresh token");
6880
6881        assert_eq!(initial_scopes, reflected_token.extensions.scope);
6882
6883        let refresh_token = access_token_response_2
6884            .refresh_token
6885            .as_ref()
6886            .expect("no refresh token was issued")
6887            .clone();
6888
6889        // Now the scopes can be constrained.
6890        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6891            refresh_token,
6892            scope: Some(["openid".to_string()].into()),
6893        }
6894        .into();
6895
6896        let access_token_response_3 = idms_prox_write
6897            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6898            .expect("Unable to exchange for OAuth2 token");
6899
6900        let access_token_unverified = JwsCompact::from_str(&access_token_response_3.access_token)
6901            .expect("Invalid Access Token");
6902
6903        let reflected_token = jws_verifier
6904            .verify(&access_token_unverified)
6905            .unwrap()
6906            .from_json::<OAuth2RFC9068Token<OAuth2RFC9068TokenExtensions>>()
6907            .expect("Failed to access internals of the refresh token");
6908
6909        assert_ne!(initial_scopes, reflected_token.extensions.scope);
6910
6911        // Keep the constrained scopes.
6912        let constrained_scopes = reflected_token.extensions.scope;
6913
6914        let refresh_token = access_token_response_3
6915            .refresh_token
6916            .as_ref()
6917            .expect("no refresh token was issued")
6918            .clone();
6919
6920        // No scope request still issues the constrained values.
6921        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6922            refresh_token,
6923            scope: None,
6924        }
6925        .into();
6926
6927        let access_token_response_4 = idms_prox_write
6928            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6929            .expect("Unable to exchange for OAuth2 token");
6930
6931        let access_token_unverified = JwsCompact::from_str(&access_token_response_4.access_token)
6932            .expect("Invalid Access Token");
6933
6934        let reflected_token = jws_verifier
6935            .verify(&access_token_unverified)
6936            .unwrap()
6937            .from_json::<OAuth2RFC9068Token<OAuth2RFC9068TokenExtensions>>()
6938            .expect("Failed to access internals of the refresh token");
6939
6940        assert_ne!(initial_scopes, reflected_token.extensions.scope);
6941        assert_eq!(constrained_scopes, reflected_token.extensions.scope);
6942
6943        let refresh_token = access_token_response_4
6944            .refresh_token
6945            .as_ref()
6946            .expect("no refresh token was issued")
6947            .clone();
6948
6949        // We can't now extend back to the initial scopes.
6950        let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
6951            refresh_token,
6952            scope: Some(initial_scopes),
6953        }
6954        .into();
6955
6956        let access_token_response_5_err = idms_prox_write
6957            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
6958            .unwrap_err();
6959
6960        assert_eq!(access_token_response_5_err, Oauth2Error::InvalidScope);
6961
6962        assert!(idms_prox_write.commit().is_ok());
6963    }
6964
6965    #[test]
6966    // I know this looks kinda dumb but at some point someone pointed out that our scope syntax wasn't compliant with rfc6749
6967    //(https://datatracker.ietf.org/doc/html/rfc6749#section-3.3), so I'm just making sure that we don't break it again.
6968    fn compliant_serialization_test() {
6969        let token_req: Result<AccessTokenRequest, serde_json::Error> = serde_json::from_str(
6970            r#"
6971            {
6972                "grant_type": "refresh_token",
6973                "refresh_token": "some_dumb_refresh_token",
6974                "scope": "invalid_scope vasd asd"
6975            }
6976        "#,
6977        );
6978        assert!(token_req.is_ok());
6979    }
6980
6981    #[idm_test]
6982    async fn test_idm_oauth2_custom_claims(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
6983        let ct = Duration::from_secs(TEST_CURRENT_TIME);
6984        let (secret, _uat, ident, oauth2_rs_uuid) =
6985            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
6986
6987        // Setup custom claim maps here.
6988        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
6989
6990        let modlist = ModifyList::new_list(vec![
6991            // Member of a claim map.
6992            Modify::Present(
6993                Attribute::OAuth2RsClaimMap,
6994                Value::OauthClaimMap(
6995                    "custom_a".to_string(),
6996                    OauthClaimMapJoin::CommaSeparatedValue,
6997                ),
6998            ),
6999            Modify::Present(
7000                Attribute::OAuth2RsClaimMap,
7001                Value::OauthClaimValue(
7002                    "custom_a".to_string(),
7003                    UUID_TESTGROUP,
7004                    btreeset!["value_a".to_string()],
7005                ),
7006            ),
7007            // If you are a member of two groups, the claim maps merge.
7008            Modify::Present(
7009                Attribute::OAuth2RsClaimMap,
7010                Value::OauthClaimValue(
7011                    "custom_a".to_string(),
7012                    UUID_IDM_ALL_ACCOUNTS,
7013                    btreeset!["value_b".to_string()],
7014                ),
7015            ),
7016            // Map with a different separator
7017            Modify::Present(
7018                Attribute::OAuth2RsClaimMap,
7019                Value::OauthClaimMap(
7020                    "custom_b".to_string(),
7021                    OauthClaimMapJoin::SpaceSeparatedValue,
7022                ),
7023            ),
7024            Modify::Present(
7025                Attribute::OAuth2RsClaimMap,
7026                Value::OauthClaimValue(
7027                    "custom_b".to_string(),
7028                    UUID_TESTGROUP,
7029                    btreeset!["value_a".to_string()],
7030                ),
7031            ),
7032            Modify::Present(
7033                Attribute::OAuth2RsClaimMap,
7034                Value::OauthClaimValue(
7035                    "custom_b".to_string(),
7036                    UUID_IDM_ALL_ACCOUNTS,
7037                    btreeset!["value_b".to_string()],
7038                ),
7039            ),
7040            // Not a member of the claim map.
7041            Modify::Present(
7042                Attribute::OAuth2RsClaimMap,
7043                Value::OauthClaimValue(
7044                    "custom_b".to_string(),
7045                    UUID_IDM_ADMINS,
7046                    btreeset!["value_c".to_string()],
7047                ),
7048            ),
7049        ]);
7050
7051        assert!(idms_prox_write
7052            .qs_write
7053            .internal_modify(
7054                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(oauth2_rs_uuid))),
7055                &modlist,
7056            )
7057            .is_ok());
7058
7059        assert!(idms_prox_write.commit().is_ok());
7060
7061        // Claim maps setup, lets go.
7062        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
7063
7064        let idms_prox_read = idms.proxy_read().await.unwrap();
7065
7066        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
7067
7068        let consent_request = good_authorisation_request!(
7069            idms_prox_read,
7070            &ident,
7071            ct,
7072            code_challenge,
7073            OAUTH2_SCOPE_OPENID.to_string()
7074        );
7075
7076        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
7077            unreachable!();
7078        };
7079
7080        // == Manually submit the consent token to the permit for the permit_success
7081        drop(idms_prox_read);
7082        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
7083
7084        let permit_success = idms_prox_write
7085            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
7086            .expect("Failed to perform OAuth2 permit");
7087
7088        // == Submit the token exchange code.
7089        let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
7090            code: permit_success.code,
7091            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
7092            // From the first step.
7093            code_verifier,
7094        }
7095        .into();
7096
7097        let token_response = idms_prox_write
7098            .check_oauth2_token_exchange(&client_authz, &token_req, ct)
7099            .expect("Failed to perform OAuth2 token exchange");
7100
7101        // 🎉 We got a token!
7102        assert_eq!(token_response.token_type, AccessTokenType::Bearer);
7103
7104        let id_token = token_response.id_token.expect("No id_token in response!");
7105        let access_token =
7106            JwsCompact::from_str(&token_response.access_token).expect("Invalid Access Token");
7107
7108        // Get the read txn for inspecting the tokens
7109        assert!(idms_prox_write.commit().is_ok());
7110
7111        let mut idms_prox_read = idms.proxy_read().await.unwrap();
7112
7113        let mut jwkset = idms_prox_read
7114            .oauth2_openid_publickey("test_resource_server")
7115            .expect("Failed to get public key");
7116
7117        let public_jwk = jwkset.keys.pop().expect("no such jwk");
7118
7119        let jws_validator =
7120            JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
7121
7122        let oidc_unverified =
7123            OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
7124
7125        let iat = ct.as_secs() as i64;
7126
7127        let oidc = jws_validator
7128            .verify(&oidc_unverified)
7129            .unwrap()
7130            .verify_exp(iat)
7131            .expect("Failed to verify oidc");
7132
7133        // Are the id_token values what we expect?
7134        assert!(
7135            oidc.iss
7136                == Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
7137                    .unwrap()
7138        );
7139        assert_eq!(oidc.sub, OidcSubject::U(UUID_TESTPERSON_1));
7140        assert_eq!(oidc.aud, "test_resource_server");
7141        assert_eq!(oidc.iat, iat);
7142        assert_eq!(oidc.nbf, Some(iat));
7143        // Previously this was the auth session but it's now inline with the access token expiry.
7144        assert_eq!(oidc.exp, iat + (OAUTH2_ACCESS_TOKEN_EXPIRY as i64));
7145        assert!(oidc.auth_time.is_none());
7146        // Is nonce correctly passed through?
7147        assert_eq!(oidc.nonce, Some("abcdef".to_string()));
7148        assert!(oidc.at_hash.is_none());
7149        assert!(oidc.acr.is_none());
7150        assert!(oidc.amr.is_none());
7151        assert_eq!(oidc.azp, Some("test_resource_server".to_string()));
7152        assert!(oidc.jti.is_some());
7153        if let Some(jti) = &oidc.jti {
7154            assert!(Uuid::from_str(jti).is_ok());
7155        }
7156        assert_eq!(oidc.s_claims.name, Some("Test Person 1".to_string()));
7157        assert_eq!(
7158            oidc.s_claims.preferred_username,
7159            Some("testperson1@example.com".to_string())
7160        );
7161        assert!(
7162            oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
7163        );
7164
7165        assert_eq!(
7166            oidc.claims.get("custom_a").and_then(|v| v.as_str()),
7167            Some("value_a,value_b")
7168        );
7169        assert_eq!(
7170            oidc.claims.get("custom_b").and_then(|v| v.as_str()),
7171            Some("value_a value_b")
7172        );
7173
7174        // Does our access token work with the userinfo endpoint?
7175        // Do the id_token details line up to the userinfo?
7176        let userinfo = idms_prox_read
7177            .oauth2_openid_userinfo("test_resource_server", &access_token, ct)
7178            .expect("failed to get userinfo");
7179
7180        assert_eq!(oidc.iss, userinfo.iss);
7181        assert_eq!(oidc.sub, userinfo.sub);
7182        assert_eq!(oidc.aud, userinfo.aud);
7183        assert_eq!(oidc.iat, userinfo.iat);
7184        assert_eq!(oidc.nbf, userinfo.nbf);
7185        assert_eq!(oidc.exp, userinfo.exp);
7186        assert!(userinfo.auth_time.is_none());
7187        assert_eq!(userinfo.nonce, Some("abcdef".to_string()));
7188        assert!(userinfo.at_hash.is_none());
7189        assert!(userinfo.jti.is_some());
7190        if let Some(jti) = &userinfo.jti {
7191            assert!(Uuid::from_str(jti).is_ok());
7192        }
7193        assert_eq!(oidc.amr, userinfo.amr);
7194        assert_eq!(oidc.azp, userinfo.azp);
7195        assert!(userinfo.jti.is_some());
7196        if let Some(jti) = &userinfo.jti {
7197            assert!(Uuid::from_str(jti).is_ok());
7198        }
7199        assert_eq!(oidc.s_claims, userinfo.s_claims);
7200        assert_eq!(oidc.claims, userinfo.claims);
7201
7202        // Check the oauth2 introspect bits.
7203        let intr_request = AccessTokenIntrospectRequest {
7204            token: token_response.access_token.clone(),
7205            token_type_hint: None,
7206            client_post_auth: ClientPostAuth::default(),
7207        };
7208        let intr_response = idms_prox_read
7209            .check_oauth2_token_introspect(&client_authz, &intr_request, ct)
7210            .expect("Failed to inspect token");
7211
7212        eprintln!("👉  {intr_response:?}");
7213        assert!(intr_response.active);
7214        assert_eq!(
7215            intr_response.scope,
7216            btreeset!["openid".to_string(), "supplement".to_string()]
7217        );
7218        assert_eq!(
7219            intr_response.client_id.as_deref(),
7220            Some("test_resource_server")
7221        );
7222        assert_eq!(
7223            intr_response.username.as_deref(),
7224            Some("testperson1@example.com")
7225        );
7226        assert_eq!(intr_response.token_type, Some(AccessTokenType::Bearer));
7227        assert_eq!(intr_response.iat, Some(ct.as_secs() as i64));
7228        assert_eq!(intr_response.nbf, Some(ct.as_secs() as i64));
7229        // Introspect doesn't have custom claims.
7230
7231        drop(idms_prox_read);
7232    }
7233
7234    #[idm_test]
7235    async fn test_idm_oauth2_public_allow_localhost_redirect(
7236        idms: &IdmServer,
7237        _idms_delayed: &mut IdmServerDelayed,
7238    ) {
7239        let ct = Duration::from_secs(TEST_CURRENT_TIME);
7240        let (_uat, ident, oauth2_rs_uuid) = setup_oauth2_resource_server_public(idms, ct).await;
7241
7242        let mut idms_prox_write: crate::idm::server::IdmServerProxyWriteTransaction<'_> =
7243            idms.proxy_write(ct).await.unwrap();
7244
7245        let redirect_uri = Url::parse("http://localhost:8765/oauth2/result")
7246            .expect("Failed to parse redirect URL");
7247
7248        let modlist = ModifyList::new_list(vec![
7249            Modify::Present(Attribute::OAuth2AllowLocalhostRedirect, Value::Bool(true)),
7250            Modify::Present(Attribute::OAuth2RsOrigin, Value::Url(redirect_uri.clone())),
7251        ]);
7252
7253        assert!(idms_prox_write
7254            .qs_write
7255            .internal_modify(
7256                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(oauth2_rs_uuid))),
7257                &modlist,
7258            )
7259            .is_ok());
7260
7261        assert!(idms_prox_write.commit().is_ok());
7262
7263        let idms_prox_read = idms.proxy_read().await.unwrap();
7264
7265        // == Setup the authorisation request
7266        let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
7267
7268        let auth_req = AuthorisationRequest {
7269            response_type: ResponseType::Code,
7270            response_mode: None,
7271            client_id: "test_resource_server".to_string(),
7272            state: Some("123".to_string()),
7273            pkce_request: Some(PkceRequest {
7274                code_challenge,
7275                code_challenge_method: CodeChallengeMethod::S256,
7276            }),
7277            redirect_uri: Url::parse("http://localhost:8765/oauth2/result").unwrap(),
7278            scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
7279            nonce: Some("abcdef".to_string()),
7280            oidc_ext: Default::default(),
7281            max_age: None,
7282            unknown_keys: Default::default(),
7283        };
7284
7285        let consent_request = idms_prox_read
7286            .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
7287            .expect("OAuth2 authorisation failed");
7288
7289        // Should be in the consent phase;
7290        let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request else {
7291            unreachable!();
7292        };
7293
7294        // == Manually submit the consent token to the permit for the permit_success
7295        drop(idms_prox_read);
7296        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
7297
7298        let permit_success = idms_prox_write
7299            .check_oauth2_authorise_permit(&ident, &consent_token, ct)
7300            .expect("Failed to perform OAuth2 permit");
7301
7302        // Check we are reflecting the CSRF properly.
7303        assert_eq!(permit_success.state.as_deref(), Some("123"));
7304
7305        // == Submit the token exchange code.
7306        let token_req = AccessTokenRequest {
7307            grant_type: GrantTypeReq::AuthorizationCode {
7308                code: permit_success.code,
7309                redirect_uri,
7310                // From the first step.
7311                code_verifier,
7312            },
7313            client_post_auth: ClientPostAuth {
7314                client_id: Some("test_resource_server".to_string()),
7315                client_secret: None,
7316            },
7317        };
7318
7319        let token_response = idms_prox_write
7320            .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
7321            .expect("Failed to perform OAuth2 token exchange");
7322
7323        // 🎉 We got a token! In the future we can then check introspection from this point.
7324        assert_eq!(token_response.token_type, AccessTokenType::Bearer);
7325
7326        assert!(idms_prox_write.commit().is_ok());
7327    }
7328
7329    #[idm_test]
7330    async fn test_idm_oauth2_basic_client_credentials_grant_valid(
7331        idms: &IdmServer,
7332        _idms_delayed: &mut IdmServerDelayed,
7333    ) {
7334        let ct = Duration::from_secs(TEST_CURRENT_TIME);
7335        let (secret, _uat, _ident, _) =
7336            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
7337        let client_authz = ClientAuthInfo::encode_basic("test_resource_server", secret.as_str());
7338
7339        // scope: Some(btreeset!["invalid_scope".to_string()]),
7340        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
7341
7342        let token_req = AccessTokenRequest {
7343            grant_type: GrantTypeReq::ClientCredentials { scope: None },
7344
7345            client_post_auth: ClientPostAuth {
7346                client_id: Some("test_resource_server".to_string()),
7347                client_secret: Some(secret),
7348            },
7349        };
7350
7351        let oauth2_token = idms_prox_write
7352            .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
7353            .expect("Failed to perform OAuth2 token exchange");
7354
7355        assert!(idms_prox_write.commit().is_ok());
7356
7357        // 🎉 We got a token! In the future we can then check introspection from this point.
7358        assert_eq!(oauth2_token.token_type, AccessTokenType::Bearer);
7359
7360        // Check Oauth2 Token Introspection
7361        let mut idms_prox_read = idms.proxy_read().await.unwrap();
7362
7363        let intr_request = AccessTokenIntrospectRequest {
7364            token: oauth2_token.access_token.clone(),
7365            token_type_hint: None,
7366            client_post_auth: ClientPostAuth::default(),
7367        };
7368        let intr_response = idms_prox_read
7369            .check_oauth2_token_introspect(&client_authz, &intr_request, ct)
7370            .expect("Failed to inspect token");
7371
7372        eprintln!("👉  {intr_response:?}");
7373        assert!(intr_response.active);
7374        assert_eq!(intr_response.scope, btreeset!["supplement".to_string()]);
7375        assert_eq!(
7376            intr_response.client_id.as_deref(),
7377            Some("test_resource_server")
7378        );
7379        assert_eq!(
7380            intr_response.username.as_deref(),
7381            Some("test_resource_server@example.com")
7382        );
7383        assert_eq!(intr_response.token_type, Some(AccessTokenType::Bearer));
7384        assert_eq!(intr_response.iat, Some(ct.as_secs() as i64));
7385        assert_eq!(intr_response.nbf, Some(ct.as_secs() as i64));
7386
7387        drop(idms_prox_read);
7388
7389        // Assert we can revoke.
7390        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
7391        let revoke_request = TokenRevokeRequest {
7392            token: oauth2_token.access_token.clone(),
7393            token_type_hint: None,
7394            client_post_auth: ClientPostAuth::default(),
7395        };
7396        assert!(idms_prox_write
7397            .oauth2_token_revoke(&client_authz, &revoke_request, ct,)
7398            .is_ok());
7399        assert!(idms_prox_write.commit().is_ok());
7400
7401        // Now must be invalid.
7402        let ct = ct + AUTH_TOKEN_GRACE_WINDOW;
7403        let mut idms_prox_read = idms.proxy_read().await.unwrap();
7404
7405        let intr_request = AccessTokenIntrospectRequest {
7406            token: oauth2_token.access_token.clone(),
7407            token_type_hint: None,
7408            client_post_auth: ClientPostAuth::default(),
7409        };
7410
7411        let intr_response = idms_prox_read
7412            .check_oauth2_token_introspect(&client_authz, &intr_request, ct)
7413            .expect("Failed to inspect token");
7414        assert!(!intr_response.active);
7415
7416        drop(idms_prox_read);
7417    }
7418
7419    #[idm_test]
7420    async fn test_idm_oauth2_basic_client_credentials_grant_invalid(
7421        idms: &IdmServer,
7422        _idms_delayed: &mut IdmServerDelayed,
7423    ) {
7424        let ct = Duration::from_secs(TEST_CURRENT_TIME);
7425        let (secret, _uat, _ident, _) =
7426            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
7427
7428        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
7429
7430        // Public Client
7431        let token_req = AccessTokenRequest {
7432            grant_type: GrantTypeReq::ClientCredentials { scope: None },
7433            client_post_auth: ClientPostAuth {
7434                client_id: Some("test_resource_server".to_string()),
7435                client_secret: None,
7436            },
7437        };
7438
7439        assert_eq!(
7440            idms_prox_write
7441                .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
7442                .unwrap_err(),
7443            Oauth2Error::AuthenticationRequired
7444        );
7445
7446        // Incorrect Password
7447        let token_req = AccessTokenRequest {
7448            grant_type: GrantTypeReq::ClientCredentials { scope: None },
7449
7450            client_post_auth: ClientPostAuth {
7451                client_id: Some("test_resource_server".to_string()),
7452                client_secret: Some("wrong password".to_string()),
7453            },
7454        };
7455
7456        assert_eq!(
7457            idms_prox_write
7458                .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
7459                .unwrap_err(),
7460            Oauth2Error::AuthenticationRequired
7461        );
7462
7463        // Invalid scope
7464        let scope = Some(btreeset!["💅".to_string()]);
7465        let token_req = AccessTokenRequest {
7466            grant_type: GrantTypeReq::ClientCredentials { scope },
7467
7468            client_post_auth: ClientPostAuth {
7469                client_id: Some("test_resource_server".to_string()),
7470                client_secret: Some(secret.clone()),
7471            },
7472        };
7473
7474        assert_eq!(
7475            idms_prox_write
7476                .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
7477                .unwrap_err(),
7478            Oauth2Error::InvalidScope
7479        );
7480
7481        // Scopes we aren't a member-of
7482        let scope = Some(btreeset!["invalid_scope".to_string()]);
7483        let token_req = AccessTokenRequest {
7484            grant_type: GrantTypeReq::ClientCredentials { scope },
7485
7486            client_post_auth: ClientPostAuth {
7487                client_id: Some("test_resource_server".to_string()),
7488                client_secret: Some(secret.clone()),
7489            },
7490        };
7491
7492        assert_eq!(
7493            idms_prox_write
7494                .check_oauth2_token_exchange(&ClientAuthInfo::none(), &token_req, ct)
7495                .unwrap_err(),
7496            Oauth2Error::AccessDenied
7497        );
7498
7499        assert!(idms_prox_write.commit().is_ok());
7500    }
7501
7502    #[test]
7503    fn test_get_code() {
7504        use super::{gen_device_code, gen_user_code, parse_user_code};
7505
7506        assert!(gen_device_code().is_ok());
7507
7508        let (res_string, res_value) = gen_user_code();
7509
7510        assert!(res_string.split('-').count() == 3);
7511
7512        let res_string_clean = res_string.replace("-", "");
7513        let res_string_as_num = res_string_clean
7514            .parse::<u32>()
7515            .expect("Failed to parse as number");
7516        assert_eq!(res_string_as_num, res_value);
7517
7518        assert_eq!(
7519            parse_user_code(&res_string).expect("Failed to parse code"),
7520            res_value
7521        );
7522    }
7523
7524    #[idm_test]
7525    async fn handle_oauth2_start_device_flow(
7526        idms: &IdmServer,
7527        _idms_delayed: &mut IdmServerDelayed,
7528    ) {
7529        let ct = duration_from_epoch_now();
7530
7531        let client_auth_info = ClientAuthInfo::from(Source::Https(
7532            "127.0.0.1"
7533                .parse()
7534                .expect("Failed to parse 127.0.0.1 as an IP!"),
7535        ));
7536        let eventid = Uuid::new_v4();
7537
7538        let res = idms
7539            .proxy_write(ct)
7540            .await
7541            .expect("Failed to get idmspwt")
7542            .handle_oauth2_start_device_flow(client_auth_info, "test_rs_id", &None, eventid);
7543        dbg!(&res);
7544        assert!(res.is_err());
7545    }
7546
7547    #[test]
7548    fn test_url_localhost_domain() {
7549        // ref #2390 - localhost with ports for OAuth2 redirect_uri
7550
7551        // ensure host_is_local isn't true for a non-local host
7552        let example_is_not_local = "https://example.com/sdfsdf";
7553        println!("Ensuring that {example_is_not_local} is not local");
7554        assert!(!host_is_local(
7555            &Url::parse(example_is_not_local)
7556                .expect("Failed to parse example.com as a host?")
7557                .host()
7558                .unwrap_or_else(|| panic!("Couldn't get a host from {example_is_not_local}"))
7559        ));
7560
7561        let test_urls = [
7562            ("http://localhost:8080/oauth2/callback", "/oauth2/callback"),
7563            ("https://localhost/foo/bar", "/foo/bar"),
7564            ("http://127.0.0.1:12345/foo", "/foo"),
7565            ("http://[::1]:12345/foo", "/foo"),
7566        ];
7567
7568        for (url, path) in test_urls.into_iter() {
7569            println!("Testing URL: {url}");
7570            let url = Url::parse(url).expect("One of the test values failed!");
7571            assert!(host_is_local(
7572                &url.host().expect("Didn't parse a host out?")
7573            ));
7574
7575            assert_eq!(url.path(), path);
7576        }
7577    }
7578
7579    #[test]
7580    fn test_oauth2_rs_type_allow_localhost_redirect() {
7581        let test_cases = [
7582            (
7583                OauthRSType::Public {
7584                    allow_localhost_redirect: true,
7585                },
7586                true,
7587            ),
7588            (
7589                OauthRSType::Public {
7590                    allow_localhost_redirect: false,
7591                },
7592                false,
7593            ),
7594            (
7595                OauthRSType::Basic {
7596                    authz_secret: "supersecret".to_string(),
7597                    enable_pkce: false,
7598                },
7599                false,
7600            ),
7601        ];
7602
7603        assert!(test_cases.iter().all(|(rs_type, expected)| {
7604            let actual = rs_type.allow_localhost_redirect();
7605            println!("Testing {rs_type:?} -> {expected}");
7606            actual == *expected
7607        }));
7608    }
7609
7610    #[idm_test]
7611    async fn test_oauth2_auth_with_no_state(
7612        idms: &IdmServer,
7613        _idms_delayed: &mut IdmServerDelayed,
7614    ) {
7615        let ct = Duration::from_secs(TEST_CURRENT_TIME);
7616        let (_secret, _uat, ident, _) =
7617            setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
7618
7619        let idms_prox_read = idms.proxy_read().await.unwrap();
7620
7621        // == Setup the authorisation request
7622        let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
7623
7624        let scope: BTreeSet<String> = OAUTH2_SCOPE_OPENID
7625            .split(" ")
7626            .map(|s| s.to_string())
7627            .collect();
7628
7629        let auth_req = AuthorisationRequest {
7630            response_type: ResponseType::Code,
7631            response_mode: None,
7632            client_id: "test_resource_server".to_string(),
7633            state: None,
7634            pkce_request: Some(PkceRequest {
7635                code_challenge,
7636                code_challenge_method: CodeChallengeMethod::S256,
7637            }),
7638            redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
7639            scope,
7640            nonce: Some("abcdef".to_string()),
7641            oidc_ext: Default::default(),
7642            max_age: None,
7643            unknown_keys: Default::default(),
7644        };
7645        println!("{auth_req:?}");
7646
7647        let consent_request = idms_prox_read
7648            .check_oauth2_authorisation(Some(&ident), &auth_req, ct)
7649            .expect("OAuth2 authorisation failed");
7650
7651        // Should be in the consent phase;
7652        let AuthoriseResponse::ConsentRequested { .. } = consent_request else {
7653            unreachable!("Expected a ConsentRequested response, got: {consent_request:?}");
7654        };
7655    }
7656}