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/// Represents the tokens returned by an OAuth2 provider.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct OAuthToken {
23    /// The access token used for API requests
24    pub access_token: String,
25    /// The type of token (usually "Bearer")
26    pub token_type: String,
27    /// Seconds until the access token expires
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub expires_in: Option<u64>,
30    /// The refresh token used to obtain new access tokens
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub refresh_token: Option<String>,
33    /// The scopes granted by the user
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub scope: Option<String>,
36    /// The OIDC ID Token
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub id_token: Option<String>,
39}
40
41/// Intermediate state for OAuth2/OIDC flows, stored in an encrypted cookie.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct OAuth2State {
44    /// CSRF protection state parameter
45    pub state: String,
46    /// OIDC nonce to prevent replay attacks
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub nonce: Option<String>,
49    /// PKCE code verifier
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub code_verifier: Option<String>,
52    /// Optional redirect URL to go back to after flow completion
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub success_url: Option<String>,
55    /// The provider identifier
56    pub provider_id: String,
57    /// Expiration timestamp (seconds since epoch)
58    pub expires_at: i64,
59}
60
61impl OAuth2State {
62    /// Encrypts the state into a base64-encoded string.
63    pub fn encrypt(&self, key: &[u8; 32]) -> Result<String, crate::auth::error::AuthError> {
64        use aes_gcm::{
65            aead::{Aead, KeyInit},
66            Aes256Gcm, Nonce,
67        };
68        use rand::RngCore;
69
70        let cipher = Aes256Gcm::new(key.into());
71        let mut nonce_bytes = [0u8; 12];
72        rand::rng().fill_bytes(&mut nonce_bytes);
73        let nonce = Nonce::from(nonce_bytes);
74
75        let json = serde_json::to_vec(self).map_err(|e| {
76            crate::auth::error::AuthError::Token(format!("Failed to serialize state: {e}"))
77        })?;
78
79        let ciphertext = cipher
80            .encrypt(&nonce, json.as_slice())
81            .map_err(|e| crate::auth::error::AuthError::Token(format!("Encryption failed: {e}")))?;
82
83        let mut combined = Vec::with_capacity(nonce_bytes.len() + ciphertext.len());
84        combined.extend_from_slice(&nonce_bytes);
85        combined.extend_from_slice(&ciphertext);
86
87        Ok(base64::Engine::encode(
88            &base64::engine::general_purpose::STANDARD,
89            combined,
90        ))
91    }
92
93    /// Decrypts the state from a base64-encoded string.
94    pub fn decrypt(encoded: &str, key: &[u8; 32]) -> Result<Self, crate::auth::error::AuthError> {
95        use aes_gcm::{
96            aead::{Aead, KeyInit},
97            Aes256Gcm, Nonce,
98        };
99
100        let combined = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded)
101            .map_err(|e| {
102                crate::auth::error::AuthError::Token(format!("Failed to decode base64 state: {e}"))
103            })?;
104
105        if combined.len() < 12 {
106            return Err(crate::auth::error::AuthError::Token(
107                "Invalid encrypted state".to_string(),
108            ));
109        }
110
111        let (nonce_bytes, ciphertext) = combined.split_at(12);
112        let nonce_arr: [u8; 12] = nonce_bytes.try_into().map_err(|_| {
113            crate::auth::error::AuthError::Token("Invalid nonce length".to_string())
114        })?;
115        let nonce = Nonce::from(nonce_arr);
116        let cipher = Aes256Gcm::new(key.into());
117
118        let decrypted = cipher
119            .decrypt(&nonce, ciphertext)
120            .map_err(|e| crate::auth::error::AuthError::Token(format!("Decryption failed: {e}")))?;
121
122        let state: Self = serde_json::from_slice(&decrypted).map_err(|e| {
123            crate::auth::error::AuthError::Token(format!("Failed to deserialize state: {e}"))
124        })?;
125
126        if chrono::Utc::now().timestamp() > state.expires_at {
127            return Err(crate::auth::error::AuthError::Token(
128                "State expired".to_string(),
129            ));
130        }
131
132        Ok(state)
133    }
134}