use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::{DateTime, Duration, Utc};
use rand::RngCore as _;
use serde::{Deserialize, Serialize};
use subtle::ConstantTimeEq as _;
pub(super) fn gen_random_token() -> String {
let mut bytes = [0u8; 32];
rand::rng().fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(bytes)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PKCEChallenge {
pub code_verifier: String,
pub code_challenge: String,
pub code_challenge_method: String,
}
impl PKCEChallenge {
#[must_use]
pub fn new() -> Self {
use sha2::{Digest, Sha256};
let verifier = gen_random_token();
let digest = Sha256::digest(verifier.as_bytes());
let challenge = URL_SAFE_NO_PAD.encode(digest);
Self {
code_verifier: verifier,
code_challenge: challenge,
code_challenge_method: "S256".to_string(),
}
}
#[must_use]
pub fn verify(&self, verifier: &str) -> bool {
use sha2::{Digest, Sha256};
let computed_challenge = URL_SAFE_NO_PAD.encode(Sha256::digest(verifier.as_bytes()));
computed_challenge.as_bytes().ct_eq(self.code_challenge.as_bytes()).into()
}
}
impl Default for PKCEChallenge {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateParameter {
pub state: String,
pub expires_at: DateTime<Utc>,
}
impl StateParameter {
#[must_use]
pub fn new() -> Self {
Self {
state: gen_random_token(),
expires_at: Utc::now() + Duration::minutes(10),
}
}
#[must_use]
pub fn is_expired(&self) -> bool {
self.expires_at <= Utc::now()
}
#[must_use]
pub fn verify(&self, provided_state: &str) -> bool {
let match_ok: bool = self.state.as_bytes().ct_eq(provided_state.as_bytes()).into();
match_ok && !self.is_expired()
}
}
impl Default for StateParameter {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NonceParameter {
pub nonce: String,
pub expires_at: DateTime<Utc>,
}
impl NonceParameter {
#[must_use]
pub fn new() -> Self {
Self {
nonce: gen_random_token(),
expires_at: Utc::now() + Duration::minutes(10),
}
}
#[must_use]
pub fn is_expired(&self) -> bool {
self.expires_at <= Utc::now()
}
#[must_use]
pub fn verify(&self, provided_nonce: &str) -> bool {
let match_ok: bool = self.nonce.as_bytes().ct_eq(provided_nonce.as_bytes()).into();
match_ok && !self.is_expired()
}
}
impl Default for NonceParameter {
fn default() -> Self {
Self::new()
}
}