use crate::{Subject, error::Result, introspect::IntrospectionResult};
use core::{future::Future, marker::Sync};
pub(crate) use jsonwebtoken::Header;
use std::collections::HashSet;
use std::fmt::Debug;
use typed_builder::TypedBuilder;
const ISSUER_CLAIM: &str = "iss";
const EXPIRES_CLAIM: &str = "exp";
const ISSUED_AT_CLAIM: &str = "iat";
const NOT_BEFORE_CLAIM: &str = "nbf";
pub(crate) const SCOPE_CLAIM: &str = "scope";
pub trait Authenticator
where
Self: Send + Sync + Clone,
{
fn authenticate(
&self,
token: &str,
introspection: &IntrospectionResult,
) -> impl Future<Output = Result<Authentication>> + Send;
fn can_handle_token(&self, token: &str, introspection_result: &IntrospectionResult) -> bool;
fn idp_id(&self) -> Option<&String>;
fn idp_ids(&self) -> Vec<Option<&str>> {
vec![self.idp_id().map(String::as_str)]
}
}
#[derive(Debug, PartialEq, Eq, Clone, TypedBuilder)]
pub struct Authentication {
token_header: Option<Header>,
claims: serde_json::Value,
subject: Subject,
name: Option<String>,
email: Option<String>,
principal_type: Option<PrincipalType>,
#[builder(default)]
roles: Option<Vec<String>>,
#[builder(default)]
audiences: HashSet<String>,
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]
pub enum PrincipalType {
Human,
Application,
}
impl Authentication {
#[must_use]
pub fn token_header(&self) -> Option<&Header> {
self.token_header.as_ref()
}
#[must_use]
pub fn claims(&self, key: &str) -> Option<&serde_json::Value> {
self.claims.get(key)
}
#[must_use]
pub fn subject(&self) -> &Subject {
&self.subject
}
#[must_use]
pub fn full_name(&self) -> Option<&str> {
self.name.as_deref()
}
#[must_use]
pub fn principal_type(&self) -> Option<PrincipalType> {
self.principal_type
}
#[must_use]
pub fn email(&self) -> Option<&str> {
self.email.as_deref()
}
#[must_use]
pub fn roles(&self) -> Option<&[String]> {
self.roles.as_deref()
}
#[must_use]
pub fn audiences(&self) -> &HashSet<String> {
&self.audiences
}
#[must_use]
pub fn idp_id(&self) -> Option<&str> {
self.subject().idp_id().map(std::string::String::as_str)
}
#[must_use]
pub fn all_claims(&self) -> &serde_json::Value {
&self.claims
}
#[must_use]
pub fn issuer(&self) -> Option<&str> {
self.claims
.get(ISSUER_CLAIM)
.and_then(serde_json::Value::as_str)
}
pub fn scopes(&self) -> impl Iterator<Item = &str> {
self.claims
.get(SCOPE_CLAIM)
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.split_whitespace()
}
#[must_use]
pub fn expires_at(&self) -> Option<i64> {
self.claims
.get(EXPIRES_CLAIM)
.and_then(serde_json::Value::as_i64)
}
#[must_use]
pub fn issued_at(&self) -> Option<i64> {
self.claims
.get(ISSUED_AT_CLAIM)
.and_then(serde_json::Value::as_i64)
}
#[must_use]
pub fn not_before(&self) -> Option<i64> {
self.claims
.get(NOT_BEFORE_CLAIM)
.and_then(serde_json::Value::as_i64)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Subject;
fn auth_with_claims(claims: serde_json::Value) -> Authentication {
Authentication::builder()
.token_header(None)
.claims(claims)
.name(None)
.email(None)
.subject(Subject::new(None, "sub".to_string()))
.principal_type(None)
.build()
}
#[test]
fn test_metadata_accessors_present() {
let auth = auth_with_claims(serde_json::json!({
"iss": "https://issuer.example.com",
"exp": 1_730_052_519,
"iat": 1_730_048_619,
"nbf": 1_730_048_619,
"scope": "openid profile email",
}));
assert_eq!(auth.issuer(), Some("https://issuer.example.com"));
assert_eq!(auth.expires_at(), Some(1_730_052_519));
assert_eq!(auth.issued_at(), Some(1_730_048_619));
assert_eq!(auth.not_before(), Some(1_730_048_619));
assert_eq!(
auth.scopes().collect::<Vec<_>>(),
["openid", "profile", "email"]
);
assert!(auth.all_claims().get("iss").is_some());
}
#[test]
fn test_metadata_accessors_absent() {
let auth = auth_with_claims(serde_json::json!({ "sub": "x" }));
assert_eq!(auth.issuer(), None);
assert_eq!(auth.expires_at(), None);
assert_eq!(auth.issued_at(), None);
assert_eq!(auth.not_before(), None);
assert_eq!(auth.scopes().count(), 0);
}
}