use std::fmt;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::grant::GrantType;
use crate::hex::encode as hex_lower;
use crate::scope::ScopeSet;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ClientId(String);
impl ClientId {
pub fn new(id: impl Into<String>) -> Self {
ClientId(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ClientId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecretHash {
scheme: String,
encoded: String,
}
impl fmt::Debug for SecretHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SecretHash")
.field("scheme", &self.scheme)
.field("encoded", &"[redacted]")
.finish()
}
}
impl SecretHash {
pub const SHA256_HEX: &'static str = "sha256-hex";
pub fn sha256(secret: &str) -> Self {
SecretHash {
scheme: SecretHash::SHA256_HEX.to_string(),
encoded: hex_lower(&Sha256::digest(secret.as_bytes())),
}
}
pub fn custom(scheme: impl Into<String>, encoded: impl Into<String>) -> Self {
SecretHash {
scheme: scheme.into(),
encoded: encoded.into(),
}
}
pub fn scheme(&self) -> &str {
&self.scheme
}
pub fn encoded(&self) -> &str {
&self.encoded
}
fn verify_builtin(&self, presented: &str) -> bool {
if self.scheme != SecretHash::SHA256_HEX {
return false;
}
let computed = hex_lower(&Sha256::digest(presented.as_bytes()));
constant_time_eq(computed.as_bytes(), self.encoded.as_bytes())
}
pub fn verify(&self, presented: &str, verifier: Option<&dyn SecretVerifier>) -> bool {
if self.scheme == SecretHash::SHA256_HEX {
self.verify_builtin(presented)
} else {
match verifier {
Some(v) => v.verify(self, presented),
None => false,
}
}
}
}
pub trait SecretVerifier: Send + Sync {
fn verify(&self, stored: &SecretHash, presented: &str) -> bool;
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ClientAuth {
Public,
ConfidentialSecret {
secret: String,
},
ConfidentialSecretHash {
hash: SecretHash,
},
#[cfg(feature = "client_assertion")]
ConfidentialAssertion {
keys: crate::client_assertion::AssertionKeys,
},
#[cfg(feature = "mtls")]
Mtls {
registration: crate::mtls::MtlsClientRegistration,
},
}
impl fmt::Debug for ClientAuth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ClientAuth::Public => f.write_str("Public"),
ClientAuth::ConfidentialSecret { secret: _ } => f
.debug_struct("ConfidentialSecret")
.field("secret", &"[redacted]")
.finish(),
ClientAuth::ConfidentialSecretHash { hash } => f
.debug_struct("ConfidentialSecretHash")
.field("hash", hash)
.finish(),
#[cfg(feature = "client_assertion")]
ClientAuth::ConfidentialAssertion { keys } => f
.debug_struct("ConfidentialAssertion")
.field("keys", keys)
.finish(),
#[cfg(feature = "mtls")]
ClientAuth::Mtls { registration } => f
.debug_struct("Mtls")
.field("registration", registration)
.finish(),
}
}
}
impl ClientAuth {
pub fn is_confidential(&self) -> bool {
!matches!(self, ClientAuth::Public)
}
pub fn verify(&self, presented: Option<&str>) -> bool {
self.verify_with(presented, None)
}
pub fn verify_with(
&self,
presented: Option<&str>,
verifier: Option<&dyn SecretVerifier>,
) -> bool {
match self {
ClientAuth::Public => presented.is_none(),
ClientAuth::ConfidentialSecret { secret } => match presented {
Some(p) => constant_time_eq(secret.as_bytes(), p.as_bytes()),
None => false,
},
ClientAuth::ConfidentialSecretHash { hash } => match presented {
Some(p) => hash.verify(p, verifier),
None => false,
},
#[cfg(feature = "client_assertion")]
ClientAuth::ConfidentialAssertion { .. } => false,
#[cfg(feature = "mtls")]
ClientAuth::Mtls { .. } => false,
}
}
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
let da = Sha256::digest(a);
let db = Sha256::digest(b);
let mut acc: u8 = 0;
for i in 0..32 {
acc |= da[i] ^ db[i];
}
acc == 0
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Client {
pub client_id: ClientId,
pub auth: ClientAuth,
pub grant_types: Vec<GrantType>,
pub redirect_uris: Vec<String>,
pub allowed_scopes: ScopeSet,
pub default_scopes: ScopeSet,
pub name: Option<String>,
pub registration: Option<Box<DynamicRegistration>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DynamicRegistration {
pub registration_access_token_hash: SecretHash,
pub client_id_issued_at: Option<u64>,
pub client_secret_expires_at: Option<u64>,
pub token_endpoint_auth_method: String,
}
impl Client {
pub fn allows_grant(&self, grant_type: GrantType) -> bool {
self.grant_types.contains(&grant_type)
}
}
#[cfg(test)]
#[path = "tests/client.rs"]
mod tests;