use std::fmt;
use base64::Engine;
use serde::Deserialize;
use sha2::Digest;
#[derive(Debug, Clone, Deserialize)]
pub struct OidcProviderMetadata {
pub issuer: String,
pub authorization_endpoint: String,
pub token_endpoint: String,
pub jwks_uri: String,
pub userinfo_endpoint: Option<String>,
#[serde(default)]
pub response_types_supported: Vec<String>,
#[serde(default)]
pub code_challenge_methods_supported: Vec<String>,
#[serde(default)]
pub id_token_signing_alg_values_supported: Vec<String>,
}
#[derive(Clone)]
pub struct OidcAuthorizationRequest {
pub url: String,
pub state: String,
pub nonce: String,
pub pkce: Option<PkcePair>,
}
impl fmt::Debug for OidcAuthorizationRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OidcAuthorizationRequest")
.field("url", &"<redacted>")
.field("state", &"<redacted>")
.field("nonce", &"<redacted>")
.field("pkce", &self.pkce.as_ref().map(|_| "<redacted>"))
.finish()
}
}
#[derive(Clone)]
pub struct OidcTokenExchangeRequest {
pub token_endpoint: String,
pub code: String,
pub redirect_uri: String,
pub state: String,
pub code_verifier: Option<String>,
}
impl fmt::Debug for OidcTokenExchangeRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OidcTokenExchangeRequest")
.field("token_endpoint", &self.token_endpoint)
.field("code", &"<redacted>")
.field("redirect_uri", &self.redirect_uri)
.field("state", &"<redacted>")
.field(
"code_verifier",
&self.code_verifier.as_ref().map(|_| "<redacted>"),
)
.finish()
}
}
#[derive(Clone)]
pub struct PkcePair {
pub verifier: String,
pub challenge: String,
pub method: &'static str,
}
impl fmt::Debug for PkcePair {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PkcePair")
.field("verifier", &"<redacted>")
.field("challenge", &"<redacted>")
.field("method", &self.method)
.finish()
}
}
impl PkcePair {
#[must_use]
pub fn generate() -> Self {
let mut bytes = [0_u8; 32];
rand::fill(&mut bytes);
let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode(sha2::Sha256::digest(verifier.as_bytes()));
Self {
verifier,
challenge,
method: "S256",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OidcClaims {
pub sub: String,
pub iss: String,
pub aud: Vec<String>,
pub exp: u64,
pub iat: u64,
pub nbf: Option<u64>,
pub nonce: Option<String>,
pub email: Option<String>,
pub email_verified: Option<bool>,
pub name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub(super) struct RawOidcClaims {
pub(super) sub: String,
pub(super) iss: String,
pub(super) aud: Audiences,
pub(super) exp: u64,
pub(super) iat: Option<u64>,
pub(super) nbf: Option<u64>,
pub(super) nonce: Option<String>,
pub(super) email: Option<String>,
pub(super) email_verified: Option<bool>,
pub(super) name: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct OidcUserInfo {
pub sub: String,
pub email: Option<String>,
pub email_verified: Option<bool>,
pub name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub(super) enum Audiences {
One(String),
Many(Vec<String>),
}
impl Audiences {
pub(super) fn into_vec(self) -> Vec<String> {
match self {
Self::One(value) => vec![value],
Self::Many(values) => values,
}
}
}
#[cfg(test)]
mod tests {
use super::{OidcAuthorizationRequest, OidcTokenExchangeRequest, PkcePair};
#[test]
fn oidc_debug_masks_callback_secrets() {
let pkce = PkcePair {
verifier: "verifier-secret".into(),
challenge: "challenge-secret".into(),
method: "S256",
};
let authorization = OidcAuthorizationRequest {
url: "https://issuer/authorize?state=state-secret&nonce=nonce-secret".into(),
state: "state-secret".into(),
nonce: "nonce-secret".into(),
pkce: Some(pkce.clone()),
};
let exchange = OidcTokenExchangeRequest {
token_endpoint: "https://issuer/token".into(),
code: "code-secret".into(),
redirect_uri: "https://app/callback".into(),
state: "state-secret".into(),
code_verifier: Some(pkce.verifier.clone()),
};
let formatted = format!("{authorization:?} {exchange:?} {pkce:?}");
for secret in [
"state-secret",
"nonce-secret",
"verifier-secret",
"challenge-secret",
"code-secret",
] {
assert!(!formatted.contains(secret), "{formatted}");
}
}
}