//! Oauth2 RFC protocol definitions. use std::collections::{BTreeMap, BTreeSet}; use base64::{engine::general_purpose::STANDARD, Engine as _}; use serde::{Deserialize, Serialize}; use serde_with::base64::{Base64, UrlSafe}; use serde_with::formats::SpaceSeparator; use serde_with::{formats, serde_as, skip_serializing_none, StringWithSeparator}; use url::Url; use uuid::Uuid; /// How many seconds a device code is valid for. pub const OAUTH2_DEVICE_CODE_EXPIRY_SECONDS: u64 = 300; /// How often a client device can query the status of the token pub const OAUTH2_DEVICE_CODE_INTERVAL_SECONDS: u64 = 5; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub enum CodeChallengeMethod { // default to plain if not requested as S256. Reject the auth? // plain // BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) S256, } #[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct PkceRequest { #[serde_as(as = "Base64")] pub code_challenge: Vec, pub code_challenge_method: CodeChallengeMethod, } /// An OAuth2 client redirects to the authorisation server with Authorisation Request /// parameters. #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct AuthorisationRequest { // Must be "code". (or token, see 4.2.1) pub response_type: String, pub client_id: String, pub state: String, #[serde(flatten)] pub pkce_request: Option, pub redirect_uri: Url, pub scope: String, // OIDC adds a nonce parameter that is optional. pub nonce: Option, // OIDC also allows other optional params #[serde(flatten)] pub oidc_ext: AuthorisationRequestOidc, // Needs to be hoisted here due to serde flatten bug #3185 pub max_age: Option, #[serde(flatten)] pub unknown_keys: BTreeMap, } /// An OIDC client redirects to the authorisation server with Authorisation Request /// parameters. #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct AuthorisationRequestOidc { pub display: Option, pub prompt: Option, pub ui_locales: Option<()>, pub claims_locales: Option<()>, pub id_token_hint: Option, pub login_hint: Option, pub acr: Option, } /// In response to an Authorisation request, the user may be prompted to consent to the /// scopes requested by the OAuth2 client. If they have previously consented, they will /// immediately proceed. #[derive(Serialize, Deserialize, Debug, Clone)] pub enum AuthorisationResponse { ConsentRequested { // A pretty-name of the client client_name: String, // A list of scopes requested / to be issued. scopes: BTreeSet, // Extra PII that may be requested pii_scopes: BTreeSet, // The users displayname (?) // pub display_name: String, // The token we need to be given back to allow this to proceed consent_token: String, }, Permitted, } #[serde_as] #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "grant_type", rename_all = "snake_case")] pub enum GrantTypeReq { AuthorizationCode { // As sent by the authorisationCode code: String, // Must be the same as the original redirect uri. redirect_uri: Url, code_verifier: Option, }, ClientCredentials { #[serde_as(as = "Option>")] scope: Option>, }, RefreshToken { refresh_token: String, #[serde_as(as = "Option>")] scope: Option>, }, /// ref #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")] DeviceCode { device_code: String, // #[serde_as(as = "Option>")] scope: Option>, }, } /// An Access Token request. This requires a set of grant-type parameters to satisfy the request. #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug)] pub struct AccessTokenRequest { #[serde(flatten)] pub grant_type: GrantTypeReq, // REQUIRED, if the client is not authenticating with the // authorization server as described in Section 3.2.1. pub client_id: Option, pub client_secret: Option, } impl From for AccessTokenRequest { fn from(req: GrantTypeReq) -> AccessTokenRequest { AccessTokenRequest { grant_type: req, client_id: None, client_secret: None, } } } #[derive(Serialize, Debug, Clone, Deserialize)] #[skip_serializing_none] pub struct OAuth2RFC9068Token where V: Clone, { /// The issuer of this token pub iss: String, /// Unique id of the subject pub sub: Uuid, /// client_id of the oauth2 rp pub aud: String, /// Expiry in UTC epoch seconds pub exp: i64, /// Not valid before. pub nbf: i64, /// Issued at time. pub iat: i64, /// -- NOT used, but part of the spec. pub jti: Option, pub client_id: String, #[serde(flatten)] pub extensions: V, } /// Extensions for RFC 9068 Access Token #[serde_as] #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct OAuth2RFC9068TokenExtensions { pub auth_time: Option, pub acr: Option, pub amr: Option>, #[serde_as(as = "StringWithSeparator::")] pub scope: BTreeSet, pub nonce: Option, pub session_id: Uuid, pub parent_session_id: Option, } /// The response for an access token #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug)] pub struct AccessTokenResponse { pub access_token: String, pub token_type: AccessTokenType, /// Expiration relative to `now` in seconds. pub expires_in: u32, pub refresh_token: Option, /// Space separated list of scopes that were approved, if this differs from the /// original request. pub scope: Option, /// If the `openid` scope was requested, an `id_token` may be present in the response. pub id_token: Option, } /// Access token types, per [IANA Registry - OAuth Access Token Types](https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-types) #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(try_from = "&str")] pub enum AccessTokenType { Bearer, PoP, #[serde(rename = "N_A")] NA, DPoP, } impl TryFrom<&str> for AccessTokenType { type Error = String; fn try_from(s: &str) -> Result { match s.to_lowercase().as_str() { "bearer" => Ok(AccessTokenType::Bearer), "pop" => Ok(AccessTokenType::PoP), "n_a" => Ok(AccessTokenType::NA), "dpop" => Ok(AccessTokenType::DPoP), _ => Err(format!("Unknown AccessTokenType: {}", s)), } } } /// Request revocation of an Access or Refresh token. On success the response is OK 200 /// with no body. #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug)] pub struct TokenRevokeRequest { pub token: String, /// Not required for Kanidm. /// pub token_type_hint: Option, } /// Request to introspect the identity of the account associated to a token. #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug)] pub struct AccessTokenIntrospectRequest { pub token: String, /// Not required for Kanidm. /// pub token_type_hint: Option, } /// Response to an introspection request. If the token is inactive or revoked, only /// `active` will be set to the value of `false`. #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug)] pub struct AccessTokenIntrospectResponse { pub active: bool, pub scope: Option, pub client_id: Option, pub username: Option, pub token_type: Option, pub exp: Option, pub iat: Option, pub nbf: Option, pub sub: Option, pub aud: Option, pub iss: Option, pub jti: Option, } impl AccessTokenIntrospectResponse { pub fn inactive() -> Self { AccessTokenIntrospectResponse { active: false, scope: None, client_id: None, username: None, token_type: None, exp: None, iat: None, nbf: None, sub: None, aud: None, iss: None, jti: None, } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ResponseType { Code, Token, IdToken, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ResponseMode { Query, Fragment, } fn response_modes_supported_default() -> Vec { vec![ResponseMode::Query, ResponseMode::Fragment] } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum GrantType { #[serde(rename = "authorization_code")] AuthorisationCode, Implicit, } fn grant_types_supported_default() -> Vec { vec![GrantType::AuthorisationCode, GrantType::Implicit] } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum SubjectType { Pairwise, Public, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub enum PkceAlg { S256, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "UPPERCASE")] /// Algorithms supported for token signatures. Prefers `ES256` pub enum IdTokenSignAlg { // WE REFUSE TO SUPPORT NONE. DON'T EVEN ASK. IT WON'T HAPPEN. ES256, RS256, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum TokenEndpointAuthMethod { ClientSecretPost, ClientSecretBasic, ClientSecretJwt, PrivateKeyJwt, } fn token_endpoint_auth_methods_supported_default() -> Vec { vec![TokenEndpointAuthMethod::ClientSecretBasic] } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum DisplayValue { Page, Popup, Touch, Wap, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "snake_case")] // https://openid.net/specs/openid-connect-core-1_0.html#ClaimTypes pub enum ClaimType { Normal, Aggregated, Distributed, } fn claim_types_supported_default() -> Vec { vec![ClaimType::Normal] } fn claims_parameter_supported_default() -> bool { false } fn request_parameter_supported_default() -> bool { false } fn request_uri_parameter_supported_default() -> bool { false } fn require_request_uri_parameter_supported_default() -> bool { false } /// The response to an OpenID connect discovery request /// #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug)] pub struct OidcDiscoveryResponse { pub issuer: Url, pub authorization_endpoint: Url, pub token_endpoint: Url, pub userinfo_endpoint: Option, pub jwks_uri: Url, pub registration_endpoint: Option, pub scopes_supported: Option>, // https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1 pub response_types_supported: Vec, // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes #[serde(default = "response_modes_supported_default")] pub response_modes_supported: Vec, // Need to fill in as authorization_code only else a default is assumed. #[serde(default = "grant_types_supported_default")] pub grant_types_supported: Vec, pub acr_values_supported: Option>, // https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg pub subject_types_supported: Vec, pub id_token_signing_alg_values_supported: Vec, pub id_token_encryption_alg_values_supported: Option>, pub id_token_encryption_enc_values_supported: Option>, pub userinfo_signing_alg_values_supported: Option>, pub userinfo_encryption_alg_values_supported: Option>, pub userinfo_encryption_enc_values_supported: Option>, pub request_object_signing_alg_values_supported: Option>, pub request_object_encryption_alg_values_supported: Option>, pub request_object_encryption_enc_values_supported: Option>, // Defaults to client_secret_basic #[serde(default = "token_endpoint_auth_methods_supported_default")] pub token_endpoint_auth_methods_supported: Vec, pub token_endpoint_auth_signing_alg_values_supported: Option>, // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest pub display_values_supported: Option>, // Default to normal. #[serde(default = "claim_types_supported_default")] pub claim_types_supported: Vec, pub claims_supported: Option>, pub service_documentation: Option, pub claims_locales_supported: Option>, pub ui_locales_supported: Option>, // Default false. #[serde(default = "claims_parameter_supported_default")] pub claims_parameter_supported: bool, pub op_policy_uri: Option, pub op_tos_uri: Option, // these are related to RFC9101 JWT-Secured Authorization Request support #[serde(default = "request_parameter_supported_default")] pub request_parameter_supported: bool, #[serde(default = "request_uri_parameter_supported_default")] pub request_uri_parameter_supported: bool, #[serde(default = "require_request_uri_parameter_supported_default")] pub require_request_uri_registration: bool, pub code_challenge_methods_supported: Vec, // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse // "content type that contains a set of Claims as its members that are a subset of the Metadata // values defined in Section 3. Other Claims MAY also be returned. " // // In addition, we also return the following claims in kanidm // rfc7009 pub revocation_endpoint: Option, pub revocation_endpoint_auth_methods_supported: Vec, // rfc7662 pub introspection_endpoint: Option, pub introspection_endpoint_auth_methods_supported: Vec, pub introspection_endpoint_auth_signing_alg_values_supported: Option>, /// Ref pub device_authorization_endpoint: Option, } /// The response to an OAuth2 rfc8414 metadata request #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug)] pub struct Oauth2Rfc8414MetadataResponse { pub issuer: Url, pub authorization_endpoint: Url, pub token_endpoint: Url, pub jwks_uri: Option, // rfc7591 reg endpoint. pub registration_endpoint: Option, pub scopes_supported: Option>, // For Oauth2 should be Code, Token. pub response_types_supported: Vec, #[serde(default = "response_modes_supported_default")] pub response_modes_supported: Vec, #[serde(default = "grant_types_supported_default")] pub grant_types_supported: Vec, #[serde(default = "token_endpoint_auth_methods_supported_default")] pub token_endpoint_auth_methods_supported: Vec, pub token_endpoint_auth_signing_alg_values_supported: Option>, pub service_documentation: Option, pub ui_locales_supported: Option>, pub op_policy_uri: Option, pub op_tos_uri: Option, // rfc7009 pub revocation_endpoint: Option, pub revocation_endpoint_auth_methods_supported: Vec, // rfc7662 pub introspection_endpoint: Option, pub introspection_endpoint_auth_methods_supported: Vec, pub introspection_endpoint_auth_signing_alg_values_supported: Option>, // RFC7636 pub code_challenge_methods_supported: Vec, } #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Default)] pub struct ErrorResponse { pub error: String, pub error_description: Option, pub error_uri: Option, } #[derive(Debug, Serialize, Deserialize)] /// Ref pub struct DeviceAuthorizationResponse { /// Base64-encoded bundle of 16 bytes device_code: String, /// 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. user_code: String, verification_uri: Url, verification_uri_complete: Url, expires_in: u64, interval: u64, } impl DeviceAuthorizationResponse { pub fn new(verification_uri: Url, device_code: [u8; 16], user_code: String) -> Self { let mut verification_uri_complete = verification_uri.clone(); verification_uri_complete .query_pairs_mut() .append_pair("user_code", &user_code); let device_code = STANDARD.encode(device_code); Self { verification_uri_complete, device_code, user_code, verification_uri, expires_in: OAUTH2_DEVICE_CODE_EXPIRY_SECONDS, interval: OAUTH2_DEVICE_CODE_INTERVAL_SECONDS, } } } #[cfg(test)] mod tests { use super::{AccessTokenRequest, GrantTypeReq}; use url::Url; #[test] fn test_oauth2_access_token_req() { let atr: AccessTokenRequest = GrantTypeReq::AuthorizationCode { code: "demo code".to_string(), redirect_uri: Url::parse("http://[::1]").unwrap(), code_verifier: None, } .into(); println!("{:?}", serde_json::to_string(&atr).expect("JSON failure")); } #[test] fn test_oauth2_access_token_type_serde() { for testcase in ["bearer", "Bearer", "BeArEr"] { let at: super::AccessTokenType = serde_json::from_str(&format!("\"{}\"", testcase)).expect("Failed to parse"); assert_eq!(at, super::AccessTokenType::Bearer); } for testcase in ["dpop", "dPoP", "DPOP", "DPoP"] { let at: super::AccessTokenType = serde_json::from_str(&format!("\"{}\"", testcase)).expect("Failed to parse"); assert_eq!(at, super::AccessTokenType::DPoP); } { let testcase = "cheese"; let at = serde_json::from_str::(&format!("\"{}\"", testcase)); assert!(at.is_err()) } } }