Skip to main content

kanidm_proto/
oauth2.rs

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