Skip to main content

kanidm_proto/
oauth2.rs

1//! Oauth2 RFC protocol definitions.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fmt::Display;
5
6use base64::{engine::general_purpose::STANDARD, Engine as _};
7use serde::{Deserialize, Serialize};
8use serde_with::base64::{Base64, UrlSafe};
9use serde_with::formats::SpaceSeparator;
10use serde_with::{
11    formats, rust::deserialize_ignore_any, serde_as, skip_serializing_none, StringWithSeparator,
12};
13use url::Url;
14use uuid::Uuid;
15
16/// How many seconds a device code is valid for.
17pub const OAUTH2_DEVICE_CODE_EXPIRY_SECONDS: u64 = 300;
18/// How often a client device can query the status of the token
19pub const OAUTH2_DEVICE_CODE_INTERVAL_SECONDS: u64 = 5;
20/// Token type URI for OAuth2 access tokens as per RFC8693.
21pub const OAUTH2_TOKEN_TYPE_ACCESS_TOKEN: &str = "urn:ietf:params:oauth:token-type:access_token";
22
23#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
24pub enum CodeChallengeMethod {
25    // default to plain if not requested as S256. Reject the auth?
26    // plain
27    // BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
28    S256,
29}
30
31#[serde_as]
32#[derive(Serialize, Deserialize, Debug, Clone)]
33pub struct PkceRequest {
34    #[serde_as(as = "Base64<UrlSafe, formats::Unpadded>")]
35    pub code_challenge: Vec<u8>,
36    pub code_challenge_method: CodeChallengeMethod,
37}
38
39/// An OAuth2 client redirects to the authorisation server with Authorisation Request
40/// parameters.
41#[serde_as]
42#[skip_serializing_none]
43#[derive(Serialize, Deserialize, Debug, Clone)]
44pub struct AuthorisationRequest {
45    // Must be "code". (or token, see 4.2.1)
46    pub response_type: ResponseType,
47    /// Response mode.
48    ///
49    /// Optional; defaults to `query` for `response_type=code` (Auth Code), and
50    /// `fragment` for `response_type=token` (Implicit Grant, which we probably
51    /// won't support).
52    ///
53    /// Reference:
54    /// [OAuth 2.0 Multiple Response Type Encoding Practices: Response Modes](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes)
55    pub response_mode: Option<ResponseMode>,
56    pub client_id: String,
57    pub state: Option<String>,
58    #[serde(flatten)]
59    pub pkce_request: Option<PkceRequest>,
60    pub redirect_uri: Url,
61    #[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
62    pub scope: BTreeSet<String>,
63    // OIDC adds a nonce parameter that is optional.
64    pub nonce: Option<String>,
65    // OIDC also allows other optional params
66    #[serde(flatten)]
67    pub oidc_ext: AuthorisationRequestOidc,
68    // Needs to be hoisted here due to serde flatten bug #3185
69    pub max_age: Option<i64>,
70    #[serde_as(as = "StringWithSeparator::<SpaceSeparator, Prompt>")]
71    #[serde(default)]
72    pub prompt: Vec<Prompt>,
73
74    #[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
75    #[serde(default)]
76    pub ui_locales: Vec<String>,
77
78    #[serde(flatten)]
79    pub unknown_keys: BTreeMap<String, serde_json::value::Value>,
80}
81
82impl AuthorisationRequest {
83    /// Get the `response_mode` appropriate for this request, taking into
84    /// account defaults from the `response_type` parameter.
85    ///
86    /// Returns `None` if the selection is invalid.
87    ///
88    /// Reference:
89    /// [OAuth 2.0 Multiple Response Type Encoding Practices: Response Modes](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes)
90    pub const fn get_response_mode(&self) -> Option<ResponseMode> {
91        match (self.response_mode, self.response_type) {
92            // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#id_token
93            // The default Response Mode for this Response Type is the fragment
94            // encoding and the query encoding MUST NOT be used.
95            (None, ResponseType::IdToken) => Some(ResponseMode::Fragment),
96            (Some(ResponseMode::Query), ResponseType::IdToken) => None,
97
98            // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
99            (None, ResponseType::Code) => Some(ResponseMode::Query),
100            // https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2
101            (None, ResponseType::Token) => Some(ResponseMode::Fragment),
102
103            // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security
104            // In no case should a set of Authorization Response parameters
105            // whose default Response Mode is the fragment encoding be encoded
106            // using the query encoding.
107            (Some(ResponseMode::Query), ResponseType::Token) => None,
108
109            // Allow others.
110            (Some(m), _) => Some(m),
111        }
112    }
113}
114
115/// An OIDC client redirects to the authorisation server with Authorisation Request
116/// parameters.
117#[skip_serializing_none]
118#[derive(Serialize, Deserialize, Debug, Clone, Default)]
119pub struct AuthorisationRequestOidc {
120    pub display: Option<String>,
121    pub prompt: Option<String>,
122    pub ui_locales: Option<()>,
123    pub claims_locales: Option<()>,
124    pub id_token_hint: Option<String>,
125    pub login_hint: Option<String>,
126    pub acr: Option<String>,
127}
128
129/// In response to an Authorisation request, the user may be prompted to consent to the
130/// scopes requested by the OAuth2 client. If they have previously consented, they will
131/// immediately proceed.
132#[derive(Serialize, Deserialize, Debug, Clone)]
133pub enum AuthorisationResponse {
134    ConsentRequested {
135        // A pretty-name of the client
136        client_name: String,
137        // A list of scopes requested / to be issued.
138        scopes: BTreeSet<String>,
139        // Extra PII that may be requested
140        pii_scopes: BTreeSet<String>,
141        // The users displayname (?)
142        // pub display_name: String,
143        // The token we need to be given back to allow this to proceed
144        consent_token: String,
145    },
146    Permitted,
147}
148
149#[serde_as]
150#[skip_serializing_none]
151#[derive(Serialize, Deserialize, Debug)]
152#[serde(tag = "grant_type", rename_all = "snake_case")]
153pub enum GrantTypeReq {
154    AuthorizationCode {
155        // As sent by the authorisationCode
156        code: String,
157        // Must be the same as the original redirect uri.
158        redirect_uri: Url,
159        code_verifier: Option<String>,
160    },
161    ClientCredentials {
162        #[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
163        scope: Option<BTreeSet<String>>,
164    },
165    RefreshToken {
166        refresh_token: String,
167        #[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
168        scope: Option<BTreeSet<String>>,
169    },
170    #[serde(rename = "urn:ietf:params:oauth:grant-type:token-exchange")]
171    TokenExchange {
172        subject_token: String,
173        subject_token_type: String,
174        requested_token_type: Option<String>,
175        audience: Option<String>,
176        resource: Option<String>,
177        actor_token: Option<String>,
178        actor_token_type: Option<String>,
179        #[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
180        scope: Option<BTreeSet<String>>,
181    },
182    /// ref <https://www.rfc-editor.org/rfc/rfc8628#section-3.4>
183    #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
184    DeviceCode {
185        device_code: String,
186        // #[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
187        scope: Option<BTreeSet<String>>,
188    },
189}
190
191/// An Access Token request. This requires a set of grant-type parameters to satisfy the request.
192#[skip_serializing_none]
193#[derive(Serialize, Deserialize, Debug)]
194pub struct AccessTokenRequest {
195    #[serde(flatten)]
196    pub grant_type: GrantTypeReq,
197    // REQUIRED, if the client is not authenticating with the
198    //  authorization server as described in Section 3.2.1.
199    #[serde(flatten)]
200    pub client_post_auth: ClientPostAuth,
201}
202
203impl From<GrantTypeReq> for AccessTokenRequest {
204    fn from(req: GrantTypeReq) -> AccessTokenRequest {
205        AccessTokenRequest {
206            grant_type: req,
207            client_post_auth: ClientPostAuth::default(),
208        }
209    }
210}
211
212#[derive(Serialize, Debug, Clone, Deserialize)]
213#[skip_serializing_none]
214pub struct OAuth2RFC9068Token<V>
215where
216    V: Clone,
217{
218    /// The issuer of this token
219    pub iss: String,
220    /// Unique id of the subject
221    pub sub: Uuid,
222    /// client_id of the oauth2 rp
223    pub aud: String,
224    /// Expiry in UTC epoch seconds
225    pub exp: i64,
226    /// Not valid before.
227    pub nbf: i64,
228    /// Issued at time.
229    pub iat: i64,
230    /// JWT ID <https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7> - we set it to the session ID
231    pub jti: Uuid,
232    pub client_id: String,
233    #[serde(flatten)]
234    pub extensions: V,
235}
236
237/// Extensions for RFC 9068 Access Token
238#[serde_as]
239#[skip_serializing_none]
240#[derive(Serialize, Deserialize, Debug, Clone)]
241pub struct OAuth2RFC9068TokenExtensions {
242    pub auth_time: Option<i64>,
243    pub acr: Option<String>,
244    pub amr: Option<Vec<String>>,
245
246    #[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
247    pub scope: BTreeSet<String>,
248
249    pub nonce: Option<String>,
250
251    pub session_id: Uuid,
252    pub parent_session_id: Option<Uuid>,
253}
254
255#[derive(Serialize, Deserialize, Debug, PartialEq)]
256pub enum IssuedTokenType {
257    AccessToken,
258    RefreshToken,
259    IdToken,
260    Saml1,
261    Saml2,
262}
263
264/// The response for an access token
265#[serde_as]
266#[skip_serializing_none]
267#[derive(Serialize, Deserialize, Debug)]
268pub struct AccessTokenResponse {
269    pub access_token: String,
270    pub token_type: AccessTokenType,
271    /// Optional RFC8693 issued_token_type.
272    pub issued_token_type: Option<IssuedTokenType>,
273    /// Expiration relative to `now` in seconds.
274    pub expires_in: u32,
275    pub refresh_token: Option<String>,
276    /// Space separated list of scopes that were approved, if this differs from the
277    /// original request.
278    #[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
279    pub scope: BTreeSet<String>,
280    /// If the `openid` scope was requested, an `id_token` may be present in the response.
281    pub id_token: Option<String>,
282}
283
284/// Access token types, per [IANA Registry - OAuth Access Token Types](https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-types)
285#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
286#[serde(try_from = "&str")]
287pub enum AccessTokenType {
288    Bearer,
289    PoP,
290    #[serde(rename = "N_A")]
291    NA,
292    DPoP,
293}
294
295impl TryFrom<&str> for AccessTokenType {
296    type Error = String;
297
298    fn try_from(s: &str) -> Result<Self, Self::Error> {
299        match s.to_lowercase().as_str() {
300            "bearer" => Ok(AccessTokenType::Bearer),
301            "pop" => Ok(AccessTokenType::PoP),
302            "n_a" => Ok(AccessTokenType::NA),
303            "dpop" => Ok(AccessTokenType::DPoP),
304            _ => Err(format!("Unknown AccessTokenType: {s}")),
305        }
306    }
307}
308
309/// Request revocation of an Access or Refresh token. On success the response is OK 200
310/// with no body.
311#[skip_serializing_none]
312#[derive(Serialize, Deserialize, Debug)]
313pub struct TokenRevokeRequest {
314    pub token: String,
315    /// Not required for Kanidm.
316    /// <https://datatracker.ietf.org/doc/html/rfc7009#section-4.1.2>
317    pub token_type_hint: Option<String>,
318
319    #[serde(flatten)]
320    pub client_post_auth: ClientPostAuth,
321}
322
323#[skip_serializing_none]
324#[derive(Serialize, Deserialize, Debug, Default)]
325/// <https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1>
326pub struct ClientPostAuth {
327    pub client_id: Option<String>,
328    pub client_secret: Option<String>,
329}
330
331impl From<(String, Option<String>)> for ClientPostAuth {
332    fn from((client_id, client_secret): (String, Option<String>)) -> Self {
333        ClientPostAuth {
334            client_id: Some(client_id),
335            client_secret,
336        }
337    }
338}
339
340impl From<(&str, Option<&str>)> for ClientPostAuth {
341    fn from((client_id, client_secret): (&str, Option<&str>)) -> Self {
342        ClientPostAuth {
343            client_id: Some(client_id.to_string()),
344            client_secret: client_secret.map(|s| s.to_string()),
345        }
346    }
347}
348
349#[skip_serializing_none]
350#[derive(Serialize, Deserialize, Debug, Default)]
351/// <https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1>
352pub struct ClientAuth {
353    pub client_id: String,
354    pub client_secret: Option<String>,
355}
356
357impl From<(&str, Option<&str>)> for ClientAuth {
358    fn from((client_id, client_secret): (&str, Option<&str>)) -> Self {
359        ClientAuth {
360            client_id: client_id.to_string(),
361            client_secret: client_secret.map(|s| s.to_string()),
362        }
363    }
364}
365
366/// Request to introspect the identity of the account associated to a token.
367#[skip_serializing_none]
368#[derive(Serialize, Deserialize, Debug)]
369pub struct AccessTokenIntrospectRequest {
370    pub token: String,
371    /// Not required for Kanidm.
372    /// <https://datatracker.ietf.org/doc/html/rfc7009#section-4.1.2>
373    pub token_type_hint: Option<String>,
374
375    // For when they want to use POST auth
376    // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
377    #[serde(flatten)]
378    pub client_post_auth: ClientPostAuth,
379}
380
381/// Response to an introspection request. If the token is inactive or revoked, only
382/// `active` will be set to the value of `false`.
383#[serde_as]
384#[skip_serializing_none]
385#[derive(Serialize, Deserialize, Debug)]
386pub struct AccessTokenIntrospectResponse {
387    pub active: bool,
388    #[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
389    pub scope: BTreeSet<String>,
390    pub client_id: Option<String>,
391    pub username: Option<String>,
392    pub token_type: Option<AccessTokenType>,
393    pub exp: Option<i64>,
394    pub iat: Option<i64>,
395    pub nbf: Option<i64>,
396    pub sub: Option<String>,
397    pub aud: Option<String>,
398    pub iss: Option<String>,
399    // JWT ID <https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7> set to session ID
400    pub jti: Uuid,
401}
402
403impl AccessTokenIntrospectResponse {
404    pub fn inactive(session_id: Uuid) -> Self {
405        AccessTokenIntrospectResponse {
406            active: false,
407            scope: BTreeSet::default(),
408            client_id: None,
409            username: None,
410            token_type: None,
411            exp: None,
412            iat: None,
413            nbf: None,
414            sub: None,
415            aud: None,
416            iss: None,
417            jti: session_id,
418        }
419    }
420}
421
422#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
423#[serde(rename_all = "snake_case")]
424pub enum ResponseType {
425    // Auth Code flow
426    // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
427    Code,
428    // Implicit Grant flow
429    // https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
430    Token,
431    // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#id_token
432    IdToken,
433}
434
435#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
436#[serde(rename_all = "snake_case")]
437pub enum ResponseMode {
438    Query,
439    Fragment,
440    FormPost,
441    #[serde(other, deserialize_with = "deserialize_ignore_any")]
442    Invalid,
443}
444
445fn response_modes_supported_default() -> Vec<ResponseMode> {
446    vec![ResponseMode::Query, ResponseMode::Fragment]
447}
448
449#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
450#[serde(rename_all = "snake_case")]
451pub enum Prompt {
452    /// None is not the absence of a value but a rather a value itself.
453    /// Prompt::None signifies to kanidm that *if* the authentications server
454    /// cannot automatically proceed thanks to an already logged in user,
455    /// It must return a error response rather than allowing a user to proceed
456    /// through the regular login flow.
457    ///
458    /// This is specified in OIDC Core 1.0 ยง3.1.2.1
459    /// <https://openid.net/specs/openid-connect-core-1_0.html>
460    None,
461    Login,
462    Consent,
463    SelectAccount,
464    #[serde(untagged)]
465    Invalid(String),
466}
467
468impl Display for Prompt {
469    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470        let s = match self {
471            Prompt::None => "none",
472            Prompt::Login => "login",
473            Prompt::Consent => "consent",
474            Prompt::SelectAccount => "select_account",
475            Prompt::Invalid(str) => &format!("invalid({})", str),
476        };
477        write!(f, "{s}")
478    }
479}
480
481impl std::str::FromStr for Prompt {
482    type Err = std::convert::Infallible;
483
484    fn from_str(s: &str) -> Result<Self, Self::Err> {
485        Ok(match s {
486            "none" => Prompt::None,
487            "login" => Prompt::Login,
488            "consent" => Prompt::Consent,
489            "select_account" => Prompt::SelectAccount,
490            other => Prompt::Invalid(other.to_string()),
491        })
492    }
493}
494
495#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
496#[serde(rename_all = "snake_case")]
497pub enum GrantType {
498    #[serde(rename = "authorization_code")]
499    AuthorisationCode,
500    Implicit,
501    #[serde(rename = "urn:ietf:params:oauth:grant-type:token-exchange")]
502    TokenExchange,
503}
504
505fn grant_types_supported_default() -> Vec<GrantType> {
506    vec![
507        GrantType::AuthorisationCode,
508        GrantType::Implicit,
509        GrantType::TokenExchange,
510    ]
511}
512
513#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
514#[serde(rename_all = "snake_case")]
515pub enum SubjectType {
516    Pairwise,
517    Public,
518}
519
520#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
521pub enum PkceAlg {
522    S256,
523}
524
525#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
526#[serde(rename_all = "UPPERCASE")]
527/// Algorithms supported for token signatures. Prefers `ES256`
528pub enum IdTokenSignAlg {
529    // WE REFUSE TO SUPPORT NONE. DON'T EVEN ASK. IT WON'T HAPPEN.
530    ES256,
531    RS256,
532}
533
534#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
535#[serde(rename_all = "snake_case")]
536pub enum EndpointAuthMethod {
537    None,
538    ClientSecretPost,
539    ClientSecretBasic,
540    ClientSecretJwt,
541    PrivateKeyJwt,
542}
543
544fn token_endpoint_auth_methods_supported_default() -> Vec<EndpointAuthMethod> {
545    vec![EndpointAuthMethod::ClientSecretBasic]
546}
547
548#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
549#[serde(rename_all = "snake_case")]
550pub enum DisplayValue {
551    Page,
552    Popup,
553    Touch,
554    Wap,
555}
556
557#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
558#[serde(rename_all = "snake_case")]
559// https://openid.net/specs/openid-connect-core-1_0.html#ClaimTypes
560pub enum ClaimType {
561    Normal,
562    Aggregated,
563    Distributed,
564}
565
566fn claim_types_supported_default() -> Vec<ClaimType> {
567    vec![ClaimType::Normal]
568}
569
570fn claims_parameter_supported_default() -> bool {
571    false
572}
573
574fn request_parameter_supported_default() -> bool {
575    false
576}
577
578fn request_uri_parameter_supported_default() -> bool {
579    false
580}
581
582fn require_request_uri_parameter_supported_default() -> bool {
583    false
584}
585
586#[derive(Serialize, Deserialize, Debug)]
587pub struct OidcWebfingerRel {
588    pub rel: String,
589    pub href: String,
590}
591
592/// The response to an Webfinger request. Only a subset of the body is defined here.
593/// <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4>
594#[skip_serializing_none]
595#[derive(Serialize, Deserialize, Debug)]
596pub struct OidcWebfingerResponse {
597    pub subject: String,
598    pub links: Vec<OidcWebfingerRel>,
599}
600
601/// The response to an OpenID connect discovery request
602/// <https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata>
603#[skip_serializing_none]
604#[derive(Serialize, Deserialize, Debug)]
605pub struct OidcDiscoveryResponse {
606    pub issuer: Url,
607    pub authorization_endpoint: Url,
608    pub token_endpoint: Url,
609    pub userinfo_endpoint: Option<Url>,
610    pub jwks_uri: Url,
611    pub registration_endpoint: Option<Url>,
612    pub scopes_supported: Option<Vec<String>>,
613    // https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1
614    pub response_types_supported: Vec<ResponseType>,
615    // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
616    #[serde(default = "response_modes_supported_default")]
617    pub response_modes_supported: Vec<ResponseMode>,
618    // Need to fill in as authorization_code only else a default is assumed.
619    #[serde(default = "grant_types_supported_default")]
620    pub grant_types_supported: Vec<GrantType>,
621    pub acr_values_supported: Option<Vec<String>>,
622    // https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
623    pub subject_types_supported: Vec<SubjectType>,
624    pub id_token_signing_alg_values_supported: Vec<IdTokenSignAlg>,
625    pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
626    pub id_token_encryption_enc_values_supported: Option<Vec<String>>,
627    pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
628    pub userinfo_encryption_alg_values_supported: Option<Vec<String>>,
629    pub userinfo_encryption_enc_values_supported: Option<Vec<String>>,
630    pub request_object_signing_alg_values_supported: Option<Vec<String>>,
631    pub request_object_encryption_alg_values_supported: Option<Vec<String>>,
632    pub request_object_encryption_enc_values_supported: Option<Vec<String>>,
633    // Defaults to client_secret_basic
634    #[serde(default = "token_endpoint_auth_methods_supported_default")]
635    pub token_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
636    pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
637    // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
638    pub display_values_supported: Option<Vec<DisplayValue>>,
639    // Default to normal.
640    #[serde(default = "claim_types_supported_default")]
641    pub claim_types_supported: Vec<ClaimType>,
642    pub claims_supported: Option<Vec<String>>,
643    pub service_documentation: Option<Url>,
644    pub claims_locales_supported: Option<Vec<String>>,
645    pub ui_locales_supported: Option<Vec<String>>,
646    // Default false.
647    #[serde(default = "claims_parameter_supported_default")]
648    pub claims_parameter_supported: bool,
649
650    pub op_policy_uri: Option<Url>,
651    pub op_tos_uri: Option<Url>,
652
653    // these are related to RFC9101 JWT-Secured Authorization Request support
654    #[serde(default = "request_parameter_supported_default")]
655    pub request_parameter_supported: bool,
656    #[serde(default = "request_uri_parameter_supported_default")]
657    pub request_uri_parameter_supported: bool,
658    #[serde(default = "require_request_uri_parameter_supported_default")]
659    pub require_request_uri_registration: bool,
660
661    pub code_challenge_methods_supported: Vec<PkceAlg>,
662
663    // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
664    // "content type that contains a set of Claims as its members that are a subset of the Metadata
665    //  values defined in Section 3. Other Claims MAY also be returned. "
666    //
667    // In addition, we also return the following claims in kanidm
668
669    // rfc7009
670    pub revocation_endpoint: Option<Url>,
671    pub revocation_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
672
673    // rfc7662
674    pub introspection_endpoint: Option<Url>,
675    pub introspection_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
676    pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
677
678    /// Ref <https://www.rfc-editor.org/rfc/rfc8628#section-4>
679    pub device_authorization_endpoint: Option<Url>,
680}
681
682/// The response to an OAuth2 rfc8414 metadata request
683#[skip_serializing_none]
684#[derive(Serialize, Deserialize, Debug)]
685pub struct Oauth2Rfc8414MetadataResponse {
686    pub issuer: Url,
687    pub authorization_endpoint: Url,
688    pub token_endpoint: Url,
689
690    pub jwks_uri: Option<Url>,
691
692    // rfc7591 reg endpoint.
693    pub registration_endpoint: Option<Url>,
694
695    pub scopes_supported: Option<Vec<String>>,
696
697    // For Oauth2 should be Code, Token.
698    pub response_types_supported: Vec<ResponseType>,
699    #[serde(default = "response_modes_supported_default")]
700    pub response_modes_supported: Vec<ResponseMode>,
701    #[serde(default = "grant_types_supported_default")]
702    pub grant_types_supported: Vec<GrantType>,
703
704    #[serde(default = "token_endpoint_auth_methods_supported_default")]
705    pub token_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
706
707    pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
708
709    pub service_documentation: Option<Url>,
710    pub ui_locales_supported: Option<Vec<String>>,
711
712    pub op_policy_uri: Option<Url>,
713    pub op_tos_uri: Option<Url>,
714
715    // rfc7009
716    pub revocation_endpoint: Option<Url>,
717    pub revocation_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
718
719    // rfc7662
720    pub introspection_endpoint: Option<Url>,
721    pub introspection_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
722    pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
723
724    // RFC7636
725    pub code_challenge_methods_supported: Vec<PkceAlg>,
726}
727
728#[skip_serializing_none]
729#[derive(Serialize, Deserialize, Debug, Default)]
730pub struct ErrorResponse {
731    pub error: String,
732    pub error_description: Option<String>,
733    pub error_uri: Option<Url>,
734}
735
736#[derive(Debug, Serialize, Deserialize)]
737/// Ref <https://www.rfc-editor.org/rfc/rfc8628#section-3.2>
738pub struct DeviceAuthorizationResponse {
739    /// Base64-encoded bundle of 16 bytes
740    device_code: String,
741    /// xxx-yyy-zzz where x/y/z are digits. Stored internally as a u32 because we'll drop the dashes and parse as a number.
742    user_code: String,
743    verification_uri: Url,
744    verification_uri_complete: Url,
745    expires_in: u64,
746    interval: u64,
747}
748
749impl DeviceAuthorizationResponse {
750    pub fn new(verification_uri: Url, device_code: [u8; 16], user_code: String) -> Self {
751        let mut verification_uri_complete = verification_uri.clone();
752        verification_uri_complete
753            .query_pairs_mut()
754            .append_pair("user_code", &user_code);
755
756        let device_code = STANDARD.encode(device_code);
757
758        Self {
759            verification_uri_complete,
760            device_code,
761            user_code,
762            verification_uri,
763            expires_in: OAUTH2_DEVICE_CODE_EXPIRY_SECONDS,
764            interval: OAUTH2_DEVICE_CODE_INTERVAL_SECONDS,
765        }
766    }
767}
768
769#[cfg(test)]
770mod tests {
771    use super::{AccessTokenRequest, GrantTypeReq, OAUTH2_TOKEN_TYPE_ACCESS_TOKEN};
772    use std::collections::BTreeSet;
773    use url::Url;
774
775    #[test]
776    fn test_oauth2_access_token_req() {
777        let atr: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
778            code: "demo code".to_string(),
779            redirect_uri: Url::parse("http://[::1]").unwrap(),
780            code_verifier: None,
781        }
782        .into();
783
784        println!("{:?}", serde_json::to_string(&atr).expect("JSON failure"));
785    }
786
787    #[test]
788    fn test_oauth2_access_token_type_serde() {
789        for testcase in ["bearer", "Bearer", "BeArEr"] {
790            let at: super::AccessTokenType =
791                serde_json::from_str(&format!("\"{testcase}\"")).expect("Failed to parse");
792            assert_eq!(at, super::AccessTokenType::Bearer);
793        }
794
795        for testcase in ["dpop", "dPoP", "DPOP", "DPoP"] {
796            let at: super::AccessTokenType =
797                serde_json::from_str(&format!("\"{testcase}\"")).expect("Failed to parse");
798            assert_eq!(at, super::AccessTokenType::DPoP);
799        }
800
801        {
802            let testcase = "cheese";
803            let at = serde_json::from_str::<super::AccessTokenType>(&format!("\"{testcase}\""));
804            assert!(at.is_err())
805        }
806    }
807
808    #[test]
809    fn test_token_exchange_grant_serialization() {
810        let scopes: BTreeSet<String> = ["groups", "openid"]
811            .into_iter()
812            .map(str::to_string)
813            .collect();
814
815        let atr = AccessTokenRequest {
816            grant_type: GrantTypeReq::TokenExchange {
817                subject_token: "subject".to_string(),
818                subject_token_type: OAUTH2_TOKEN_TYPE_ACCESS_TOKEN.to_string(),
819                requested_token_type: None,
820                audience: Some("test_resource_server".to_string()),
821                resource: None,
822                actor_token: None,
823                actor_token_type: None,
824                scope: Some(scopes.clone()),
825            },
826            client_post_auth: Default::default(),
827        };
828
829        let json = serde_json::to_string(&atr).expect("JSON failure");
830        let de: AccessTokenRequest = serde_json::from_str(&json).expect("Roundtrip failure");
831
832        match de.grant_type {
833            GrantTypeReq::TokenExchange {
834                subject_token,
835                subject_token_type,
836                requested_token_type,
837                audience,
838                actor_token,
839                actor_token_type,
840                scope: descope,
841                ..
842            } => {
843                assert_eq!(subject_token, "subject");
844                assert_eq!(subject_token_type, OAUTH2_TOKEN_TYPE_ACCESS_TOKEN);
845                assert_eq!(requested_token_type, None);
846                assert_eq!(audience.as_deref(), Some("test_resource_server"));
847                assert_eq!(actor_token, None);
848                assert_eq!(actor_token_type, None);
849                assert_eq!(descope, Some(scopes));
850            }
851            _ => panic!("Wrong grant type"),
852        }
853    }
854
855    #[test]
856    fn test_authorisation_request_prompt_single_value() {
857        let qs = "response_type=code\
858            &client_id=test_client\
859            &redirect_uri=http%3A%2F%2Flocalhost\
860            &scope=openid\
861            &prompt=login";
862
863        let req: super::AuthorisationRequest =
864            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
865
866        assert_eq!(req.prompt.len(), 1);
867        assert!(req.prompt.contains(&super::Prompt::Login));
868    }
869
870    #[test]
871    fn test_authorisation_request_prompt_multiple_values() {
872        let qs = "response_type=code\
873            &client_id=test_client\
874            &redirect_uri=http%3A%2F%2Flocalhost\
875            &scope=openid\
876            &prompt=login%20consent";
877
878        let req: super::AuthorisationRequest =
879            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
880
881        assert_eq!(req.prompt.len(), 2);
882        assert!(req.prompt.contains(&super::Prompt::Login));
883        assert!(req.prompt.contains(&super::Prompt::Consent));
884    }
885
886    #[test]
887    fn test_authorisation_request_prompt_none() {
888        let qs = "response_type=code\
889            &client_id=test_client\
890            &redirect_uri=http%3A%2F%2Flocalhost\
891            &scope=openid\
892            &prompt=none";
893
894        let req: super::AuthorisationRequest =
895            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
896
897        assert_eq!(req.prompt.len(), 1);
898        assert!(req.prompt.contains(&super::Prompt::None));
899    }
900
901    #[test]
902    fn test_authorisation_request_prompt_absent() {
903        let qs = "response_type=code\
904            &client_id=test_client\
905            &redirect_uri=http%3A%2F%2Flocalhost\
906            &scope=openid";
907
908        let req: super::AuthorisationRequest =
909            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
910
911        assert!(req.prompt.is_empty());
912    }
913
914    #[test]
915    fn test_authorisation_request_prompt_invalid_value() {
916        let qs = "response_type=code\
917            &client_id=test_client\
918            &redirect_uri=http%3A%2F%2Flocalhost\
919            &scope=openid\
920            &prompt=bogus";
921
922        let req: super::AuthorisationRequest =
923            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
924
925        assert_eq!(req.prompt.len(), 1);
926        assert!(req
927            .prompt
928            .contains(&super::Prompt::Invalid("bogus".to_string())));
929    }
930
931    #[test]
932    fn test_authorisation_request_prompt_select_account() {
933        let qs = "response_type=code\
934            &client_id=test_client\
935            &redirect_uri=http%3A%2F%2Flocalhost\
936            &scope=openid\
937            &prompt=select_account";
938
939        let req: super::AuthorisationRequest =
940            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
941
942        assert_eq!(req.prompt.len(), 1);
943        assert!(req.prompt.contains(&super::Prompt::SelectAccount));
944    }
945
946    #[test]
947    fn test_authorisation_request_ui_locales() {
948        let qs = "response_type=code\
949            &client_id=test_client\
950            &redirect_uri=http%3A%2F%2Flocalhost\
951            &scope=openid\
952            &ui_locales=en-US";
953
954        let req: super::AuthorisationRequest =
955            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
956
957        assert_eq!(req.ui_locales.len(), 1);
958        assert!(req.ui_locales.contains(&"en-US".to_string()));
959
960        let qs = "response_type=code\
961            &client_id=test_client\
962            &redirect_uri=http%3A%2F%2Flocalhost\
963            &scope=openid\
964            &ui_locales=en-US%20fr-FR";
965
966        let req: super::AuthorisationRequest =
967            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
968        assert_eq!(req.ui_locales.len(), 2);
969        assert!(req.ui_locales.contains(&"fr-FR".to_string()));
970
971        let qs = "response_type=code\
972            &client_id=test_client\
973            &redirect_uri=http%3A%2F%2Flocalhost\
974            &scope=openid";
975
976        let req: super::AuthorisationRequest =
977            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
978        assert_eq!(req.ui_locales.len(), 0);
979        assert!(req.ui_locales.is_empty());
980    }
981
982    #[test]
983    fn test_authorisation_request_prompt_all_valid_values() {
984        let qs = "response_type=code\
985            &client_id=test_client\
986            &redirect_uri=http%3A%2F%2Flocalhost\
987            &scope=openid\
988            &prompt=login+consent+select_account";
989
990        let req: super::AuthorisationRequest =
991            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
992
993        assert_eq!(req.prompt.len(), 3);
994        assert!(req.prompt.contains(&super::Prompt::Login));
995        assert!(req.prompt.contains(&super::Prompt::Consent));
996        assert!(req.prompt.contains(&super::Prompt::SelectAccount));
997    }
998}