pub mod jwt;
pub mod nonce;
#[cfg(feature = "es256")]
pub mod es256;
#[cfg(feature = "eddsa")]
pub mod eddsa;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResponseType {
#[serde(rename = "id_token")]
IdToken,
#[serde(rename = "vp_token")]
VpToken,
#[serde(rename = "vp_token id_token")]
VpTokenIdToken,
#[serde(rename = "code")]
Code,
}
impl std::fmt::Display for ResponseType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IdToken => write!(f, "id_token"),
Self::VpToken => write!(f, "vp_token"),
Self::VpTokenIdToken => write!(f, "vp_token id_token"),
Self::Code => write!(f, "code"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResponseMode {
#[serde(rename = "fragment")]
Fragment,
#[serde(rename = "direct_post")]
DirectPost,
#[serde(rename = "direct_post.jwt")]
DirectPostJwt,
}
impl std::fmt::Display for ResponseMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Fragment => write!(f, "fragment"),
Self::DirectPost => write!(f, "direct_post"),
Self::DirectPostJwt => write!(f, "direct_post.jwt"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SubjectSyntaxType {
JwkThumbprint,
Did(String),
}
impl SubjectSyntaxType {
pub const JWK_THUMBPRINT_URN: &'static str = "urn:ietf:params:oauth:jwk-thumbprint";
pub fn parse(s: &str) -> Self {
if s == Self::JWK_THUMBPRINT_URN {
Self::JwkThumbprint
} else {
Self::Did(s.to_string())
}
}
pub fn as_str(&self) -> &str {
match self {
Self::JwkThumbprint => Self::JWK_THUMBPRINT_URN,
Self::Did(method) => method,
}
}
pub fn is_did(&self) -> bool {
matches!(self, Self::Did(_))
}
}
impl std::fmt::Display for SubjectSyntaxType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
pub fn compute_jwk_thumbprint(jwk: &Value) -> Option<String> {
let kty = jwk.get("kty")?.as_str()?;
let canonical = match kty {
"EC" => {
let crv = jwk.get("crv")?.as_str()?;
let x = jwk.get("x")?.as_str()?;
let y = jwk.get("y")?.as_str()?;
format!(r#"{{"crv":"{crv}","kty":"EC","x":"{x}","y":"{y}"}}"#)
}
"OKP" => {
let crv = jwk.get("crv")?.as_str()?;
let x = jwk.get("x")?.as_str()?;
format!(r#"{{"crv":"{crv}","kty":"OKP","x":"{x}"}}"#)
}
"RSA" => {
let e = jwk.get("e")?.as_str()?;
let n = jwk.get("n")?.as_str()?;
format!(r#"{{"e":"{e}","kty":"RSA","n":"{n}"}}"#)
}
_ => return None,
};
let hash = Sha256::digest(canonical.as_bytes());
Some(URL_SAFE_NO_PAD.encode(hash))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayProperties {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logo: Option<LogoProperties>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_color: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogoProperties {
pub uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub alt_text: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub subject_syntax_types_supported: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_token_signed_response_alg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect_uris: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub policy_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tos_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logo_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_name: Option<String>,
#[serde(flatten)]
pub additional: serde_json::Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
#[non_exhaustive]
pub enum OAuthError {
#[error("invalid_request")]
#[serde(rename = "invalid_request")]
InvalidRequest,
#[error("unauthorized_client")]
#[serde(rename = "unauthorized_client")]
UnauthorizedClient,
#[error("access_denied")]
#[serde(rename = "access_denied")]
AccessDenied,
#[error("unsupported_response_type")]
#[serde(rename = "unsupported_response_type")]
UnsupportedResponseType,
#[error("invalid_scope")]
#[serde(rename = "invalid_scope")]
InvalidScope,
#[error("server_error")]
#[serde(rename = "server_error")]
ServerError,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn jwk_thumbprint_ec_p256() {
let jwk = json!({
"kty": "EC",
"crv": "P-256",
"x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
"y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"
});
let thumbprint = compute_jwk_thumbprint(&jwk).unwrap();
assert!(!thumbprint.is_empty());
assert_eq!(thumbprint.len(), 43);
}
#[test]
fn jwk_thumbprint_okp_ed25519() {
let jwk = json!({
"kty": "OKP",
"crv": "Ed25519",
"x": "Xx4_L89E6RsyvDTzN9wuN3cDwgifPkXMgFJv_HMIxdk"
});
let thumbprint = compute_jwk_thumbprint(&jwk).unwrap();
assert_eq!(thumbprint.len(), 43);
}
#[test]
fn jwk_thumbprint_deterministic() {
let jwk = json!({"kty": "EC", "crv": "P-256", "x": "abc", "y": "def"});
let t1 = compute_jwk_thumbprint(&jwk).unwrap();
let t2 = compute_jwk_thumbprint(&jwk).unwrap();
assert_eq!(t1, t2);
}
#[test]
fn jwk_thumbprint_different_keys() {
let jwk1 = json!({"kty": "EC", "crv": "P-256", "x": "a", "y": "b"});
let jwk2 = json!({"kty": "EC", "crv": "P-256", "x": "c", "y": "d"});
assert_ne!(compute_jwk_thumbprint(&jwk1), compute_jwk_thumbprint(&jwk2));
}
#[test]
fn jwk_thumbprint_ignores_extra_fields() {
let jwk1 = json!({"kty": "EC", "crv": "P-256", "x": "a", "y": "b"});
let jwk2 = json!({"kty": "EC", "crv": "P-256", "x": "a", "y": "b", "kid": "extra"});
assert_eq!(compute_jwk_thumbprint(&jwk1), compute_jwk_thumbprint(&jwk2));
}
#[test]
fn jwk_thumbprint_unsupported_kty() {
let jwk = json!({"kty": "oct", "k": "secret"});
assert!(compute_jwk_thumbprint(&jwk).is_none());
}
#[test]
fn response_type_display() {
assert_eq!(ResponseType::IdToken.to_string(), "id_token");
assert_eq!(ResponseType::VpToken.to_string(), "vp_token");
assert_eq!(
ResponseType::VpTokenIdToken.to_string(),
"vp_token id_token"
);
}
#[test]
fn response_mode_display() {
assert_eq!(ResponseMode::Fragment.to_string(), "fragment");
assert_eq!(ResponseMode::DirectPost.to_string(), "direct_post");
assert_eq!(ResponseMode::DirectPostJwt.to_string(), "direct_post.jwt");
}
#[test]
fn subject_syntax_type_parsing() {
let jwk = SubjectSyntaxType::parse("urn:ietf:params:oauth:jwk-thumbprint");
assert_eq!(jwk, SubjectSyntaxType::JwkThumbprint);
assert!(!jwk.is_did());
let did = SubjectSyntaxType::parse("did:key");
assert!(did.is_did());
assert_eq!(did.as_str(), "did:key");
}
#[test]
fn client_metadata_serialization() {
let meta = ClientMetadata {
client_name: Some("Test RP".into()),
subject_syntax_types_supported: Some(vec![
"urn:ietf:params:oauth:jwk-thumbprint".into(),
"did:key".into(),
]),
..Default::default()
};
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("Test RP"));
assert!(json.contains("did:key"));
let parsed: ClientMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.client_name.as_deref(), Some("Test RP"));
}
}