Skip to main content

nylas_types/
auth.rs

1//! # Authentication Types
2//!
3//! OAuth 2.0 authentication, PKCE, and token management types for Nylas API v3.
4
5use crate::common::{EmailAddress, GrantId, Provider};
6use serde::{Deserialize, Serialize};
7
8/// OAuth authorization code exchange request
9#[derive(Debug, Clone, Serialize)]
10pub struct TokenExchangeRequest {
11    /// The authorization code from the OAuth callback
12    pub code: String,
13
14    /// Must be "authorization_code"
15    pub grant_type: String,
16
17    /// Your Nylas client ID
18    pub client_id: String,
19
20    /// Optional client secret (for confidential clients)
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub client_secret: Option<String>,
23
24    /// PKCE code verifier (if using PKCE)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub code_verifier: Option<String>,
27
28    /// Redirect URI used in the authorization request
29    pub redirect_uri: String,
30}
31
32/// OAuth token exchange response
33#[derive(Debug, Clone, Deserialize)]
34pub struct TokenExchangeResponse {
35    /// Access token for API requests
36    pub access_token: String,
37
38    /// Token type (always "Bearer")
39    pub token_type: String,
40
41    /// Grant ID associated with this token
42    pub grant_id: GrantId,
43
44    /// Scope granted (comma-separated)
45    pub scope: String,
46
47    /// Email address of the authenticated user
48    pub email: EmailAddress,
49
50    /// Provider (google, microsoft, etc.)
51    pub provider: Provider,
52}
53
54/// PKCE code challenge method
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum CodeChallengeMethod {
58    /// Plain text (not recommended)
59    Plain,
60
61    /// SHA-256 hash (recommended)
62    S256,
63}
64
65/// PKCE code pair for secure OAuth flows
66///
67/// Proof Key for Code Exchange (PKCE) prevents authorization code interception attacks.
68/// Always use S256 method for production applications.
69///
70/// # Example
71///
72/// ```
73/// # use nylas_types::auth::PkceCodePair;
74/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
75/// // Generate a new PKCE code pair
76/// let pkce = PkceCodePair::generate()?;
77///
78/// // Use the challenge in the authorization URL
79/// println!("Challenge: {}", pkce.challenge);
80///
81/// // Store the verifier securely for token exchange
82/// println!("Verifier: {}", pkce.verifier);
83/// # Ok(())
84/// # }
85/// ```
86#[derive(Debug, Clone)]
87pub struct PkceCodePair {
88    /// Code verifier (random string, 43-128 characters)
89    pub verifier: String,
90
91    /// Code challenge (hash of verifier)
92    pub challenge: String,
93
94    /// Challenge method used
95    pub method: CodeChallengeMethod,
96}
97
98impl PkceCodePair {
99    /// Generate a new PKCE code pair using S256 method
100    ///
101    /// Creates a cryptographically secure random verifier and computes
102    /// the SHA-256 challenge.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if random number generation fails.
107    ///
108    /// # Example
109    ///
110    /// ```
111    /// # use nylas_types::auth::PkceCodePair;
112    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
113    /// let pkce = PkceCodePair::generate()?;
114    /// assert!(!pkce.verifier.is_empty());
115    /// assert!(!pkce.challenge.is_empty());
116    /// # Ok(())
117    /// # }
118    /// ```
119    pub fn generate() -> Result<Self, PkceError> {
120        use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
121        use sha2::{Digest, Sha256};
122
123        // Generate 32 random bytes (256 bits)
124        let mut verifier_bytes = [0u8; 32];
125        getrandom::getrandom(&mut verifier_bytes)
126            .map_err(|e| PkceError::RandomGeneration(e.to_string()))?;
127
128        // Base64url encode (no padding)
129        let verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
130
131        // SHA256 hash
132        let mut hasher = Sha256::new();
133        hasher.update(verifier.as_bytes());
134        let challenge_bytes = hasher.finalize();
135
136        // Base64url encode challenge
137        let challenge = URL_SAFE_NO_PAD.encode(challenge_bytes);
138
139        Ok(Self {
140            verifier,
141            challenge,
142            method: CodeChallengeMethod::S256,
143        })
144    }
145}
146
147/// PKCE generation errors
148#[derive(Debug, thiserror::Error)]
149pub enum PkceError {
150    /// Failed to generate random bytes
151    #[error("Failed to generate random bytes: {0}")]
152    RandomGeneration(String),
153}
154
155/// Provider detection request
156#[derive(Debug, Clone, Serialize)]
157pub struct ProviderDetectRequest {
158    /// Email address to detect provider for
159    pub email: EmailAddress,
160}
161
162/// Provider detection response
163#[derive(Debug, Clone, Deserialize)]
164pub struct ProviderDetectResponse {
165    /// Detected provider
166    pub provider: Provider,
167
168    /// Whether the provider was detected
169    pub detected: bool,
170
171    /// Provider type (oauth or imap)
172    #[serde(rename = "type")]
173    pub provider_type: String,
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_pkce_generation() {
182        let pkce = PkceCodePair::generate().unwrap();
183        assert!(!pkce.verifier.is_empty());
184        assert!(!pkce.challenge.is_empty());
185        assert_eq!(pkce.method, CodeChallengeMethod::S256);
186    }
187
188    #[test]
189    fn test_pkce_verifier_length() {
190        let pkce = PkceCodePair::generate().unwrap();
191        // Base64url encoded 32 bytes should be 43 characters (no padding)
192        assert_eq!(pkce.verifier.len(), 43);
193    }
194
195    #[test]
196    fn test_pkce_challenge_deterministic() {
197        use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
198        use sha2::{Digest, Sha256};
199
200        let pkce = PkceCodePair::generate().unwrap();
201
202        // Manually compute challenge from verifier
203        let mut hasher = Sha256::new();
204        hasher.update(pkce.verifier.as_bytes());
205        let expected_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
206
207        assert_eq!(pkce.challenge, expected_challenge);
208    }
209
210    #[test]
211    fn test_token_exchange_request_serialization() {
212        let request = TokenExchangeRequest {
213            code: "auth_code_123".to_string(),
214            grant_type: "authorization_code".to_string(),
215            client_id: "client_123".to_string(),
216            client_secret: Some("secret_123".to_string()),
217            code_verifier: None,
218            redirect_uri: "http://localhost:3000/callback".to_string(),
219        };
220
221        let json = serde_json::to_string(&request).unwrap();
222        assert!(json.contains("auth_code_123"));
223        assert!(json.contains("authorization_code"));
224        assert!(json.contains("client_123"));
225    }
226
227    #[test]
228    fn test_code_challenge_method_serialization() {
229        let s256 = CodeChallengeMethod::S256;
230        let json = serde_json::to_string(&s256).unwrap();
231        assert_eq!(json, "\"s256\"");
232
233        let plain = CodeChallengeMethod::Plain;
234        let json = serde_json::to_string(&plain).unwrap();
235        assert_eq!(json, "\"plain\"");
236    }
237
238    #[test]
239    fn test_provider_detect_request_serialization() {
240        let request = ProviderDetectRequest {
241            email: EmailAddress::new("user@gmail.com").unwrap(),
242        };
243
244        let json = serde_json::to_string(&request).unwrap();
245        assert!(json.contains("user@gmail.com"));
246    }
247}