Skip to main content

authkestra_engine/auth/
state.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5/// A unified identity structure returned by all providers.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Identity {
8    /// The provider identifier (e.g., "github", "google")
9    pub provider_id: String,
10    /// The unique ID of the user within the provider's system
11    pub external_id: String,
12    /// The user's email address, if available and authorized
13    pub email: Option<String>,
14    /// The user's username or display name, if available
15    pub username: Option<String>,
16    /// Additional provider-specific attributes
17    pub attributes: HashMap<String, String>,
18}
19
20/// The result of an authentication attempt.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub enum AuthResult {
23    /// Authentication complete, issue a full session.
24    Success(Identity),
25    /// Primary authentication succeeded, but a second factor is required.
26    MfaRequired {
27        /// A temporary, short-lived token to bind the MFA submission to this session
28        mfa_token: String,
29        /// The external ID of the user trying to login
30        user_id: String,
31        /// A list of allowed second factors for this user
32        allowed_methods: Vec<String>,
33    },
34}
35
36/// Claims inside the temporary MFA JWT token.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct MfaTokenClaims {
39    /// Subject (the user ID)
40    pub sub: String,
41    /// Must be true for MFA tokens
42    pub mfa_pending: bool,
43    /// Expiration timestamp
44    pub exp: usize,
45}
46
47/// Represents the tokens returned by an OAuth2 provider.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct OAuthToken {
50    /// The access token used for API requests
51    pub access_token: String,
52    /// The type of token (usually "Bearer")
53    pub token_type: String,
54    /// Seconds until the access token expires
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub expires_in: Option<u64>,
57    /// The refresh token used to obtain new access tokens
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub refresh_token: Option<String>,
60    /// The scopes granted by the user
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub scope: Option<String>,
63    /// The OIDC ID Token
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub id_token: Option<String>,
66}
67
68/// Intermediate state for OAuth2/OIDC flows, stored in an encrypted cookie.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct OAuth2State {
71    /// CSRF protection state parameter
72    pub state: String,
73    /// OIDC nonce to prevent replay attacks
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub nonce: Option<String>,
76    /// PKCE code verifier
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub code_verifier: Option<String>,
79    /// Optional redirect URL to go back to after flow completion
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub success_url: Option<String>,
82    /// The provider identifier
83    pub provider_id: String,
84    /// Expiration timestamp (seconds since epoch)
85    pub expires_at: i64,
86}
87
88impl OAuth2State {
89    /// Encrypts the state into a base64-encoded string.
90    pub fn encrypt(&self, key: &[u8; 32]) -> Result<String, crate::auth::error::AuthError> {
91        use aes_gcm::{
92            aead::{Aead, KeyInit},
93            Aes256Gcm, Nonce,
94        };
95        use rand::RngCore;
96
97        let cipher = Aes256Gcm::new(key.into());
98        let mut nonce_bytes = [0u8; 12];
99        rand::rng().fill_bytes(&mut nonce_bytes);
100        let nonce = Nonce::from(nonce_bytes);
101
102        let json = serde_json::to_vec(self).map_err(|e| {
103            crate::auth::error::AuthError::Token(format!("Failed to serialize state: {e}"))
104        })?;
105
106        let ciphertext = cipher
107            .encrypt(&nonce, json.as_slice())
108            .map_err(|e| crate::auth::error::AuthError::Token(format!("Encryption failed: {e}")))?;
109
110        let mut combined = Vec::with_capacity(nonce_bytes.len() + ciphertext.len());
111        combined.extend_from_slice(&nonce_bytes);
112        combined.extend_from_slice(&ciphertext);
113
114        Ok(base64::Engine::encode(
115            &base64::engine::general_purpose::STANDARD,
116            combined,
117        ))
118    }
119
120    /// Decrypts the state from a base64-encoded string.
121    pub fn decrypt(encoded: &str, key: &[u8; 32]) -> Result<Self, crate::auth::error::AuthError> {
122        use aes_gcm::{
123            aead::{Aead, KeyInit},
124            Aes256Gcm, Nonce,
125        };
126
127        let combined = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded)
128            .map_err(|e| {
129                crate::auth::error::AuthError::Token(format!("Failed to decode base64 state: {e}"))
130            })?;
131
132        if combined.len() < 12 {
133            return Err(crate::auth::error::AuthError::Token(
134                "Invalid encrypted state".to_string(),
135            ));
136        }
137
138        let (nonce_bytes, ciphertext) = combined.split_at(12);
139        let nonce_arr: [u8; 12] = nonce_bytes.try_into().map_err(|_| {
140            crate::auth::error::AuthError::Token("Invalid nonce length".to_string())
141        })?;
142        let nonce = Nonce::from(nonce_arr);
143        let cipher = Aes256Gcm::new(key.into());
144
145        let decrypted = cipher
146            .decrypt(&nonce, ciphertext)
147            .map_err(|e| crate::auth::error::AuthError::Token(format!("Decryption failed: {e}")))?;
148
149        let state: Self = serde_json::from_slice(&decrypted).map_err(|e| {
150            crate::auth::error::AuthError::Token(format!("Failed to deserialize state: {e}"))
151        })?;
152
153        if chrono::Utc::now().timestamp() > state.expires_at {
154            return Err(crate::auth::error::AuthError::Token(
155                "State expired".to_string(),
156            ));
157        }
158
159        Ok(state)
160    }
161}