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