use std::sync::Arc;
use serde::Deserialize;
use crate::claims::StandardClaims;
use crate::config::JwtVerifierConfig;
use crate::error::Result;
use crate::extractor::IdentityExtractor;
use crate::verifier::JwtVerifier;
#[derive(Debug, Deserialize)]
pub struct OktaClaims {
pub iss: String,
pub sub: String,
pub aud: OktaAudience,
pub exp: i64,
pub iat: Option<i64>,
pub uid: Option<String>,
pub cid: Option<String>,
pub scp: Option<Vec<String>>,
pub email: Option<String>,
pub email_verified: Option<bool>,
pub name: Option<String>,
pub preferred_username: Option<String>,
pub given_name: Option<String>,
pub family_name: Option<String>,
pub groups: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum OktaAudience {
Single(String),
Multiple(Vec<String>),
}
impl OktaAudience {
pub fn as_slice(&self) -> &[String] {
match self {
OktaAudience::Single(s) => std::slice::from_ref(s),
OktaAudience::Multiple(v) => v,
}
}
}
impl StandardClaims for OktaClaims {
fn iss(&self) -> &str {
&self.iss
}
fn sub(&self) -> &str {
&self.sub
}
fn aud(&self) -> &[String] {
self.aud.as_slice()
}
fn exp(&self) -> i64 {
self.exp
}
fn iat(&self) -> Option<i64> {
self.iat
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct OktaIdentity {
pub subject: Arc<str>,
pub user_id: Option<Arc<str>>,
pub client_id: Option<Arc<str>>,
pub email: Option<Arc<str>>,
pub name: Option<Arc<str>>,
pub groups: Option<Vec<Arc<str>>>,
}
#[derive(Clone, Debug)]
pub struct OktaExtractor;
impl IdentityExtractor for OktaExtractor {
type Claims = OktaClaims;
type Identity = OktaIdentity;
fn extract_identity(&self, claims: &Self::Claims) -> Result<Self::Identity> {
Ok(OktaIdentity {
subject: Arc::from(claims.sub.as_str()),
user_id: claims.uid.as_deref().map(Arc::from),
client_id: claims.cid.as_deref().map(Arc::from),
email: claims.email.as_deref().map(Arc::from),
name: claims.name.as_deref().map(Arc::from),
groups: claims
.groups
.as_ref()
.map(|g| g.iter().map(|s| Arc::from(s.as_str())).collect()),
})
}
}
pub type OktaJwtVerifier = JwtVerifier<OktaExtractor>;
impl OktaJwtVerifier {
pub async fn with_issuer(
expected_issuer: impl Into<String>,
audience: impl Into<String>,
) -> Result<Self> {
let config = JwtVerifierConfig::new(expected_issuer, audience);
Self::new(config, OktaExtractor).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_access_token_claims() {
let json = r#"{
"iss": "https://dev-123456.okta.com/oauth2/default",
"sub": "00u1234567890",
"aud": "api://default",
"exp": 1700000000,
"iat": 1699996400,
"cid": "0oa1234567890",
"uid": "00u1234567890",
"scp": ["openid", "profile", "email"],
"email": "user@example.com",
"email_verified": true,
"groups": ["Everyone", "Developers"]
}"#;
let claims: OktaClaims = serde_json::from_str(json).unwrap();
assert_eq!(claims.iss, "https://dev-123456.okta.com/oauth2/default");
assert_eq!(claims.sub, "00u1234567890");
assert_eq!(claims.aud.as_slice(), &["api://default".to_string()]);
assert_eq!(claims.exp, 1700000000);
assert_eq!(claims.iat, Some(1699996400));
assert_eq!(claims.cid.as_deref(), Some("0oa1234567890"));
assert_eq!(claims.uid.as_deref(), Some("00u1234567890"));
assert_eq!(
claims.scp,
Some(vec![
"openid".to_string(),
"profile".to_string(),
"email".to_string()
])
);
assert_eq!(claims.email.as_deref(), Some("user@example.com"));
assert_eq!(claims.email_verified, Some(true));
assert_eq!(
claims.groups,
Some(vec!["Everyone".to_string(), "Developers".to_string()])
);
}
#[test]
fn deserialize_id_token_claims() {
let json = r#"{
"iss": "https://dev-123456.okta.com/oauth2/default",
"sub": "00u1234567890",
"aud": ["0oa1234567890"],
"exp": 1700000000,
"email": "user@example.com",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe"
}"#;
let claims: OktaClaims = serde_json::from_str(json).unwrap();
assert_eq!(claims.aud.as_slice(), &["0oa1234567890".to_string()]);
assert_eq!(claims.name.as_deref(), Some("Jane Doe"));
assert_eq!(claims.given_name.as_deref(), Some("Jane"));
assert_eq!(claims.family_name.as_deref(), Some("Doe"));
assert!(claims.iat.is_none());
assert!(claims.uid.is_none());
assert!(claims.cid.is_none());
assert!(claims.scp.is_none());
assert!(claims.groups.is_none());
}
#[test]
fn standard_claims_implementation() {
let claims = OktaClaims {
iss: "https://dev-123456.okta.com/oauth2/default".to_string(),
sub: "00u1234567890".to_string(),
aud: OktaAudience::Single("api://default".to_string()),
exp: 1700000000,
iat: Some(1699996400),
uid: None,
cid: None,
scp: None,
email: None,
email_verified: None,
name: None,
preferred_username: None,
given_name: None,
family_name: None,
groups: None,
};
assert_eq!(claims.iss(), "https://dev-123456.okta.com/oauth2/default");
assert_eq!(claims.sub(), "00u1234567890");
assert_eq!(claims.aud(), &["api://default".to_string()]);
assert_eq!(claims.exp(), 1700000000);
assert_eq!(claims.iat(), Some(1699996400));
}
#[test]
fn extract_identity_from_access_token() {
let claims = OktaClaims {
iss: "https://dev-123456.okta.com/oauth2/default".to_string(),
sub: "00u1234567890".to_string(),
aud: OktaAudience::Single("api://default".to_string()),
exp: 1700000000,
iat: Some(1699996400),
uid: Some("00u1234567890".to_string()),
cid: Some("0oa1234567890".to_string()),
scp: Some(vec!["openid".to_string()]),
email: Some("user@example.com".to_string()),
email_verified: Some(true),
name: Some("Jane Doe".to_string()),
preferred_username: Some("user@example.com".to_string()),
given_name: Some("Jane".to_string()),
family_name: Some("Doe".to_string()),
groups: Some(vec!["Everyone".to_string(), "Developers".to_string()]),
};
let identity = OktaExtractor.extract_identity(&claims).unwrap();
assert_eq!(&*identity.subject, "00u1234567890");
assert_eq!(identity.user_id.as_deref(), Some("00u1234567890"));
assert_eq!(identity.client_id.as_deref(), Some("0oa1234567890"));
assert_eq!(identity.email.as_deref(), Some("user@example.com"));
assert_eq!(identity.name.as_deref(), Some("Jane Doe"));
let groups: Vec<&str> = identity
.groups
.as_ref()
.unwrap()
.iter()
.map(|g| &**g)
.collect();
assert_eq!(groups, vec!["Everyone", "Developers"]);
}
#[test]
fn extract_identity_with_minimal_claims() {
let claims = OktaClaims {
iss: "https://dev-123456.okta.com/oauth2/default".to_string(),
sub: "00u1234567890".to_string(),
aud: OktaAudience::Multiple(vec!["0oa1234567890".to_string()]),
exp: 1700000000,
iat: None,
uid: None,
cid: None,
scp: None,
email: None,
email_verified: None,
name: None,
preferred_username: None,
given_name: None,
family_name: None,
groups: None,
};
let identity = OktaExtractor.extract_identity(&claims).unwrap();
assert_eq!(&*identity.subject, "00u1234567890");
assert!(identity.user_id.is_none());
assert!(identity.client_id.is_none());
assert!(identity.email.is_none());
assert!(identity.name.is_none());
assert!(identity.groups.is_none());
}
#[test]
fn okta_identity_equality() {
let identity_a = OktaIdentity {
subject: Arc::from("00u1234567890"),
user_id: Some(Arc::from("00u1234567890")),
client_id: None,
email: Some(Arc::from("user@example.com")),
name: None,
groups: None,
};
let identity_b = identity_a.clone();
assert_eq!(identity_a, identity_b);
}
}