Skip to main content

shared/application/session/
jwt_service.rs

1use crate::error::{AuthError, CoreError, Result, TokenErrorType};
2
3use crate::domain::model::{Claims, IssuedTokens, RefreshToken, TokenType};
4use crate::intern::session::SessionService;
5
6impl SessionService {
7    #[tracing::instrument(name = "auth.consume_refresh_token", skip(self, refresh_token))]
8    pub async fn consume_refresh_token(&self, refresh_token: &str) -> Result<String> {
9        let claims: Claims = self.crypto.jwt()?.decode(refresh_token)?;
10
11        if claims.token_type != TokenType::RefreshToken {
12            return Err(CoreError::Unauthenticated(AuthError::TokenInvalid {
13                token_type: TokenErrorType::RefreshToken,
14            }));
15        }
16
17        match self.jwt_repository.find_and_consume(&claims).await {
18            Ok(_) => Ok(claims.sub),
19            Err(CoreError::Unauthenticated { .. }) => {
20                // Potential breach — revoke everything for this user
21                // TODO: Send an email indicating a breach
22                self.jwt_repository.revoke(&claims.sub).await?;
23                Err(CoreError::Unauthenticated(AuthError::TokenReplay {
24                    token_type: TokenErrorType::RefreshToken,
25                }))
26            }
27            Err(e) => Err(e), // NotFound, Expired bubble up as-is
28        }
29    }
30
31    #[tracing::instrument(
32        name = "auth.issue_jwt", skip(self), fields(user.id = user_id)
33    )]
34    pub async fn issue_jwt(
35        &self,
36        user_id: &str,
37        full_permissions: Vec<String>,
38    ) -> Result<IssuedTokens> {
39        let jwt_config = self.configuration.auth.jwt()?;
40        let rbac_config = &self.configuration.auth.rbac;
41        let jwt = self.crypto.jwt()?;
42
43        let (access_claims, refresh_claims) = Claims::new(user_id, &rbac_config.default_role)
44            .with_issuer(&jwt_config.issuer)
45            .with_audience(&jwt_config.audience)
46            .with_permissions(full_permissions)
47            .into_token_pair(jwt_config);
48        let jti = refresh_claims.jti;
49
50        let refresh_token = RefreshToken::new(&jti.to_string())
51            .with_user_id(user_id)
52            .with_expire_at(jwt_config.refresh_token_expires_in);
53        self.jwt_repository.insert(refresh_token).await?;
54
55        let access = jwt.encode(access_claims)?;
56        let refresh = jwt.encode(refresh_claims)?;
57        Ok(IssuedTokens::default()
58            .with_access_token(&access)
59            .with_refresh_token(&refresh)
60            .with_jti(jti))
61    }
62
63    #[tracing::instrument(name = "auth.invalidate_jwt", skip(self, refresh_token))]
64    pub async fn invalidate_jwt(&self, refresh_token: &str) -> Result<()> {
65        let claims: Claims = self.crypto.jwt()?.decode(refresh_token)?;
66
67        if claims.token_type != TokenType::RefreshToken {
68            return Err(CoreError::from(AuthError::TokenInvalid {
69                token_type: TokenErrorType::RefreshToken,
70            }));
71        }
72
73        self.jwt_repository.invalidate(claims.jti).await?;
74        Ok(())
75    }
76
77    // async fn logout(&self, payload: AuthPayload) -> Result<()> {
78    //     self.jwt_service.invalidate(&payload.jti).await?;
79    //     self.session_service.revoke(&payload.user_id).await?;
80    //     Ok(())
81    // }
82    #[tracing::instrument(name = "auth.logout_all", skip(self), fields(user.id = user_id))]
83    pub async fn logout_all(&self, user_id: &str) -> Result<()> {
84        self.jwt_repository.revoke(user_id).await?;
85        self.session_repository.revoke(user_id).await?;
86        Ok(())
87    }
88
89    #[tracing::instrument(name = "auth.find_jwt_by_jti", skip(self, jti))]
90    pub async fn find_jwt_by_jti(&self, jti: &str) -> Result<RefreshToken> {
91        self.jwt_repository.find_by_jti(jti).await
92    }
93}