Skip to main content

auth_password/
resolver.rs

1use crate::jwt::{self, JwtConfig};
2use platform_core::{ActorContext, ActorResolutionRequest, ActorResolver};
3use std::sync::Arc;
4
5/// Actor resolver that validates JWT tokens issued by the auth-password module
6/// when `token_strategy` is `"jwt"`.
7///
8/// Chain position: after `AuthActorResolver`, before the final fallback.
9/// Session tokens (prefixed `sess_`) are skipped — they belong to the session resolver.
10#[derive(Debug, Clone)]
11pub struct JwtActorResolver {
12    config: JwtConfig,
13    fallback: Arc<dyn ActorResolver>,
14}
15
16impl JwtActorResolver {
17    #[must_use]
18    pub fn new(config: JwtConfig, fallback: Arc<dyn ActorResolver>) -> Self {
19        Self { config, fallback }
20    }
21}
22
23#[async_trait::async_trait]
24impl ActorResolver for JwtActorResolver {
25    async fn resolve_actor(&self, request: ActorResolutionRequest) -> ActorContext {
26        if let Some(token) = bearer_jwt(&request) {
27            if let Some(claims) = jwt::verify_token(&token, &self.config) {
28                return ActorContext::User {
29                    user_id: claims.sub,
30                    scopes: Vec::new(),
31                };
32            }
33        }
34
35        self.fallback.resolve_actor(request).await
36    }
37}
38
39/// Extract a Bearer token that looks like a JWT candidate.
40///
41/// Skips:
42/// - Empty tokens
43/// - `dev-user:` / `dev-service:` dev tokens (handled by DevActorResolver)
44/// - `sess_`-prefixed session tokens (handled by AuthActorResolver)
45fn bearer_jwt(request: &ActorResolutionRequest) -> Option<String> {
46    request
47        .authorization
48        .as_deref()
49        .and_then(|header| header.strip_prefix("Bearer "))
50        .map(str::trim)
51        .filter(|token| !token.is_empty())
52        .filter(|token| !token.starts_with("dev-user:") && !token.starts_with("dev-service:"))
53        .filter(|token| !token.starts_with("sess_"))
54        .map(ToOwned::to_owned)
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn extracts_bearer_jwt() {
63        let request = ActorResolutionRequest {
64            authorization: Some("Bearer eyJhbGciOiJIUzI1NiJ9.test".to_owned()),
65            cookie: None,
66        };
67        assert_eq!(
68            bearer_jwt(&request),
69            Some("eyJhbGciOiJIUzI1NiJ9.test".to_owned())
70        );
71    }
72
73    #[test]
74    fn skips_session_tokens() {
75        let request = ActorResolutionRequest {
76            authorization: Some("Bearer sess_abc123".to_owned()),
77            cookie: None,
78        };
79        assert!(bearer_jwt(&request).is_none());
80    }
81
82    #[test]
83    fn skips_dev_tokens() {
84        let request = ActorResolutionRequest {
85            authorization: Some("Bearer dev-user:test".to_owned()),
86            cookie: None,
87        };
88        assert!(bearer_jwt(&request).is_none());
89    }
90
91    #[test]
92    fn returns_none_when_no_authorization() {
93        let request = ActorResolutionRequest {
94            authorization: None,
95            cookie: None,
96        };
97        assert!(bearer_jwt(&request).is_none());
98    }
99}