1use crate::config::JwtConfig;
4use crate::error::AuthError;
5use crate::identity::AuthIdentity;
6use crate::strategy::AuthStrategy;
7use async_trait::async_trait;
8use chrono::{Duration, Utc};
9use doido_core::Result;
10use doido_model::sea_orm::DatabaseConnection;
11use http::header;
12use http::request::Parts;
13use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct JwtClaims {
20 pub sub: Value,
21 pub exp: i64,
22 pub iat: i64,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub iss: Option<String>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub typ: Option<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct TokenPair {
32 pub access_token: String,
33 pub refresh_token: String,
34 pub token_type: String,
35 pub expires_in: u64,
36}
37
38pub struct JwtStrategy {
40 config: JwtConfig,
41 encoding: EncodingKey,
42 decoding: DecodingKey,
43}
44
45impl JwtStrategy {
46 pub fn new(config: JwtConfig) -> Result<Self, AuthError> {
47 config.validate()?;
48 let secret = config.secret.as_bytes();
49 Ok(Self {
50 config: config.clone(),
51 encoding: EncodingKey::from_secret(secret),
52 decoding: DecodingKey::from_secret(secret),
53 })
54 }
55
56 fn validation(&self) -> Validation {
57 let mut validation = Validation::new(Algorithm::HS256);
58 if let Some(iss) = &self.config.issuer {
59 validation.set_issuer(&[iss.as_str()]);
60 }
61 validation
62 }
63
64 fn issue_token(&self, user_id: &Value, ttl_secs: u64, typ: &str) -> Result<String, AuthError> {
65 let now = Utc::now();
66 let claims = JwtClaims {
67 sub: user_id.clone(),
68 iat: now.timestamp(),
69 exp: (now + Duration::seconds(ttl_secs as i64)).timestamp(),
70 iss: self.config.issuer.clone(),
71 typ: Some(typ.into()),
72 };
73 encode(&Header::default(), &claims, &self.encoding).map_err(AuthError::from)
74 }
75
76 pub fn issue_tokens(&self, user_id: &Value) -> Result<TokenPair, AuthError> {
78 Ok(TokenPair {
79 access_token: self.issue_token(user_id, self.config.access_ttl, "access")?,
80 refresh_token: self.issue_token(user_id, self.config.refresh_ttl, "refresh")?,
81 token_type: "Bearer".into(),
82 expires_in: self.config.access_ttl,
83 })
84 }
85
86 pub fn verify_token(&self, token: &str) -> Result<JwtClaims, AuthError> {
88 decode::<JwtClaims>(token, &self.decoding, &self.validation())
89 .map(|data| data.claims)
90 .map_err(AuthError::from)
91 }
92
93 fn bearer_token(parts: &Parts) -> Option<String> {
94 let value = parts.headers.get(header::AUTHORIZATION)?.to_str().ok()?;
95 let token = value.strip_prefix("Bearer ")?.trim();
96 if token.is_empty() {
97 None
98 } else {
99 Some(token.to_string())
100 }
101 }
102}
103
104#[async_trait]
105impl AuthStrategy for JwtStrategy {
106 fn name(&self) -> &str {
107 "jwt"
108 }
109
110 async fn authenticate(
111 &self,
112 parts: &Parts,
113 _db: &DatabaseConnection,
114 ) -> Result<Option<AuthIdentity>> {
115 let token = match Self::bearer_token(parts) {
116 Some(t) => t,
117 None => return Ok(None),
118 };
119 let claims = self.verify_token(&token)?;
120 Ok(Some(AuthIdentity {
121 user_id: claims.sub,
122 }))
123 }
124}