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    pub client_id: Option<String>,
177    pub client_secret: Option<String>,
178}
179
180impl From<GrantTypeReq> for AccessTokenRequest {
181    fn from(req: GrantTypeReq) -> AccessTokenRequest {
182        AccessTokenRequest {
183            grant_type: req,
184            client_id: None,
185            client_secret: None,
186        }
187    }
188}
189
190#[derive(Serialize, Debug, Clone, Deserialize)]
191#[skip_serializing_none]
192pub struct OAuth2RFC9068Token<V>
193where
194    V: Clone,
195{
196    /// The issuer of this token
197    pub iss: String,
198    /// Unique id of the subject
199    pub sub: Uuid,
200    /// client_id of the oauth2 rp
201    pub aud: String,
202    /// Expiry in UTC epoch seconds
203    pub exp: i64,
204    /// Not valid before.
205    pub nbf: i64,
206    /// Issued at time.
207    pub iat: i64,
208    /// -- NOT used, but part of the spec.
209    pub jti: Option<String>,
210    pub client_id: String,
211    #[serde(flatten)]
212    pub extensions: V,
213}
214
215/// Extensions for RFC 9068 Access Token
216#[serde_as]
217#[skip_serializing_none]
218#[derive(Serialize, Deserialize, Debug, Clone)]
219pub struct OAuth2RFC9068TokenExtensions {
220    pub auth_time: Option<i64>,
221    pub acr: Option<String>,
222    pub amr: Option<Vec<String>>,
223
224    #[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
225    pub scope: BTreeSet<String>,
226
227    pub nonce: Option<String>,
228
229    pub session_id: Uuid,
230    pub parent_session_id: Option<Uuid>,
231}
232
233/// The response for an access token
234#[serde_as]
235#[skip_serializing_none]
236#[derive(Serialize, Deserialize, Debug)]
237pub struct AccessTokenResponse {
238    pub access_token: String,
239    pub token_type: AccessTokenType,
240    /// Expiration relative to `now` in seconds.
241    pub expires_in: u32,
242    pub refresh_token: Option<String>,
243    /// Space separated list of scopes that were approved, if this differs from the
244    /// original request.
245    #[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
246    pub scope: BTreeSet<String>,
247    /// If the `openid` scope was requested, an `id_token` may be present in the response.
248    pub id_token: Option<String>,
249}
250
251/// Access token types, per [IANA Registry - OAuth Access Token Types](https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-types)
252#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
253#[serde(try_from = "&str")]
254pub enum AccessTokenType {
255    Bearer,
256    PoP,
257    #[serde(rename = "N_A")]
258    NA,
259    DPoP,
260}
261
262impl TryFrom<&str> for AccessTokenType {
263    type Error = String;
264
265    fn try_from(s: &str) -> Result<Self, Self::Error> {
266        match s.to_lowercase().as_str() {
267            "bearer" => Ok(AccessTokenType::Bearer),
268            "pop" => Ok(AccessTokenType::PoP),
269            "n_a" => Ok(AccessTokenType::NA),
270            "dpop" => Ok(AccessTokenType::DPoP),
271            _ => Err(format!("Unknown AccessTokenType: {s}")),
272        }
273    }
274}
275
276/// Request revocation of an Access or Refresh token. On success the response is OK 200
277/// with no body.
278#[skip_serializing_none]
279#[derive(Serialize, Deserialize, Debug)]
280pub struct TokenRevokeRequest {
281    pub token: String,
282    /// Not required for Kanidm.
283    /// <https://datatracker.ietf.org/doc/html/rfc7009#section-4.1.2>
284    pub token_type_hint: Option<String>,
285}
286
287/// Request to introspect the identity of the account associated to a token.
288#[skip_serializing_none]
289#[derive(Serialize, Deserialize, Debug)]
290pub struct AccessTokenIntrospectRequest {
291    pub token: String,
292    /// Not required for Kanidm.
293    /// <https://datatracker.ietf.org/doc/html/rfc7009#section-4.1.2>
294    pub token_type_hint: Option<String>,
295}
296
297/// Response to an introspection request. If the token is inactive or revoked, only
298/// `active` will be set to the value of `false`.
299#[serde_as]
300#[skip_serializing_none]
301#[derive(Serialize, Deserialize, Debug)]
302pub struct AccessTokenIntrospectResponse {
303    pub active: bool,
304    #[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
305    pub scope: BTreeSet<String>,
306    pub client_id: Option<String>,
307    pub username: Option<String>,
308    pub token_type: Option<AccessTokenType>,
309    pub exp: Option<i64>,
310    pub iat: Option<i64>,
311    pub nbf: Option<i64>,
312    pub sub: Option<String>,
313    pub aud: Option<String>,
314    pub iss: Option<String>,
315    pub jti: Option<String>,
316}
317
318impl AccessTokenIntrospectResponse {
319    pub fn inactive() -> Self {
320        AccessTokenIntrospectResponse {
321            active: false,
322            scope: BTreeSet::default(),
323            client_id: None,
324            username: None,
325            token_type: None,
326            exp: None,
327            iat: None,
328            nbf: None,
329            sub: None,
330            aud: None,
331            iss: None,
332            jti: None,
333        }
334    }
335}
336
337#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
338#[serde(rename_all = "snake_case")]
339pub enum ResponseType {
340    // Auth Code flow
341    // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
342    Code,
343    // Implicit Grant flow
344    // https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
345    Token,
346    // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#id_token
347    IdToken,
348}
349
350#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
351#[serde(rename_all = "snake_case")]
352pub enum ResponseMode {
353    Query,
354    Fragment,
355    FormPost,
356    #[serde(other, deserialize_with = "deserialize_ignore_any")]
357    Invalid,
358}
359
360fn response_modes_supported_default() -> Vec<ResponseMode> {
361    vec![ResponseMode::Query, ResponseMode::Fragment]
362}
363
364#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
365#[serde(rename_all = "snake_case")]
366pub enum GrantType {
367    #[serde(rename = "authorization_code")]
368    AuthorisationCode,
369    Implicit,
370}
371
372fn grant_types_supported_default() -> Vec<GrantType> {
373    vec![GrantType::AuthorisationCode, GrantType::Implicit]
374}
375
376#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
377#[serde(rename_all = "snake_case")]
378pub enum SubjectType {
379    Pairwise,
380    Public,
381}
382
383#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
384pub enum PkceAlg {
385    S256,
386}
387
388#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
389#[serde(rename_all = "UPPERCASE")]
390/// Algorithms supported for token signatures. Prefers `ES256`
391pub enum IdTokenSignAlg {
392    // WE REFUSE TO SUPPORT NONE. DON'T EVEN ASK. IT WON'T HAPPEN.
393    ES256,
394    RS256,
395}
396
397#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
398#[serde(rename_all = "snake_case")]
399pub enum TokenEndpointAuthMethod {
400    ClientSecretPost,
401    ClientSecretBasic,
402    ClientSecretJwt,
403    PrivateKeyJwt,
404}
405
406fn token_endpoint_auth_methods_supported_default() -> Vec<TokenEndpointAuthMethod> {
407    vec![TokenEndpointAuthMethod::ClientSecretBasic]
408}
409
410#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
411#[serde(rename_all = "snake_case")]
412pub enum DisplayValue {
413    Page,
414    Popup,
415    Touch,
416    Wap,
417}
418
419#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
420#[serde(rename_all = "snake_case")]
421// https://openid.net/specs/openid-connect-core-1_0.html#ClaimTypes
422pub enum ClaimType {
423    Normal,
424    Aggregated,
425    Distributed,
426}
427
428fn claim_types_supported_default() -> Vec<ClaimType> {
429    vec![ClaimType::Normal]
430}
431
432fn claims_parameter_supported_default() -> bool {
433    false
434}
435
436fn request_parameter_supported_default() -> bool {
437    false
438}
439
440fn request_uri_parameter_supported_default() -> bool {
441    false
442}
443
444fn require_request_uri_parameter_supported_default() -> bool {
445    false
446}
447
448#[derive(Serialize, Deserialize, Debug)]
449pub struct OidcWebfingerRel {
450    pub rel: String,
451    pub href: String,
452}
453
454/// The response to an Webfinger request. Only a subset of the body is defined here.
455/// <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4>
456#[skip_serializing_none]
457#[derive(Serialize, Deserialize, Debug)]
458pub struct OidcWebfingerResponse {
459    pub subject: String,
460    pub links: Vec<OidcWebfingerRel>,
461}
462
463/// The response to an OpenID connect discovery request
464/// <https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata>
465#[skip_serializing_none]
466#[derive(Serialize, Deserialize, Debug)]
467pub struct OidcDiscoveryResponse {
468    pub issuer: Url,
469    pub authorization_endpoint: Url,
470    pub token_endpoint: Url,
471    pub userinfo_endpoint: Option<Url>,
472    pub jwks_uri: Url,
473    pub registration_endpoint: Option<Url>,
474    pub scopes_supported: Option<Vec<String>>,
475    // https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1
476    pub response_types_supported: Vec<ResponseType>,
477    // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
478    #[serde(default = "response_modes_supported_default")]
479    pub response_modes_supported: Vec<ResponseMode>,
480    // Need to fill in as authorization_code only else a default is assumed.
481    #[serde(default = "grant_types_supported_default")]
482    pub grant_types_supported: Vec<GrantType>,
483    pub acr_values_supported: Option<Vec<String>>,
484    // https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
485    pub subject_types_supported: Vec<SubjectType>,
486    pub id_token_signing_alg_values_supported: Vec<IdTokenSignAlg>,
487    pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
488    pub id_token_encryption_enc_values_supported: Option<Vec<String>>,
489    pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
490    pub userinfo_encryption_alg_values_supported: Option<Vec<String>>,
491    pub userinfo_encryption_enc_values_supported: Option<Vec<String>>,
492    pub request_object_signing_alg_values_supported: Option<Vec<String>>,
493    pub request_object_encryption_alg_values_supported: Option<Vec<String>>,
494    pub request_object_encryption_enc_values_supported: Option<Vec<String>>,
495    // Defaults to client_secret_basic
496    #[serde(default = "token_endpoint_auth_methods_supported_default")]
497    pub token_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
498    pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
499    // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
500    pub display_values_supported: Option<Vec<DisplayValue>>,
501    // Default to normal.
502    #[serde(default = "claim_types_supported_default")]
503    pub claim_types_supported: Vec<ClaimType>,
504    pub claims_supported: Option<Vec<String>>,
505    pub service_documentation: Option<Url>,
506    pub claims_locales_supported: Option<Vec<String>>,
507    pub ui_locales_supported: Option<Vec<String>>,
508    // Default false.
509    #[serde(default = "claims_parameter_supported_default")]
510    pub claims_parameter_supported: bool,
511
512    pub op_policy_uri: Option<Url>,
513    pub op_tos_uri: Option<Url>,
514
515    // these are related to RFC9101 JWT-Secured Authorization Request support
516    #[serde(default = "request_parameter_supported_default")]
517    pub request_parameter_supported: bool,
518    #[serde(default = "request_uri_parameter_supported_default")]
519    pub request_uri_parameter_supported: bool,
520    #[serde(default = "require_request_uri_parameter_supported_default")]
521    pub require_request_uri_registration: bool,
522
523    pub code_challenge_methods_supported: Vec<PkceAlg>,
524
525    // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
526    // "content type that contains a set of Claims as its members that are a subset of the Metadata
527    //  values defined in Section 3. Other Claims MAY also be returned. "
528    //
529    // In addition, we also return the following claims in kanidm
530
531    // rfc7009
532    pub revocation_endpoint: Option<Url>,
533    pub revocation_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
534
535    // rfc7662
536    pub introspection_endpoint: Option<Url>,
537    pub introspection_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
538    pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
539
540    /// Ref <https://www.rfc-editor.org/rfc/rfc8628#section-4>
541    pub device_authorization_endpoint: Option<Url>,
542}
543
544/// The response to an OAuth2 rfc8414 metadata request
545#[skip_serializing_none]
546#[derive(Serialize, Deserialize, Debug)]
547pub struct Oauth2Rfc8414MetadataResponse {
548    pub issuer: Url,
549    pub authorization_endpoint: Url,
550    pub token_endpoint: Url,
551
552    pub jwks_uri: Option<Url>,
553
554    // rfc7591 reg endpoint.
555    pub registration_endpoint: Option<Url>,
556
557    pub scopes_supported: Option<Vec<String>>,
558
559    // For Oauth2 should be Code, Token.
560    pub response_types_supported: Vec<ResponseType>,
561    #[serde(default = "response_modes_supported_default")]
562    pub response_modes_supported: Vec<ResponseMode>,
563    #[serde(default = "grant_types_supported_default")]
564    pub grant_types_supported: Vec<GrantType>,
565
566    #[serde(default = "token_endpoint_auth_methods_supported_default")]
567    pub token_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
568
569    pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
570
571    pub service_documentation: Option<Url>,
572    pub ui_locales_supported: Option<Vec<String>>,
573
574    pub op_policy_uri: Option<Url>,
575    pub op_tos_uri: Option<Url>,
576
577    // rfc7009
578    pub revocation_endpoint: Option<Url>,
579    pub revocation_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
580
581    // rfc7662
582    pub introspection_endpoint: Option<Url>,
583    pub introspection_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
584    pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
585
586    // RFC7636
587    pub code_challenge_methods_supported: Vec<PkceAlg>,
588}
589
590#[skip_serializing_none]
591#[derive(Serialize, Deserialize, Debug, Default)]
592pub struct ErrorResponse {
593    pub error: String,
594    pub error_description: Option<String>,
595    pub error_uri: Option<Url>,
596}
597
598#[derive(Debug, Serialize, Deserialize)]
599/// Ref <https://www.rfc-editor.org/rfc/rfc8628#section-3.2>
600pub struct DeviceAuthorizationResponse {
601    /// Base64-encoded bundle of 16 bytes
602    device_code: String,
603    /// 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.
604    user_code: String,
605    verification_uri: Url,
606    verification_uri_complete: Url,
607    expires_in: u64,
608    interval: u64,
609}
610
611impl DeviceAuthorizationResponse {
612    pub fn new(verification_uri: Url, device_code: [u8; 16], user_code: String) -> Self {
613        let mut verification_uri_complete = verification_uri.clone();
614        verification_uri_complete
615            .query_pairs_mut()
616            .append_pair("user_code", &user_code);
617
618        let device_code = STANDARD.encode(device_code);
619
620        Self {
621            verification_uri_complete,
622            device_code,
623            user_code,
624            verification_uri,
625            expires_in: OAUTH2_DEVICE_CODE_EXPIRY_SECONDS,
626            interval: OAUTH2_DEVICE_CODE_INTERVAL_SECONDS,
627        }
628    }
629}
630
631#[cfg(test)]
632mod tests {
633    use super::{AccessTokenRequest, GrantTypeReq};
634    use url::Url;
635
636    #[test]
637    fn test_oauth2_access_token_req() {
638        let atr: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
639            code: "demo code".to_string(),
640            redirect_uri: Url::parse("http://[::1]").unwrap(),
641            code_verifier: None,
642        }
643        .into();
644
645        println!("{:?}", serde_json::to_string(&atr).expect("JSON failure"));
646    }
647
648    #[test]
649    fn test_oauth2_access_token_type_serde() {
650        for testcase in ["bearer", "Bearer", "BeArEr"] {
651            let at: super::AccessTokenType =
652                serde_json::from_str(&format!("\"{testcase}\"")).expect("Failed to parse");
653            assert_eq!(at, super::AccessTokenType::Bearer);
654        }
655
656        for testcase in ["dpop", "dPoP", "DPOP", "DPoP"] {
657            let at: super::AccessTokenType =
658                serde_json::from_str(&format!("\"{testcase}\"")).expect("Failed to parse");
659            assert_eq!(at, super::AccessTokenType::DPoP);
660        }
661
662        {
663            let testcase = "cheese";
664            let at = serde_json::from_str::<super::AccessTokenType>(&format!("\"{testcase}\""));
665            assert!(at.is_err())
666        }
667    }
668}