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;
pub const OAUTH2_DEVICE_CODE_EXPIRY_SECONDS: u64 = 300;
pub const OAUTH2_DEVICE_CODE_INTERVAL_SECONDS: u64 = 5;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
pub enum CodeChallengeMethod {
S256,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PkceRequest {
#[serde_as(as = "Base64<UrlSafe, formats::Unpadded>")]
pub code_challenge: Vec<u8>,
pub code_challenge_method: CodeChallengeMethod,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuthorisationRequest {
pub response_type: String,
pub client_id: String,
pub state: String,
#[serde(flatten)]
pub pkce_request: Option<PkceRequest>,
pub redirect_uri: Url,
pub scope: String,
pub nonce: Option<String>,
#[serde(flatten)]
pub oidc_ext: AuthorisationRequestOidc,
pub max_age: Option<i64>,
#[serde(flatten)]
pub unknown_keys: BTreeMap<String, serde_json::value::Value>,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct AuthorisationRequestOidc {
pub display: Option<String>,
pub prompt: Option<String>,
pub ui_locales: Option<()>,
pub claims_locales: Option<()>,
pub id_token_hint: Option<String>,
pub login_hint: Option<String>,
pub acr: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum AuthorisationResponse {
ConsentRequested {
client_name: String,
scopes: BTreeSet<String>,
pii_scopes: BTreeSet<String>,
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 {
code: String,
redirect_uri: Url,
code_verifier: Option<String>,
},
ClientCredentials {
#[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
scope: Option<BTreeSet<String>>,
},
RefreshToken {
refresh_token: String,
#[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
scope: Option<BTreeSet<String>>,
},
#[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
DeviceCode {
device_code: String,
scope: Option<BTreeSet<String>>,
},
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessTokenRequest {
#[serde(flatten)]
pub grant_type: GrantTypeReq,
pub client_id: Option<String>,
pub client_secret: Option<String>,
}
impl From<GrantTypeReq> 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<V>
where
V: Clone,
{
pub iss: String,
pub sub: Uuid,
pub aud: String,
pub exp: i64,
pub nbf: i64,
pub iat: i64,
pub jti: Option<String>,
pub client_id: String,
#[serde(flatten)]
pub extensions: V,
}
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OAuth2RFC9068TokenExtensions {
pub auth_time: Option<i64>,
pub acr: Option<String>,
pub amr: Option<Vec<String>>,
#[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
pub scope: BTreeSet<String>,
pub nonce: Option<String>,
pub session_id: Uuid,
pub parent_session_id: Option<Uuid>,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessTokenResponse {
pub access_token: String,
pub token_type: AccessTokenType,
pub expires_in: u32,
pub refresh_token: Option<String>,
pub scope: Option<String>,
pub id_token: Option<String>,
}
#[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<Self, Self::Error> {
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)),
}
}
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)]
pub struct TokenRevokeRequest {
pub token: String,
pub token_type_hint: Option<String>,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessTokenIntrospectRequest {
pub token: String,
pub token_type_hint: Option<String>,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessTokenIntrospectResponse {
pub active: bool,
pub scope: Option<String>,
pub client_id: Option<String>,
pub username: Option<String>,
pub token_type: Option<AccessTokenType>,
pub exp: Option<i64>,
pub iat: Option<i64>,
pub nbf: Option<i64>,
pub sub: Option<String>,
pub aud: Option<String>,
pub iss: Option<String>,
pub jti: Option<String>,
}
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<ResponseMode> {
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<GrantType> {
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")]
pub enum IdTokenSignAlg {
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<TokenEndpointAuthMethod> {
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")]
pub enum ClaimType {
Normal,
Aggregated,
Distributed,
}
fn claim_types_supported_default() -> Vec<ClaimType> {
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
}
#[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<Url>,
pub jwks_uri: Url,
pub registration_endpoint: Option<Url>,
pub scopes_supported: Option<Vec<String>>,
pub response_types_supported: Vec<ResponseType>,
#[serde(default = "response_modes_supported_default")]
pub response_modes_supported: Vec<ResponseMode>,
#[serde(default = "grant_types_supported_default")]
pub grant_types_supported: Vec<GrantType>,
pub acr_values_supported: Option<Vec<String>>,
pub subject_types_supported: Vec<SubjectType>,
pub id_token_signing_alg_values_supported: Vec<IdTokenSignAlg>,
pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
pub id_token_encryption_enc_values_supported: Option<Vec<String>>,
pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
pub userinfo_encryption_alg_values_supported: Option<Vec<String>>,
pub userinfo_encryption_enc_values_supported: Option<Vec<String>>,
pub request_object_signing_alg_values_supported: Option<Vec<String>>,
pub request_object_encryption_alg_values_supported: Option<Vec<String>>,
pub request_object_encryption_enc_values_supported: Option<Vec<String>>,
#[serde(default = "token_endpoint_auth_methods_supported_default")]
pub token_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
pub display_values_supported: Option<Vec<DisplayValue>>,
#[serde(default = "claim_types_supported_default")]
pub claim_types_supported: Vec<ClaimType>,
pub claims_supported: Option<Vec<String>>,
pub service_documentation: Option<Url>,
pub claims_locales_supported: Option<Vec<String>>,
pub ui_locales_supported: Option<Vec<String>>,
#[serde(default = "claims_parameter_supported_default")]
pub claims_parameter_supported: bool,
pub op_policy_uri: Option<Url>,
pub op_tos_uri: Option<Url>,
#[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<PkceAlg>,
pub revocation_endpoint: Option<Url>,
pub revocation_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
pub introspection_endpoint: Option<Url>,
pub introspection_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
pub device_authorization_endpoint: Option<Url>,
}
#[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<Url>,
pub registration_endpoint: Option<Url>,
pub scopes_supported: Option<Vec<String>>,
pub response_types_supported: Vec<ResponseType>,
#[serde(default = "response_modes_supported_default")]
pub response_modes_supported: Vec<ResponseMode>,
#[serde(default = "grant_types_supported_default")]
pub grant_types_supported: Vec<GrantType>,
#[serde(default = "token_endpoint_auth_methods_supported_default")]
pub token_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
pub service_documentation: Option<Url>,
pub ui_locales_supported: Option<Vec<String>>,
pub op_policy_uri: Option<Url>,
pub op_tos_uri: Option<Url>,
pub revocation_endpoint: Option<Url>,
pub revocation_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
pub introspection_endpoint: Option<Url>,
pub introspection_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
pub code_challenge_methods_supported: Vec<PkceAlg>,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct ErrorResponse {
pub error: String,
pub error_description: Option<String>,
pub error_uri: Option<Url>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceAuthorizationResponse {
device_code: String,
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::<super::AccessTokenType>(&format!("\"{}\"", testcase));
assert!(at.is_err())
}
}
}