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
381impl From<String> for AccessTokenIntrospectRequest {
382    fn from(token: String) -> Self {
383        Self {
384            token,
385            token_type_hint: None,
386            client_post_auth: ClientPostAuth::default(),
387        }
388    }
389}
390
391/// Response to an introspection request. If the token is inactive or revoked, only
392/// `active` will be set to the value of `false`.
393#[serde_as]
394#[skip_serializing_none]
395#[derive(Serialize, Deserialize, Debug, Default)]
396pub struct AccessTokenIntrospectResponse {
397    pub active: bool,
398    #[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
399    pub scope: BTreeSet<String>,
400    pub client_id: Option<String>,
401    pub username: Option<String>,
402    pub token_type: Option<AccessTokenType>,
403    pub exp: Option<i64>,
404    pub iat: Option<i64>,
405    pub nbf: Option<i64>,
406    pub sub: Option<String>,
407    pub aud: Option<String>,
408    pub iss: Option<String>,
409    // JWT ID <https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7> set to session ID
410    pub jti: Uuid,
411}
412
413impl AccessTokenIntrospectResponse {
414    pub fn inactive(session_id: Uuid) -> Self {
415        AccessTokenIntrospectResponse {
416            active: false,
417            scope: BTreeSet::default(),
418            client_id: None,
419            username: None,
420            token_type: None,
421            exp: None,
422            iat: None,
423            nbf: None,
424            sub: None,
425            aud: None,
426            iss: None,
427            jti: session_id,
428        }
429    }
430}
431
432#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
433#[serde(rename_all = "snake_case")]
434pub enum ResponseType {
435    // Auth Code flow
436    // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
437    Code,
438    // Implicit Grant flow
439    // https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
440    Token,
441    // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#id_token
442    IdToken,
443}
444
445#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
446#[serde(rename_all = "snake_case")]
447pub enum ResponseMode {
448    Query,
449    Fragment,
450    FormPost,
451    #[serde(other, deserialize_with = "deserialize_ignore_any")]
452    Invalid,
453}
454
455fn response_modes_supported_default() -> Vec<ResponseMode> {
456    vec![ResponseMode::Query, ResponseMode::Fragment]
457}
458
459#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
460#[serde(rename_all = "snake_case")]
461pub enum Prompt {
462    /// None is not the absence of a value but a rather a value itself.
463    /// Prompt::None signifies to kanidm that *if* the authentications server
464    /// cannot automatically proceed thanks to an already logged in user,
465    /// It must return a error response rather than allowing a user to proceed
466    /// through the regular login flow.
467    ///
468    /// This is specified in OIDC Core 1.0 ยง3.1.2.1
469    /// <https://openid.net/specs/openid-connect-core-1_0.html>
470    None,
471    Login,
472    Consent,
473    SelectAccount,
474    #[serde(untagged)]
475    Invalid(String),
476}
477
478impl Display for Prompt {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        let s = match self {
481            Prompt::None => "none",
482            Prompt::Login => "login",
483            Prompt::Consent => "consent",
484            Prompt::SelectAccount => "select_account",
485            Prompt::Invalid(str) => &format!("invalid({})", str),
486        };
487        write!(f, "{s}")
488    }
489}
490
491impl std::str::FromStr for Prompt {
492    type Err = std::convert::Infallible;
493
494    fn from_str(s: &str) -> Result<Self, Self::Err> {
495        Ok(match s {
496            "none" => Prompt::None,
497            "login" => Prompt::Login,
498            "consent" => Prompt::Consent,
499            "select_account" => Prompt::SelectAccount,
500            other => Prompt::Invalid(other.to_string()),
501        })
502    }
503}
504
505#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
506#[serde(rename_all = "snake_case")]
507pub enum GrantType {
508    #[serde(rename = "authorization_code")]
509    AuthorisationCode,
510    Implicit,
511    #[serde(rename = "urn:ietf:params:oauth:grant-type:token-exchange")]
512    TokenExchange,
513    ClientCredentials,
514    RefreshToken,
515    #[serde(rename = "urn:ietf:params:oauth:grant-type:jwt-bearer")]
516    JwtBearer,
517}
518
519fn grant_types_supported_default() -> Vec<GrantType> {
520    vec![
521        GrantType::AuthorisationCode,
522        GrantType::Implicit,
523        GrantType::TokenExchange,
524    ]
525}
526
527#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
528#[serde(rename_all = "snake_case")]
529pub enum SubjectType {
530    Pairwise,
531    Public,
532}
533
534#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
535pub enum PkceAlg {
536    S256,
537}
538
539#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
540#[serde(rename_all = "UPPERCASE")]
541/// Algorithms supported for token signatures. Prefers `ES256`
542pub enum IdTokenSignAlg {
543    // WE REFUSE TO SUPPORT NONE. DON'T EVEN ASK. IT WON'T HAPPEN.
544    ES256,
545    RS256,
546}
547
548#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
549#[serde(rename_all = "snake_case")]
550pub enum EndpointAuthMethod {
551    None,
552    ClientSecretPost,
553    ClientSecretBasic,
554    ClientSecretJwt,
555    PrivateKeyJwt,
556}
557
558fn token_endpoint_auth_methods_supported_default() -> Vec<EndpointAuthMethod> {
559    vec![EndpointAuthMethod::ClientSecretBasic]
560}
561
562#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
563#[serde(rename_all = "snake_case")]
564pub enum DisplayValue {
565    Page,
566    Popup,
567    Touch,
568    Wap,
569}
570
571#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
572#[serde(rename_all = "snake_case")]
573// https://openid.net/specs/openid-connect-core-1_0.html#ClaimTypes
574pub enum ClaimType {
575    Normal,
576    Aggregated,
577    Distributed,
578}
579
580fn claim_types_supported_default() -> Vec<ClaimType> {
581    vec![ClaimType::Normal]
582}
583
584fn claims_parameter_supported_default() -> bool {
585    false
586}
587
588fn request_parameter_supported_default() -> bool {
589    false
590}
591
592fn request_uri_parameter_supported_default() -> bool {
593    false
594}
595
596fn require_request_uri_parameter_supported_default() -> bool {
597    false
598}
599
600#[derive(Serialize, Deserialize, Debug)]
601pub struct OidcWebfingerRel {
602    pub rel: String,
603    pub href: String,
604}
605
606/// The response to an Webfinger request. Only a subset of the body is defined here.
607/// <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4>
608#[skip_serializing_none]
609#[derive(Serialize, Deserialize, Debug)]
610pub struct OidcWebfingerResponse {
611    pub subject: String,
612    pub links: Vec<OidcWebfingerRel>,
613}
614
615/// The response to an OpenID connect discovery request
616/// <https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata>
617#[skip_serializing_none]
618#[derive(Serialize, Deserialize, Debug)]
619pub struct OidcDiscoveryResponse {
620    pub issuer: Url,
621    pub authorization_endpoint: Url,
622    pub token_endpoint: Url,
623    pub userinfo_endpoint: Option<Url>,
624    pub jwks_uri: Url,
625    pub registration_endpoint: Option<Url>,
626    pub scopes_supported: Option<Vec<String>>,
627    // https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1
628    pub response_types_supported: Vec<ResponseType>,
629    // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
630    #[serde(default = "response_modes_supported_default")]
631    pub response_modes_supported: Vec<ResponseMode>,
632    // Need to fill in as authorization_code only else a default is assumed.
633    #[serde(default = "grant_types_supported_default")]
634    pub grant_types_supported: Vec<GrantType>,
635    pub acr_values_supported: Option<Vec<String>>,
636    // https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
637    pub subject_types_supported: Vec<SubjectType>,
638    pub id_token_signing_alg_values_supported: Vec<IdTokenSignAlg>,
639    pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
640    pub id_token_encryption_enc_values_supported: Option<Vec<String>>,
641    pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
642    pub userinfo_encryption_alg_values_supported: Option<Vec<String>>,
643    pub userinfo_encryption_enc_values_supported: Option<Vec<String>>,
644    pub request_object_signing_alg_values_supported: Option<Vec<String>>,
645    pub request_object_encryption_alg_values_supported: Option<Vec<String>>,
646    pub request_object_encryption_enc_values_supported: Option<Vec<String>>,
647    // Defaults to client_secret_basic
648    #[serde(default = "token_endpoint_auth_methods_supported_default")]
649    pub token_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
650    pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
651    // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
652    pub display_values_supported: Option<Vec<DisplayValue>>,
653    // Default to normal.
654    #[serde(default = "claim_types_supported_default")]
655    pub claim_types_supported: Vec<ClaimType>,
656    pub claims_supported: Option<Vec<String>>,
657    pub service_documentation: Option<Url>,
658    pub claims_locales_supported: Option<Vec<String>>,
659    pub ui_locales_supported: Option<Vec<String>>,
660    // Default false.
661    #[serde(default = "claims_parameter_supported_default")]
662    pub claims_parameter_supported: bool,
663
664    pub op_policy_uri: Option<Url>,
665    pub op_tos_uri: Option<Url>,
666
667    // these are related to RFC9101 JWT-Secured Authorization Request support
668    #[serde(default = "request_parameter_supported_default")]
669    pub request_parameter_supported: bool,
670    #[serde(default = "request_uri_parameter_supported_default")]
671    pub request_uri_parameter_supported: bool,
672    #[serde(default = "require_request_uri_parameter_supported_default")]
673    pub require_request_uri_registration: bool,
674
675    pub code_challenge_methods_supported: Vec<PkceAlg>,
676
677    // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
678    // "content type that contains a set of Claims as its members that are a subset of the Metadata
679    //  values defined in Section 3. Other Claims MAY also be returned. "
680    //
681    // In addition, we also return the following claims in kanidm
682
683    // rfc7009
684    pub revocation_endpoint: Option<Url>,
685    pub revocation_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
686
687    // rfc7662
688    pub introspection_endpoint: Option<Url>,
689    pub introspection_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
690    pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
691
692    /// Ref <https://www.rfc-editor.org/rfc/rfc8628#section-4>
693    pub device_authorization_endpoint: Option<Url>,
694}
695
696/// The response to an OAuth2 rfc8414 metadata request
697#[skip_serializing_none]
698#[derive(Serialize, Deserialize, Debug)]
699pub struct Oauth2Rfc8414MetadataResponse {
700    pub issuer: Url,
701    pub authorization_endpoint: Url,
702    pub token_endpoint: Url,
703
704    pub jwks_uri: Option<Url>,
705
706    // rfc7591 reg endpoint.
707    pub registration_endpoint: Option<Url>,
708
709    pub scopes_supported: Option<Vec<String>>,
710
711    // For Oauth2 should be Code, Token.
712    pub response_types_supported: Vec<ResponseType>,
713    #[serde(default = "response_modes_supported_default")]
714    pub response_modes_supported: Vec<ResponseMode>,
715    #[serde(default = "grant_types_supported_default")]
716    pub grant_types_supported: Vec<GrantType>,
717
718    #[serde(default = "token_endpoint_auth_methods_supported_default")]
719    pub token_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
720
721    pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
722
723    pub service_documentation: Option<Url>,
724    pub ui_locales_supported: Option<Vec<String>>,
725
726    pub op_policy_uri: Option<Url>,
727    pub op_tos_uri: Option<Url>,
728
729    // rfc7009
730    pub revocation_endpoint: Option<Url>,
731    pub revocation_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
732
733    // rfc7662
734    pub introspection_endpoint: Option<Url>,
735    pub introspection_endpoint_auth_methods_supported: Vec<EndpointAuthMethod>,
736    pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
737
738    // RFC7636
739    pub code_challenge_methods_supported: Vec<PkceAlg>,
740}
741
742#[skip_serializing_none]
743#[derive(Serialize, Deserialize, Debug, Default)]
744pub struct ErrorResponse {
745    pub error: String,
746    pub error_description: Option<String>,
747    pub error_uri: Option<Url>,
748}
749
750#[derive(Debug, Serialize, Deserialize)]
751/// Ref <https://www.rfc-editor.org/rfc/rfc8628#section-3.2>
752pub struct DeviceAuthorizationResponse {
753    /// Base64-encoded bundle of 16 bytes
754    device_code: String,
755    /// 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.
756    user_code: String,
757    verification_uri: Url,
758    verification_uri_complete: Url,
759    expires_in: u64,
760    interval: u64,
761}
762
763impl DeviceAuthorizationResponse {
764    pub fn new(verification_uri: Url, device_code: [u8; 16], user_code: String) -> Self {
765        let mut verification_uri_complete = verification_uri.clone();
766        verification_uri_complete
767            .query_pairs_mut()
768            .append_pair("user_code", &user_code);
769
770        let device_code = STANDARD.encode(device_code);
771
772        Self {
773            verification_uri_complete,
774            device_code,
775            user_code,
776            verification_uri,
777            expires_in: OAUTH2_DEVICE_CODE_EXPIRY_SECONDS,
778            interval: OAUTH2_DEVICE_CODE_INTERVAL_SECONDS,
779        }
780    }
781}
782
783#[cfg(test)]
784mod tests {
785    use super::{AccessTokenRequest, GrantTypeReq, OAUTH2_TOKEN_TYPE_ACCESS_TOKEN};
786    use std::collections::BTreeSet;
787    use url::Url;
788
789    #[test]
790    fn test_oauth2_access_token_req() {
791        let atr: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
792            code: "demo code".to_string(),
793            redirect_uri: Url::parse("http://[::1]").unwrap(),
794            code_verifier: None,
795        }
796        .into();
797
798        println!("{:?}", serde_json::to_string(&atr).expect("JSON failure"));
799    }
800
801    #[test]
802    fn test_oauth2_access_token_type_serde() {
803        for testcase in ["bearer", "Bearer", "BeArEr"] {
804            let at: super::AccessTokenType =
805                serde_json::from_str(&format!("\"{testcase}\"")).expect("Failed to parse");
806            assert_eq!(at, super::AccessTokenType::Bearer);
807        }
808
809        for testcase in ["dpop", "dPoP", "DPOP", "DPoP"] {
810            let at: super::AccessTokenType =
811                serde_json::from_str(&format!("\"{testcase}\"")).expect("Failed to parse");
812            assert_eq!(at, super::AccessTokenType::DPoP);
813        }
814
815        {
816            let testcase = "cheese";
817            let at = serde_json::from_str::<super::AccessTokenType>(&format!("\"{testcase}\""));
818            assert!(at.is_err())
819        }
820    }
821
822    #[test]
823    fn test_token_exchange_grant_serialization() {
824        let scopes: BTreeSet<String> = ["groups", "openid"]
825            .into_iter()
826            .map(str::to_string)
827            .collect();
828
829        let atr = AccessTokenRequest {
830            grant_type: GrantTypeReq::TokenExchange {
831                subject_token: "subject".to_string(),
832                subject_token_type: OAUTH2_TOKEN_TYPE_ACCESS_TOKEN.to_string(),
833                requested_token_type: None,
834                audience: Some("test_resource_server".to_string()),
835                resource: None,
836                actor_token: None,
837                actor_token_type: None,
838                scope: Some(scopes.clone()),
839            },
840            client_post_auth: Default::default(),
841        };
842
843        let json = serde_json::to_string(&atr).expect("JSON failure");
844        let de: AccessTokenRequest = serde_json::from_str(&json).expect("Roundtrip failure");
845
846        match de.grant_type {
847            GrantTypeReq::TokenExchange {
848                subject_token,
849                subject_token_type,
850                requested_token_type,
851                audience,
852                actor_token,
853                actor_token_type,
854                scope: descope,
855                ..
856            } => {
857                assert_eq!(subject_token, "subject");
858                assert_eq!(subject_token_type, OAUTH2_TOKEN_TYPE_ACCESS_TOKEN);
859                assert_eq!(requested_token_type, None);
860                assert_eq!(audience.as_deref(), Some("test_resource_server"));
861                assert_eq!(actor_token, None);
862                assert_eq!(actor_token_type, None);
863                assert_eq!(descope, Some(scopes));
864            }
865            _ => panic!("Wrong grant type"),
866        }
867    }
868
869    #[test]
870    fn test_authorisation_request_prompt_single_value() {
871        let qs = "response_type=code\
872            &client_id=test_client\
873            &redirect_uri=http%3A%2F%2Flocalhost\
874            &scope=openid\
875            &prompt=login";
876
877        let req: super::AuthorisationRequest =
878            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
879
880        assert_eq!(req.prompt.len(), 1);
881        assert!(req.prompt.contains(&super::Prompt::Login));
882    }
883
884    #[test]
885    fn test_authorisation_request_prompt_multiple_values() {
886        let qs = "response_type=code\
887            &client_id=test_client\
888            &redirect_uri=http%3A%2F%2Flocalhost\
889            &scope=openid\
890            &prompt=login%20consent";
891
892        let req: super::AuthorisationRequest =
893            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
894
895        assert_eq!(req.prompt.len(), 2);
896        assert!(req.prompt.contains(&super::Prompt::Login));
897        assert!(req.prompt.contains(&super::Prompt::Consent));
898    }
899
900    #[test]
901    fn test_authorisation_request_prompt_none() {
902        let qs = "response_type=code\
903            &client_id=test_client\
904            &redirect_uri=http%3A%2F%2Flocalhost\
905            &scope=openid\
906            &prompt=none";
907
908        let req: super::AuthorisationRequest =
909            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
910
911        assert_eq!(req.prompt.len(), 1);
912        assert!(req.prompt.contains(&super::Prompt::None));
913    }
914
915    #[test]
916    fn test_authorisation_request_prompt_absent() {
917        let qs = "response_type=code\
918            &client_id=test_client\
919            &redirect_uri=http%3A%2F%2Flocalhost\
920            &scope=openid";
921
922        let req: super::AuthorisationRequest =
923            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
924
925        assert!(req.prompt.is_empty());
926    }
927
928    #[test]
929    fn test_authorisation_request_prompt_invalid_value() {
930        let qs = "response_type=code\
931            &client_id=test_client\
932            &redirect_uri=http%3A%2F%2Flocalhost\
933            &scope=openid\
934            &prompt=bogus";
935
936        let req: super::AuthorisationRequest =
937            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
938
939        assert_eq!(req.prompt.len(), 1);
940        assert!(req
941            .prompt
942            .contains(&super::Prompt::Invalid("bogus".to_string())));
943    }
944
945    #[test]
946    fn test_authorisation_request_prompt_select_account() {
947        let qs = "response_type=code\
948            &client_id=test_client\
949            &redirect_uri=http%3A%2F%2Flocalhost\
950            &scope=openid\
951            &prompt=select_account";
952
953        let req: super::AuthorisationRequest =
954            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
955
956        assert_eq!(req.prompt.len(), 1);
957        assert!(req.prompt.contains(&super::Prompt::SelectAccount));
958    }
959
960    #[test]
961    fn test_authorisation_request_ui_locales() {
962        let qs = "response_type=code\
963            &client_id=test_client\
964            &redirect_uri=http%3A%2F%2Flocalhost\
965            &scope=openid\
966            &ui_locales=en-US";
967
968        let req: super::AuthorisationRequest =
969            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
970
971        assert_eq!(req.ui_locales.len(), 1);
972        assert!(req.ui_locales.contains(&"en-US".to_string()));
973
974        let qs = "response_type=code\
975            &client_id=test_client\
976            &redirect_uri=http%3A%2F%2Flocalhost\
977            &scope=openid\
978            &ui_locales=en-US%20fr-FR";
979
980        let req: super::AuthorisationRequest =
981            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
982        assert_eq!(req.ui_locales.len(), 2);
983        assert!(req.ui_locales.contains(&"fr-FR".to_string()));
984
985        let qs = "response_type=code\
986            &client_id=test_client\
987            &redirect_uri=http%3A%2F%2Flocalhost\
988            &scope=openid";
989
990        let req: super::AuthorisationRequest =
991            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
992        assert_eq!(req.ui_locales.len(), 0);
993        assert!(req.ui_locales.is_empty());
994    }
995
996    #[test]
997    fn test_authorisation_request_prompt_all_valid_values() {
998        let qs = "response_type=code\
999            &client_id=test_client\
1000            &redirect_uri=http%3A%2F%2Flocalhost\
1001            &scope=openid\
1002            &prompt=login+consent+select_account";
1003
1004        let req: super::AuthorisationRequest =
1005            serde_urlencoded::from_str(qs).expect("Failed to deserialize");
1006
1007        assert_eq!(req.prompt.len(), 3);
1008        assert!(req.prompt.contains(&super::Prompt::Login));
1009        assert!(req.prompt.contains(&super::Prompt::Consent));
1010        assert!(req.prompt.contains(&super::Prompt::SelectAccount));
1011    }
1012}