libauth_rs/provider/stytch/b2b/
session.rs1use crate::error::{AuthError, AuthResult};
2use crate::provider::Provider;
3use crate::traits::{Authenticator, Authn, Authorizer, ClientAccess, Session};
4use crate::types::{AuthProvider as AuthProviderType, Token, UserContext};
5use async_trait::async_trait;
6use std::collections::HashMap;
7use std::sync::Arc;
8use tracing::debug;
9
10#[derive(Clone)]
18pub struct StytchProvider {
19 stytch_client: Arc<stytch::client::Client>,
21}
22
23impl std::fmt::Debug for StytchProvider {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 f.debug_struct("StytchProvider").finish()
26 }
27}
28
29impl StytchProvider {
30 pub async fn new(config: &Provider) -> AuthResult<Self> {
31 let stytch = config.config.as_stytch()?;
32
33 let sdk_client =
35 stytch::client::Client::new_b2b(&stytch.project_id, &stytch.project_secret).map_err(
36 |e| AuthError::ConfigurationError {
37 message: format!("Failed to create Stytch B2B SDK client: {}", e),
38 },
39 )?;
40
41 Ok(Self {
42 stytch_client: Arc::new(sdk_client),
43 })
44 }
45
46 pub fn sdk(&self) -> &stytch::client::Client {
48 &self.stytch_client
49 }
50
51 fn is_valid_token_format(token: &str) -> bool {
56 if !token.starts_with("stytch_session_") {
58 return false;
59 }
60
61 let parts: Vec<&str> = token.split('_').collect();
63
64 if parts.len() < 3 {
66 return false;
67 }
68
69 let len = token.len();
71 (40..=100).contains(&len)
72 }
73}
74
75#[async_trait]
76impl Authenticator for StytchProvider {
77 async fn authenticate(&self, token: &Token) -> AuthResult<UserContext> {
78 if !Self::is_valid_token_format(token) {
79 return Err(AuthError::InvalidToken {
80 message: "Invalid Stytch B2B session token format".to_string(),
81 provider: Some("stytch-b2b-session".to_string()),
82 });
83 }
84
85 debug!(
86 token_prefix = &token[..15.min(token.len())],
87 "Authenticating Stytch B2B session"
88 );
89
90 let sessions = stytch::b2b::sessions::Sessions::new(self.stytch_client.as_ref().clone());
92
93 let request = stytch::b2b::sessions::AuthenticateRequest {
94 session_token: Some(token.clone()),
95 session_jwt: None,
96 session_duration_minutes: None,
97 session_custom_claims: None,
98 authorization_check: None,
99 };
100
101 let response = sessions.authenticate(request).await.map_err(|e| {
102 tracing::warn!(
103 error = %e,
104 "Stytch B2B session authentication failed"
105 );
106 AuthError::InvalidToken {
107 message: format!("B2B session authentication failed: {}", e),
108 provider: Some("stytch-b2b-session".to_string()),
109 }
110 })?;
111
112 let member = response.member;
114 let member_session = response.member_session;
115 let organization = response.organization;
116
117 let user_id = member.member_id;
118
119 let email = member.email_address;
121
122 let name = member.name;
124
125 let mut metadata = HashMap::new();
127 metadata.insert(
128 "stytch_member_id".to_string(),
129 serde_json::Value::String(user_id.clone()),
130 );
131 metadata.insert(
132 "organization_id".to_string(),
133 serde_json::Value::String(organization.organization_id.clone()),
134 );
135 metadata.insert(
136 "organization_name".to_string(),
137 serde_json::Value::String(organization.organization_name.clone()),
138 );
139 metadata.insert(
140 "auth_type".to_string(),
141 serde_json::Value::String("b2b_session".to_string()),
142 );
143 metadata.insert(
144 "session_id".to_string(),
145 serde_json::Value::String(member_session.member_session_id.clone()),
146 );
147
148 if !member.roles.is_empty() {
150 metadata.insert("roles".to_string(), serde_json::json!(member.roles));
151 }
152
153 let expires_at = Some(member_session.expires_at);
155
156 Ok(UserContext {
157 user_id,
158 email: Some(email),
159 name: Some(name),
160 provider: AuthProviderType::Stytch,
161 session_id: Some(member_session.member_session_id),
162 expires_at,
163 metadata,
164 })
165 }
166}
167
168#[async_trait]
169impl Authorizer for StytchProvider {
170 async fn authorize(&self, user: &UserContext, action: &str) -> AuthResult<()> {
171 if let Some(roles) = user.metadata.get("roles") {
173 if let Some(roles_array) = roles.as_array() {
174 let has_role = roles_array.iter().any(|role| {
175 if let Some(role_obj) = role.as_object() {
176 if let Some(role_id) = role_obj.get("role_id").and_then(|v| v.as_str()) {
177 role_id == action || role_id == "admin"
178 } else {
179 false
180 }
181 } else {
182 false
183 }
184 });
185
186 if has_role {
187 return Ok(());
188 }
189 }
190 }
191
192 Err(AuthError::InsufficientPermissions(format!(
193 "User does not have required role: {}",
194 action
195 )))
196 }
197}
198
199#[async_trait]
200impl Session for StytchProvider {
201 async fn validate(&self, token: &Token) -> AuthResult<UserContext> {
202 self.authenticate(token).await
204 }
205}
206
207impl Authn for StytchProvider {}
209
210impl ClientAccess for StytchProvider {
212 type Client = Arc<stytch::client::Client>;
213
214 fn client(&self) -> Self::Client {
215 Arc::clone(&self.stytch_client)
216 }
217}