kanidm_proto/
oauth2.rs

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