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