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    /// JWT ID <https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7> - we set it to the session ID
208    pub jti: Uuid,
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    // JWT ID <https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7> set to session ID
366    pub jti: Uuid,
367}
368
369impl AccessTokenIntrospectResponse {
370    pub fn inactive(session_id: Uuid) -> Self {
371        AccessTokenIntrospectResponse {
372            active: false,
373            scope: BTreeSet::default(),
374            client_id: None,
375            username: None,
376            token_type: None,
377            exp: None,
378            iat: None,
379            nbf: None,
380            sub: None,
381            aud: None,
382            iss: None,
383            jti: session_id,
384        }
385    }
386}
387
388#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
389#[serde(rename_all = "snake_case")]
390pub enum ResponseType {
391    // Auth Code flow
392    // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
393    Code,
394    // Implicit Grant flow
395    // https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
396    Token,
397    // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#id_token
398    IdToken,
399}
400
401#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq)]
402#[serde(rename_all = "snake_case")]
403pub enum ResponseMode {
404    Query,
405    Fragment,
406    FormPost,
407    #[serde(other, deserialize_with = "deserialize_ignore_any")]
408    Invalid,
409}
410
411fn response_modes_supported_default() -> Vec<ResponseMode> {
412    vec![ResponseMode::Query, ResponseMode::Fragment]
413}
414
415#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
416#[serde(rename_all = "snake_case")]
417pub enum GrantType {
418    #[serde(rename = "authorization_code")]
419    AuthorisationCode,
420    Implicit,
421}
422
423fn grant_types_supported_default() -> Vec<GrantType> {
424    vec![GrantType::AuthorisationCode, GrantType::Implicit]
425}
426
427#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
428#[serde(rename_all = "snake_case")]
429pub enum SubjectType {
430    Pairwise,
431    Public,
432}
433
434#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
435pub enum PkceAlg {
436    S256,
437}
438
439#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
440#[serde(rename_all = "UPPERCASE")]
441/// Algorithms supported for token signatures. Prefers `ES256`
442pub enum IdTokenSignAlg {
443    // WE REFUSE TO SUPPORT NONE. DON'T EVEN ASK. IT WON'T HAPPEN.
444    ES256,
445    RS256,
446}
447
448#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
449#[serde(rename_all = "snake_case")]
450pub enum TokenEndpointAuthMethod {
451    ClientSecretPost,
452    ClientSecretBasic,
453    ClientSecretJwt,
454    PrivateKeyJwt,
455}
456
457fn token_endpoint_auth_methods_supported_default() -> Vec<TokenEndpointAuthMethod> {
458    vec![TokenEndpointAuthMethod::ClientSecretBasic]
459}
460
461#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
462#[serde(rename_all = "snake_case")]
463pub enum DisplayValue {
464    Page,
465    Popup,
466    Touch,
467    Wap,
468}
469
470#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
471#[serde(rename_all = "snake_case")]
472// https://openid.net/specs/openid-connect-core-1_0.html#ClaimTypes
473pub enum ClaimType {
474    Normal,
475    Aggregated,
476    Distributed,
477}
478
479fn claim_types_supported_default() -> Vec<ClaimType> {
480    vec![ClaimType::Normal]
481}
482
483fn claims_parameter_supported_default() -> bool {
484    false
485}
486
487fn request_parameter_supported_default() -> bool {
488    false
489}
490
491fn request_uri_parameter_supported_default() -> bool {
492    false
493}
494
495fn require_request_uri_parameter_supported_default() -> bool {
496    false
497}
498
499#[derive(Serialize, Deserialize, Debug)]
500pub struct OidcWebfingerRel {
501    pub rel: String,
502    pub href: String,
503}
504
505/// The response to an Webfinger request. Only a subset of the body is defined here.
506/// <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4>
507#[skip_serializing_none]
508#[derive(Serialize, Deserialize, Debug)]
509pub struct OidcWebfingerResponse {
510    pub subject: String,
511    pub links: Vec<OidcWebfingerRel>,
512}
513
514/// The response to an OpenID connect discovery request
515/// <https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata>
516#[skip_serializing_none]
517#[derive(Serialize, Deserialize, Debug)]
518pub struct OidcDiscoveryResponse {
519    pub issuer: Url,
520    pub authorization_endpoint: Url,
521    pub token_endpoint: Url,
522    pub userinfo_endpoint: Option<Url>,
523    pub jwks_uri: Url,
524    pub registration_endpoint: Option<Url>,
525    pub scopes_supported: Option<Vec<String>>,
526    // https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1
527    pub response_types_supported: Vec<ResponseType>,
528    // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
529    #[serde(default = "response_modes_supported_default")]
530    pub response_modes_supported: Vec<ResponseMode>,
531    // Need to fill in as authorization_code only else a default is assumed.
532    #[serde(default = "grant_types_supported_default")]
533    pub grant_types_supported: Vec<GrantType>,
534    pub acr_values_supported: Option<Vec<String>>,
535    // https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
536    pub subject_types_supported: Vec<SubjectType>,
537    pub id_token_signing_alg_values_supported: Vec<IdTokenSignAlg>,
538    pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
539    pub id_token_encryption_enc_values_supported: Option<Vec<String>>,
540    pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
541    pub userinfo_encryption_alg_values_supported: Option<Vec<String>>,
542    pub userinfo_encryption_enc_values_supported: Option<Vec<String>>,
543    pub request_object_signing_alg_values_supported: Option<Vec<String>>,
544    pub request_object_encryption_alg_values_supported: Option<Vec<String>>,
545    pub request_object_encryption_enc_values_supported: Option<Vec<String>>,
546    // Defaults to client_secret_basic
547    #[serde(default = "token_endpoint_auth_methods_supported_default")]
548    pub token_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
549    pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
550    // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
551    pub display_values_supported: Option<Vec<DisplayValue>>,
552    // Default to normal.
553    #[serde(default = "claim_types_supported_default")]
554    pub claim_types_supported: Vec<ClaimType>,
555    pub claims_supported: Option<Vec<String>>,
556    pub service_documentation: Option<Url>,
557    pub claims_locales_supported: Option<Vec<String>>,
558    pub ui_locales_supported: Option<Vec<String>>,
559    // Default false.
560    #[serde(default = "claims_parameter_supported_default")]
561    pub claims_parameter_supported: bool,
562
563    pub op_policy_uri: Option<Url>,
564    pub op_tos_uri: Option<Url>,
565
566    // these are related to RFC9101 JWT-Secured Authorization Request support
567    #[serde(default = "request_parameter_supported_default")]
568    pub request_parameter_supported: bool,
569    #[serde(default = "request_uri_parameter_supported_default")]
570    pub request_uri_parameter_supported: bool,
571    #[serde(default = "require_request_uri_parameter_supported_default")]
572    pub require_request_uri_registration: bool,
573
574    pub code_challenge_methods_supported: Vec<PkceAlg>,
575
576    // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
577    // "content type that contains a set of Claims as its members that are a subset of the Metadata
578    //  values defined in Section 3. Other Claims MAY also be returned. "
579    //
580    // In addition, we also return the following claims in kanidm
581
582    // rfc7009
583    pub revocation_endpoint: Option<Url>,
584    pub revocation_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
585
586    // rfc7662
587    pub introspection_endpoint: Option<Url>,
588    pub introspection_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
589    pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
590
591    /// Ref <https://www.rfc-editor.org/rfc/rfc8628#section-4>
592    pub device_authorization_endpoint: Option<Url>,
593}
594
595/// The response to an OAuth2 rfc8414 metadata request
596#[skip_serializing_none]
597#[derive(Serialize, Deserialize, Debug)]
598pub struct Oauth2Rfc8414MetadataResponse {
599    pub issuer: Url,
600    pub authorization_endpoint: Url,
601    pub token_endpoint: Url,
602
603    pub jwks_uri: Option<Url>,
604
605    // rfc7591 reg endpoint.
606    pub registration_endpoint: Option<Url>,
607
608    pub scopes_supported: Option<Vec<String>>,
609
610    // For Oauth2 should be Code, Token.
611    pub response_types_supported: Vec<ResponseType>,
612    #[serde(default = "response_modes_supported_default")]
613    pub response_modes_supported: Vec<ResponseMode>,
614    #[serde(default = "grant_types_supported_default")]
615    pub grant_types_supported: Vec<GrantType>,
616
617    #[serde(default = "token_endpoint_auth_methods_supported_default")]
618    pub token_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
619
620    pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
621
622    pub service_documentation: Option<Url>,
623    pub ui_locales_supported: Option<Vec<String>>,
624
625    pub op_policy_uri: Option<Url>,
626    pub op_tos_uri: Option<Url>,
627
628    // rfc7009
629    pub revocation_endpoint: Option<Url>,
630    pub revocation_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
631
632    // rfc7662
633    pub introspection_endpoint: Option<Url>,
634    pub introspection_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
635    pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
636
637    // RFC7636
638    pub code_challenge_methods_supported: Vec<PkceAlg>,
639}
640
641#[skip_serializing_none]
642#[derive(Serialize, Deserialize, Debug, Default)]
643pub struct ErrorResponse {
644    pub error: String,
645    pub error_description: Option<String>,
646    pub error_uri: Option<Url>,
647}
648
649#[derive(Debug, Serialize, Deserialize)]
650/// Ref <https://www.rfc-editor.org/rfc/rfc8628#section-3.2>
651pub struct DeviceAuthorizationResponse {
652    /// Base64-encoded bundle of 16 bytes
653    device_code: String,
654    /// 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.
655    user_code: String,
656    verification_uri: Url,
657    verification_uri_complete: Url,
658    expires_in: u64,
659    interval: u64,
660}
661
662impl DeviceAuthorizationResponse {
663    pub fn new(verification_uri: Url, device_code: [u8; 16], user_code: String) -> Self {
664        let mut verification_uri_complete = verification_uri.clone();
665        verification_uri_complete
666            .query_pairs_mut()
667            .append_pair("user_code", &user_code);
668
669        let device_code = STANDARD.encode(device_code);
670
671        Self {
672            verification_uri_complete,
673            device_code,
674            user_code,
675            verification_uri,
676            expires_in: OAUTH2_DEVICE_CODE_EXPIRY_SECONDS,
677            interval: OAUTH2_DEVICE_CODE_INTERVAL_SECONDS,
678        }
679    }
680}
681
682#[cfg(test)]
683mod tests {
684    use super::{AccessTokenRequest, GrantTypeReq};
685    use url::Url;
686
687    #[test]
688    fn test_oauth2_access_token_req() {
689        let atr: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
690            code: "demo code".to_string(),
691            redirect_uri: Url::parse("http://[::1]").unwrap(),
692            code_verifier: None,
693        }
694        .into();
695
696        println!("{:?}", serde_json::to_string(&atr).expect("JSON failure"));
697    }
698
699    #[test]
700    fn test_oauth2_access_token_type_serde() {
701        for testcase in ["bearer", "Bearer", "BeArEr"] {
702            let at: super::AccessTokenType =
703                serde_json::from_str(&format!("\"{testcase}\"")).expect("Failed to parse");
704            assert_eq!(at, super::AccessTokenType::Bearer);
705        }
706
707        for testcase in ["dpop", "dPoP", "DPOP", "DPoP"] {
708            let at: super::AccessTokenType =
709                serde_json::from_str(&format!("\"{testcase}\"")).expect("Failed to parse");
710            assert_eq!(at, super::AccessTokenType::DPoP);
711        }
712
713        {
714            let testcase = "cheese";
715            let at = serde_json::from_str::<super::AccessTokenType>(&format!("\"{testcase}\""));
716            assert!(at.is_err())
717        }
718    }
719}