use crate::errors::{AuthError, Result, TokenError};
use crate::providers::{OAuthProvider, ProfileExtractor, ProviderProfile};
use base64::Engine as _;
use chrono::{DateTime, Utc};
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use rsa::pkcs1::DecodeRsaPublicKey;
use rsa::pkcs8::DecodePublicKey;
use rsa::traits::PublicKeyParts;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[cfg(feature = "postgres-storage")]
use sqlx::FromRow;
use std::collections::HashMap;
use std::time::Duration;
use uuid::Uuid;
#[cfg_attr(feature = "postgres-storage", derive(FromRow))]
#[derive(Clone, Serialize, Deserialize)]
pub struct AuthToken {
pub token_id: String,
pub user_id: String,
pub access_token: String,
pub token_type: Option<String>,
pub subject: Option<String>,
pub issuer: Option<String>,
pub refresh_token: Option<String>,
pub issued_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub scopes: crate::types::Scopes,
pub auth_method: String,
pub client_id: Option<String>,
pub user_profile: Option<ProviderProfile>,
pub permissions: crate::types::Permissions,
pub roles: crate::types::Roles,
pub metadata: TokenMetadata,
}
impl std::fmt::Debug for AuthToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthToken")
.field("token_id", &self.token_id)
.field("user_id", &self.user_id)
.field("access_token", &"[REDACTED]")
.field("token_type", &self.token_type)
.field("subject", &self.subject)
.field("issuer", &self.issuer)
.field(
"refresh_token",
if self.refresh_token.is_some() {
&"Some([REDACTED])"
} else {
&"None"
},
)
.field("issued_at", &self.issued_at)
.field("expires_at", &self.expires_at)
.field("scopes", &self.scopes)
.field("auth_method", &self.auth_method)
.field("client_id", &self.client_id)
.field("permissions", &self.permissions)
.field("roles", &self.roles)
.field("metadata", &self.metadata)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct AuthTokenBuilder {
token_id: String,
user_id: String,
access_token: String,
token_type: Option<String>,
subject: Option<String>,
issuer: Option<String>,
refresh_token: Option<String>,
issued_at: DateTime<Utc>,
expires_at: DateTime<Utc>,
scopes: crate::types::Scopes,
auth_method: String,
client_id: Option<String>,
user_profile: Option<ProviderProfile>,
permissions: crate::types::Permissions,
roles: crate::types::Roles,
metadata: TokenMetadata,
}
impl AuthTokenBuilder {
pub fn new(
token_id: impl Into<String>,
user_id: impl Into<String>,
access_token: impl Into<String>,
) -> Self {
let now = Utc::now();
Self {
token_id: token_id.into(),
user_id: user_id.into(),
access_token: access_token.into(),
token_type: None,
subject: None,
issuer: None,
refresh_token: None,
issued_at: now,
expires_at: now + chrono::Duration::hours(1),
scopes: crate::types::Scopes::empty(),
auth_method: "unknown".to_string(),
client_id: None,
user_profile: None,
permissions: crate::types::Permissions::empty(),
roles: crate::types::Roles::empty(),
metadata: TokenMetadata::default(),
}
}
pub fn token_type(mut self, token_type: impl Into<String>) -> Self {
self.token_type = Some(token_type.into());
self
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
pub fn refresh_token(mut self, refresh_token: impl Into<String>) -> Self {
self.refresh_token = Some(refresh_token.into());
self
}
pub fn issued_at(mut self, issued_at: DateTime<Utc>) -> Self {
self.issued_at = issued_at;
self
}
pub fn expires_at(mut self, expires_at: DateTime<Utc>) -> Self {
self.expires_at = expires_at;
self
}
pub fn scopes(mut self, scopes: crate::types::Scopes) -> Self {
self.scopes = scopes;
self
}
pub fn auth_method(mut self, auth_method: impl Into<String>) -> Self {
self.auth_method = auth_method.into();
self
}
pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
pub fn user_profile(mut self, user_profile: ProviderProfile) -> Self {
self.user_profile = Some(user_profile);
self
}
pub fn permissions(mut self, permissions: crate::types::Permissions) -> Self {
self.permissions = permissions;
self
}
pub fn roles(mut self, roles: crate::types::Roles) -> Self {
self.roles = roles;
self
}
pub fn metadata(mut self, metadata: TokenMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn build(self) -> AuthToken {
AuthToken {
token_id: self.token_id,
user_id: self.user_id,
access_token: self.access_token,
token_type: self.token_type,
subject: self.subject,
issuer: self.issuer,
refresh_token: self.refresh_token,
issued_at: self.issued_at,
expires_at: self.expires_at,
scopes: self.scopes,
auth_method: self.auth_method,
client_id: self.client_id,
user_profile: self.user_profile,
permissions: self.permissions,
roles: self.roles,
metadata: self.metadata,
}
}
}
impl AuthToken {
pub fn builder(
token_id: impl Into<String>,
user_id: impl Into<String>,
access_token: impl Into<String>,
) -> AuthTokenBuilder {
AuthTokenBuilder::new(token_id, user_id, access_token)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TokenMetadata {
pub issued_ip: Option<String>,
pub user_agent: Option<String>,
pub device_id: Option<String>,
pub session_id: Option<String>,
pub revoked: bool,
pub revoked_at: Option<DateTime<Utc>>,
pub revoked_reason: Option<String>,
pub last_used: Option<DateTime<Utc>>,
pub use_count: u64,
pub custom: HashMap<String, serde_json::Value>,
}
impl TokenMetadata {
pub fn builder() -> TokenMetadataBuilder {
TokenMetadataBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct TokenMetadataBuilder {
inner: TokenMetadata,
}
impl TokenMetadataBuilder {
pub fn issued_ip(mut self, ip: impl Into<String>) -> Self {
self.inner.issued_ip = Some(ip.into());
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.inner.user_agent = Some(ua.into());
self
}
pub fn device_id(mut self, id: impl Into<String>) -> Self {
self.inner.device_id = Some(id.into());
self
}
pub fn session_id(mut self, id: impl Into<String>) -> Self {
self.inner.session_id = Some(id.into());
self
}
pub fn custom(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.inner.custom.insert(key.into(), value);
self
}
pub fn build(self) -> TokenMetadata {
self.inner
}
}
#[cfg(feature = "postgres-storage")]
use sqlx::{Decode, Postgres, Type, postgres::PgValueRef};
#[cfg(feature = "postgres-storage")]
impl<'r> Decode<'r, Postgres> for TokenMetadata {
fn decode(value: PgValueRef<'r>) -> std::result::Result<Self, sqlx::error::BoxDynError> {
let json: serde_json::Value = <serde_json::Value as Decode<Postgres>>::decode(value)?;
serde_json::from_value(json).map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
}
}
#[cfg(feature = "postgres-storage")]
impl Type<Postgres> for TokenMetadata {
fn type_info() -> sqlx::postgres::PgTypeInfo {
<serde_json::Value as Type<Postgres>>::type_info()
}
fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
<serde_json::Value as Type<Postgres>>::compatible(ty)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenInfo {
pub user_id: String,
pub username: Option<String>,
pub email: Option<String>,
pub name: Option<String>,
pub roles: Vec<String>,
pub permissions: Vec<String>,
pub attributes: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtClaims {
pub sub: String,
pub iss: String,
pub aud: String,
pub exp: i64,
pub iat: i64,
pub nbf: i64,
pub jti: String,
pub scope: String,
pub permissions: Option<Vec<String>>,
pub roles: Option<Vec<String>>,
pub client_id: Option<String>,
#[serde(flatten)]
pub custom: HashMap<String, serde_json::Value>,
}
pub struct TokenManager {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
previous_decoding_key: Option<DecodingKey>,
key_material: KeyMaterial,
previous_key_material: Option<KeyMaterial>,
algorithm: Algorithm,
issuer: String,
audience: String,
default_lifetime: Duration,
}
#[derive(Clone)]
enum KeyMaterial {
Hmac(Vec<u8>),
Rsa { private: Vec<u8>, public: Vec<u8> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwksPublicKey {
pub algorithm: Algorithm,
pub kid: String,
pub n: String,
pub e: String,
}
impl AuthToken {
pub fn new(
user_id: impl Into<String>,
access_token: impl Into<String>,
expires_in: std::time::Duration,
auth_method: impl Into<String>,
) -> Self {
let now = Utc::now();
let expires_in_chrono =
chrono::Duration::from_std(expires_in).unwrap_or(chrono::Duration::hours(1));
Self {
token_id: Uuid::new_v4().to_string(),
user_id: user_id.into(),
access_token: access_token.into(),
refresh_token: None,
token_type: Some("Bearer".to_string()),
subject: None,
issuer: None,
issued_at: now,
expires_at: now + expires_in_chrono,
scopes: crate::types::Scopes::empty(),
auth_method: auth_method.into(),
client_id: None,
user_profile: None,
permissions: crate::types::Permissions::empty(),
roles: crate::types::Roles::empty(),
metadata: TokenMetadata::default(),
}
}
pub fn access_token(&self) -> &str {
&self.access_token
}
pub fn user_id(&self) -> &str {
&self.user_id
}
pub fn expires_at(&self) -> DateTime<Utc> {
self.expires_at
}
pub fn token_value(&self) -> &str {
&self.access_token
}
pub fn token_type(&self) -> Option<&str> {
self.token_type.as_deref()
}
pub fn subject(&self) -> Option<&str> {
self.subject.as_deref()
}
pub fn issuer(&self) -> Option<&str> {
self.issuer.as_deref()
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
pub fn is_expiring(&self, within: Duration) -> bool {
Utc::now() + within > self.expires_at
}
pub fn is_revoked(&self) -> bool {
self.metadata.revoked
}
pub fn is_valid(&self) -> bool {
!self.is_expired() && !self.is_revoked()
}
pub fn has_refresh_token(&self) -> bool {
self.refresh_token.is_some()
}
pub fn get_refresh_token(&self) -> Option<&str> {
self.refresh_token.as_deref()
}
pub fn revoke(&mut self, reason: Option<String>) {
self.metadata.revoked = true;
self.metadata.revoked_at = Some(Utc::now());
self.metadata.revoked_reason = reason;
}
pub fn mark_used(&mut self) {
self.metadata.last_used = Some(Utc::now());
self.metadata.use_count += 1;
}
pub fn add_scope(&mut self, scope: impl Into<String>) {
let scope = scope.into();
if !self.scopes.contains(&scope) {
self.scopes.push(scope);
}
}
pub fn has_scope(&self, scope: &str) -> bool {
self.scopes.contains(&scope.to_string())
}
pub fn with_refresh_token(mut self, refresh_token: impl Into<String>) -> Self {
self.refresh_token = Some(refresh_token.into());
self
}
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
pub fn with_scopes(mut self, scopes: impl Into<crate::types::Scopes>) -> Self {
self.scopes = scopes.into();
self
}
pub fn with_metadata(mut self, metadata: TokenMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn time_until_expiry(&self) -> Duration {
let now = Utc::now();
if self.expires_at > now {
(self.expires_at - now).to_std().unwrap_or(Duration::ZERO)
} else {
Duration::ZERO
}
}
pub fn add_custom_claim(&mut self, key: impl Into<String>, value: serde_json::Value) {
self.metadata.custom.insert(key.into(), value);
}
pub fn get_custom_claim(&self, key: &str) -> Option<&serde_json::Value> {
self.metadata.custom.get(key)
}
pub fn has_permission(&self, permission: &str) -> bool {
self.permissions.contains(&permission.to_string())
}
pub fn add_permission(&mut self, permission: impl Into<String>) {
let permission = permission.into();
if !self.permissions.contains(&permission) {
self.permissions.push(permission);
}
}
pub fn add_role(&mut self, role: impl Into<String>) {
let role = role.into();
if !self.roles.contains(&role) {
self.roles.push(role);
}
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.contains(&role.to_string())
}
pub fn with_permissions(mut self, permissions: impl Into<crate::types::Permissions>) -> Self {
self.permissions = permissions.into();
self
}
pub fn with_roles(mut self, roles: impl Into<crate::types::Roles>) -> Self {
self.roles = roles.into();
self
}
}
impl Clone for TokenManager {
fn clone(&self) -> Self {
let (previous_decoding_key, previous_key_material) = match &self.previous_key_material {
Some(KeyMaterial::Hmac(secret)) => (
Some(DecodingKey::from_secret(secret)),
Some(KeyMaterial::Hmac(secret.clone())),
),
Some(KeyMaterial::Rsa { public, .. }) => (
DecodingKey::from_rsa_pem(public).ok(),
self.previous_key_material.clone(),
),
None => (None, None),
};
match &self.key_material {
KeyMaterial::Hmac(secret) => Self {
encoding_key: EncodingKey::from_secret(secret),
decoding_key: DecodingKey::from_secret(secret),
previous_decoding_key,
key_material: self.key_material.clone(),
previous_key_material,
algorithm: self.algorithm,
issuer: self.issuer.clone(),
audience: self.audience.clone(),
default_lifetime: self.default_lifetime,
},
KeyMaterial::Rsa { private, public } => Self {
encoding_key: EncodingKey::from_rsa_pem(private).expect("RSA private key PEM re-parse failed during Clone — this indicates memory corruption"),
decoding_key: DecodingKey::from_rsa_pem(public).expect("RSA public key PEM re-parse failed during Clone — this indicates memory corruption"),
previous_decoding_key,
key_material: self.key_material.clone(),
previous_key_material,
algorithm: self.algorithm,
issuer: self.issuer.clone(),
audience: self.audience.clone(),
default_lifetime: self.default_lifetime,
},
}
}
}
impl TokenManager {
fn jwks_from_public_pem(public_key: &[u8], algorithm: Algorithm) -> Result<JwksPublicKey> {
let pem = std::str::from_utf8(public_key)
.map_err(|e| AuthError::crypto(format!("Invalid RSA public key PEM encoding: {e}")))?;
let public_key = rsa::RsaPublicKey::from_public_key_pem(pem)
.or_else(|_| rsa::RsaPublicKey::from_pkcs1_pem(pem))
.map_err(|e| {
AuthError::crypto(format!(
"Failed to parse RSA public key for JWKS export: {e}"
))
})?;
let modulus = public_key.n().to_bytes_be();
let exponent = public_key.e().to_bytes_be();
let kid_digest = Sha256::digest(&modulus);
let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modulus);
let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&exponent);
let kid = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(kid_digest);
Ok(JwksPublicKey {
algorithm,
kid,
n,
e,
})
}
pub fn export_public_jwks(&self) -> Result<Vec<JwksPublicKey>> {
let mut keys = Vec::new();
if let KeyMaterial::Rsa { public, .. } = &self.key_material {
keys.push(Self::jwks_from_public_pem(public, self.algorithm)?);
}
if let Some(KeyMaterial::Rsa { public, .. }) = &self.previous_key_material {
let previous = Self::jwks_from_public_pem(public, self.algorithm)?;
if !keys.iter().any(|key| key.kid == previous.kid) {
keys.push(previous);
}
}
Ok(keys)
}
pub fn new_hmac(secret: &[u8], issuer: impl Into<String>, audience: impl Into<String>) -> Self {
Self {
encoding_key: EncodingKey::from_secret(secret),
decoding_key: DecodingKey::from_secret(secret),
previous_decoding_key: None,
key_material: KeyMaterial::Hmac(secret.to_vec()),
previous_key_material: None,
algorithm: Algorithm::HS256,
issuer: issuer.into(),
audience: audience.into(),
default_lifetime: Duration::from_secs(3600), }
}
pub fn new_rsa(
private_key: &[u8],
public_key: &[u8],
issuer: impl Into<String>,
audience: impl Into<String>,
) -> Result<Self> {
let encoding_key = EncodingKey::from_rsa_pem(private_key)
.map_err(|e| AuthError::crypto(format!("Invalid RSA private key: {e}")))?;
let decoding_key = DecodingKey::from_rsa_pem(public_key)
.map_err(|e| AuthError::crypto(format!("Invalid RSA public key: {e}")))?;
Ok(Self {
encoding_key,
decoding_key,
previous_decoding_key: None,
key_material: KeyMaterial::Rsa {
private: private_key.to_vec(),
public: public_key.to_vec(),
},
previous_key_material: None,
algorithm: Algorithm::RS256,
issuer: issuer.into(),
audience: audience.into(),
default_lifetime: Duration::from_secs(3600), })
}
pub fn rotate_hmac_key(&mut self, new_secret: &[u8]) {
if let KeyMaterial::Hmac(secret) = &self.key_material {
self.previous_decoding_key = Some(DecodingKey::from_secret(secret));
self.previous_key_material = Some(KeyMaterial::Hmac(secret.clone()));
}
self.encoding_key = EncodingKey::from_secret(new_secret);
self.decoding_key = DecodingKey::from_secret(new_secret);
self.key_material = KeyMaterial::Hmac(new_secret.to_vec());
self.algorithm = Algorithm::HS256;
}
pub fn rotate_rsa_key(&mut self, private_key: &[u8], public_key: &[u8]) -> Result<()> {
let new_encoding_key = EncodingKey::from_rsa_pem(private_key)
.map_err(|e| AuthError::crypto(format!("Invalid RSA private key: {e}")))?;
let new_decoding_key = DecodingKey::from_rsa_pem(public_key)
.map_err(|e| AuthError::crypto(format!("Invalid RSA public key: {e}")))?;
if let KeyMaterial::Rsa { public, .. } = &self.key_material {
self.previous_decoding_key = DecodingKey::from_rsa_pem(public).ok();
self.previous_key_material = Some(self.key_material.clone());
}
self.encoding_key = new_encoding_key;
self.decoding_key = new_decoding_key;
self.key_material = KeyMaterial::Rsa {
private: private_key.to_vec(),
public: public_key.to_vec(),
};
self.algorithm = Algorithm::RS256;
Ok(())
}
pub fn retire_previous_key(&mut self) {
self.previous_decoding_key = None;
self.previous_key_material = None;
}
pub fn with_default_lifetime(mut self, lifetime: Duration) -> Self {
self.default_lifetime = lifetime;
self
}
pub fn create_jwt_token(
&self,
user_id: impl Into<String>,
scopes: Vec<String>,
lifetime: Option<Duration>,
) -> Result<String> {
let user_id = user_id.into();
let lifetime = lifetime.unwrap_or(self.default_lifetime);
let now = Utc::now();
let exp = now + chrono::Duration::from_std(lifetime).unwrap_or(chrono::Duration::hours(1));
let claims = JwtClaims {
sub: user_id,
iss: self.issuer.clone(),
aud: self.audience.clone(),
exp: exp.timestamp(),
iat: now.timestamp(),
nbf: now.timestamp(),
jti: Uuid::new_v4().to_string(),
scope: scopes.join(" "),
permissions: None,
roles: None,
client_id: None,
custom: HashMap::new(),
};
let header = Header::new(self.algorithm);
encode(&header, &claims, &self.encoding_key)
.map_err(|e| TokenError::creation_failed(format!("JWT encoding failed: {e}")).into())
}
pub fn validate_jwt_token(&self, token: &str) -> Result<JwtClaims> {
let mut validation = Validation::new(self.algorithm);
validation.set_issuer(&[&self.issuer]);
validation.set_audience(&[&self.audience]);
match decode::<JwtClaims>(token, &self.decoding_key, &validation) {
Ok(token_data) => Ok(token_data.claims),
Err(e) => {
match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
Err(AuthError::Token(TokenError::Expired))
}
jsonwebtoken::errors::ErrorKind::InvalidSignature => {
if let Some(prev_key) = &self.previous_decoding_key
&& let Ok(prev_token_data) =
decode::<JwtClaims>(token, prev_key, &validation)
{
return Ok(prev_token_data.claims);
}
Err(AuthError::Token(TokenError::Invalid {
message: "Invalid token signature".to_string(),
}))
}
_ => Err(AuthError::Token(TokenError::Invalid {
message: "Invalid token format".to_string(),
})),
}
}
}
}
pub fn create_auth_token(
&self,
user_id: impl Into<String>,
scopes: impl Into<crate::types::Scopes>,
auth_method: impl Into<String>,
lifetime: Option<std::time::Duration>,
) -> Result<AuthToken> {
let user_id_str = user_id.into();
let scopes: crate::types::Scopes = scopes.into();
let lifetime = lifetime.unwrap_or(self.default_lifetime);
let jwt_token = self.create_jwt_token(&user_id_str, scopes.to_vec(), Some(lifetime))?;
let token =
AuthToken::new(user_id_str, jwt_token, lifetime, auth_method).with_scopes(scopes);
Ok(token)
}
pub fn validate_auth_token(&self, token: &AuthToken) -> Result<()> {
if token.is_expired() {
return Err(TokenError::Expired.into());
}
if token.is_revoked() {
return Err(TokenError::Invalid {
message: "Token has been revoked".to_string(),
}
.into());
}
if token.auth_method == "jwt" || token.access_token.contains('.') {
self.validate_jwt_token(&token.access_token)?;
}
Ok(())
}
pub fn refresh_token(&self, token: &AuthToken) -> Result<AuthToken> {
if token.is_expired() {
return Err(TokenError::Expired.into());
}
if token.is_revoked() {
return Err(TokenError::Invalid {
message: "Cannot refresh revoked token".to_string(),
}
.into());
}
self.create_auth_token(
&token.user_id,
token.scopes.clone(),
&token.auth_method,
Some(self.default_lifetime),
)
}
pub fn extract_token_info(&self, token: &str) -> Result<TokenInfo> {
let claims = self.validate_jwt_token(token)?;
Ok(TokenInfo {
user_id: claims.sub,
username: claims
.custom
.get("username")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
email: claims
.custom
.get("email")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
name: claims
.custom
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
roles: claims
.custom
.get("roles")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect()
})
.unwrap_or_default(),
permissions: claims
.scope
.split_whitespace()
.map(|s| s.to_string())
.collect(),
attributes: claims.custom,
})
}
}
#[async_trait::async_trait]
pub trait TokenToProfile {
async fn to_profile(&self, provider: &OAuthProvider) -> Result<ProviderProfile>;
async fn to_profile_with_extractor(
&self,
provider: &OAuthProvider,
extractor: &ProfileExtractor,
) -> Result<ProviderProfile>;
}
#[async_trait::async_trait]
impl TokenToProfile for AuthToken {
async fn to_profile(&self, provider: &OAuthProvider) -> Result<ProviderProfile> {
let extractor = ProfileExtractor::new();
extractor.extract_profile(self, provider).await
}
async fn to_profile_with_extractor(
&self,
provider: &OAuthProvider,
extractor: &ProfileExtractor,
) -> Result<ProviderProfile> {
extractor.extract_profile(self, provider).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_token_creation() {
let token = AuthToken::new(
"user123",
"token123",
Duration::from_secs(3600), "password",
);
assert_eq!(token.user_id(), "user123");
assert_eq!(token.access_token(), "token123");
assert!(!token.is_expired());
assert!(!token.is_revoked());
assert!(token.is_valid());
}
#[test]
fn test_token_expiry() {
let token = AuthToken::new("user123", "token123", Duration::from_millis(1), "password");
std::thread::sleep(std::time::Duration::from_millis(10));
assert!(token.is_expired());
assert!(!token.is_valid());
}
#[test]
fn test_token_revocation() {
let mut token = AuthToken::new(
"user123",
"token123",
Duration::from_secs(3600), "password",
);
assert!(!token.is_revoked());
token.revoke(Some("User logout".to_string()));
assert!(token.is_revoked());
assert!(!token.is_valid());
assert!(token.metadata.revoked);
}
}