use crate::compact::{JwaAlg, Jwk, JwsCompact, JwsCompactVerifyData};
use crate::jws::{Jws, JwsCompactSign2Data, JwsSigned};
use crate::traits::{JwsSignable, JwsVerifiable};
use crate::error::JwtError;
use crate::{btreemap_empty, vec_empty};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use url::Url;
use uuid::Uuid;
pub struct OidcUnverified {
jwsc: JwsCompact,
}
pub struct OidcExpUnverified {
oidc: OidcToken,
}
pub struct OidcSigned {
jws: JwsSigned,
}
#[derive(Debug, Serialize, Clone, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum OidcSubject {
U(Uuid),
S(String),
}
impl fmt::Display for OidcSubject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OidcSubject::U(u) => write!(f, "{}", u),
OidcSubject::S(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Default)]
pub struct OidcClaims {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preferred_username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email_verified: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub zoneinfo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
#[serde(skip_serializing_if = "vec_empty", default)]
pub scopes: Vec<String>,
}
#[derive(Debug, Serialize, Clone, Deserialize, PartialEq)]
pub struct OidcToken {
pub iss: Url,
pub sub: OidcSubject,
pub aud: String,
pub exp: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub nbf: Option<i64>,
pub iat: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_time: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub at_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub acr: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amr: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub azp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(flatten)]
pub s_claims: OidcClaims,
#[serde(flatten, skip_serializing_if = "btreemap_empty")]
pub claims: BTreeMap<String, serde_json::value::Value>,
}
impl JwsSignable for OidcToken {
type Signed = OidcSigned;
fn data(&self) -> Result<JwsCompactSign2Data, JwtError> {
let mut jwts = Jws::into_json(self).map_err(|_| JwtError::InvalidJwt)?;
jwts.set_typ(Some("JWT"));
jwts.data()
}
fn post_process(&self, jwsc: JwsCompact) -> Result<Self::Signed, JwtError> {
Ok(OidcSigned {
jws: JwsSigned { jwsc },
})
}
}
impl OidcUnverified {
pub fn get_jwk_pubkey(&self) -> Option<&Jwk> {
self.jwsc.get_jwk_pubkey()
}
}
impl FromStr for OidcUnverified {
type Err = JwtError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
JwsCompact::from_str(s).map(|jwsc| OidcUnverified { jwsc })
}
}
impl JwsVerifiable for OidcUnverified {
type Verified = OidcExpUnverified;
fn data(&self) -> JwsCompactVerifyData<'_> {
self.jwsc.data()
}
fn alg(&self) -> JwaAlg {
self.jwsc.alg()
}
fn kid(&self) -> Option<&str> {
self.jwsc.kid()
}
fn post_process(&self, value: Jws) -> Result<Self::Verified, JwtError> {
let oidc: OidcToken = value.from_json().map_err(|_| JwtError::InvalidJwt)?;
Ok(OidcExpUnverified { oidc })
}
}
impl OidcExpUnverified {
pub fn verify_exp(self, curtime: i64) -> Result<OidcToken, JwtError> {
if self.oidc.exp != 0 && self.oidc.exp < curtime {
Err(JwtError::OidcTokenExpired)
} else {
Ok(self.oidc)
}
}
}
impl OidcSigned {
pub fn invalidate(self) -> OidcUnverified {
OidcUnverified {
jwsc: self.jws.jwsc,
}
}
}
impl fmt::Display for OidcSigned {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.jws.fmt(f)
}
}
#[cfg(all(feature = "openssl", test))]
mod tests {
use super::{OidcSubject, OidcToken};
use crate::crypto::JwsEs256Signer;
use crate::traits::{JwsSigner, JwsSignerToVerifier, JwsVerifier};
use url::Url;
#[test]
fn test_sign_and_validate() {
let _ = tracing_subscriber::fmt::try_init();
let jwt = OidcToken {
iss: Url::parse("https://oidc.example.com").unwrap(),
sub: OidcSubject::S("a unique id".to_string()),
aud: "test".to_string(),
exp: 0,
nbf: Some(0),
iat: 0,
auth_time: None,
nonce: None,
at_hash: None,
acr: None,
amr: None,
azp: None,
jti: None,
s_claims: Default::default(),
claims: Default::default(),
};
let jws_es256_signer =
JwsEs256Signer::generate_es256().expect("failed to construct signer.");
let jwk_es256_verifier = jws_es256_signer
.get_verifier()
.expect("failed to get verifier from signer");
let jwts = jws_es256_signer.sign(&jwt).expect("failed to sign jwt");
let jwtu = jwts.invalidate();
let released = jwk_es256_verifier
.verify(&jwtu)
.expect("Unable to validate jwt")
.verify_exp(0)
.expect("Unable to validate oidc exp");
assert!(released == jwt);
}
}