1use chrono::{DateTime, Duration, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct TokenResponse {
9 pub access_token: String,
11 pub refresh_token: Option<String>,
13 pub token_type: String,
15 pub expires_in: u64,
17 pub id_token: Option<String>,
19 pub scope: Option<String>,
21}
22
23impl TokenResponse {
24 pub const fn new(access_token: String, token_type: String, expires_in: u64) -> Self {
26 Self {
27 access_token,
28 refresh_token: None,
29 token_type,
30 expires_in,
31 id_token: None,
32 scope: None,
33 }
34 }
35
36 pub fn expiry_time(&self) -> DateTime<Utc> {
38 Utc::now() + Duration::seconds(self.expires_in.cast_signed())
39 }
40
41 pub fn is_expired(&self) -> bool {
43 self.expiry_time() <= Utc::now()
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct IdTokenClaims {
50 pub iss: String,
52 pub sub: String,
54 pub aud: String,
56 pub exp: i64,
58 pub iat: i64,
60 pub auth_time: Option<i64>,
62 pub nonce: Option<String>,
64 pub email: Option<String>,
66 pub email_verified: Option<bool>,
68 pub name: Option<String>,
70 pub picture: Option<String>,
72 pub locale: Option<String>,
74}
75
76impl IdTokenClaims {
77 pub const fn new(iss: String, sub: String, aud: String, exp: i64, iat: i64) -> Self {
79 Self {
80 iss,
81 sub,
82 aud,
83 exp,
84 iat,
85 auth_time: None,
86 nonce: None,
87 email: None,
88 email_verified: None,
89 name: None,
90 picture: None,
91 locale: None,
92 }
93 }
94
95 pub fn is_expired(&self) -> bool {
97 self.exp <= Utc::now().timestamp()
98 }
99
100 pub fn is_expiring_soon(&self, grace_seconds: i64) -> bool {
102 self.exp <= (Utc::now().timestamp() + grace_seconds)
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct UserInfo {
109 pub sub: String,
111 pub email: Option<String>,
113 pub email_verified: Option<bool>,
115 pub name: Option<String>,
117 pub picture: Option<String>,
119 pub locale: Option<String>,
121}
122
123impl UserInfo {
124 pub const fn new(sub: String) -> Self {
126 Self {
127 sub,
128 email: None,
129 email_verified: None,
130 name: None,
131 picture: None,
132 locale: None,
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
144 fn test_token_response_deserializes_from_json() {
145 let json = r#"{
146 "access_token": "eyJhbGciOiJSUzI1NiJ9.test.sig",
147 "token_type": "Bearer",
148 "expires_in": 3600,
149 "refresh_token": "rt-abc123",
150 "scope": "openid profile email"
151 }"#;
152
153 let token: TokenResponse = serde_json::from_str(json)
154 .expect("valid OAuth token response JSON must deserialize successfully");
155
156 assert_eq!(token.access_token, "eyJhbGciOiJSUzI1NiJ9.test.sig");
157 assert_eq!(token.token_type, "Bearer");
158 assert_eq!(token.expires_in, 3600);
159 assert_eq!(token.refresh_token, Some("rt-abc123".to_string()));
160 assert_eq!(token.scope, Some("openid profile email".to_string()));
161 }
162
163 #[test]
164 fn test_token_response_missing_optional_fields() {
165 let json = r#"{
166 "access_token": "at_minimal",
167 "token_type": "Bearer",
168 "expires_in": 3600
169 }"#;
170
171 let token: TokenResponse = serde_json::from_str(json)
172 .expect("token response without optional fields must still deserialize");
173
174 assert!(token.refresh_token.is_none(), "missing refresh_token must deserialize to None");
175 assert!(token.id_token.is_none(), "missing id_token must deserialize to None");
176 assert!(token.scope.is_none(), "missing scope must deserialize to None");
177 }
178
179 #[test]
180 fn test_token_response_missing_access_token_fails() {
181 let json = r#"{
182 "token_type": "Bearer",
183 "expires_in": 3600
184 }"#;
185
186 let result: Result<TokenResponse, _> = serde_json::from_str(json);
187 assert!(result.is_err(), "token response without access_token must fail to deserialize");
188 }
189
190 #[test]
191 fn test_token_response_expiry_is_in_future() {
192 let token = TokenResponse::new("at".to_string(), "Bearer".to_string(), 3600);
193 let expiry = token.expiry_time();
194 assert!(
195 expiry > Utc::now(),
196 "expiry_time for a token with expires_in=3600 must be in the future"
197 );
198 }
199
200 #[test]
201 fn test_token_response_new_is_not_expired() {
202 let token = TokenResponse::new("at".to_string(), "Bearer".to_string(), 3600);
203 assert!(
204 !token.is_expired(),
205 "a freshly created token with expires_in=3600 must not be expired"
206 );
207 }
208
209 #[test]
212 fn test_id_token_claims_not_expired() {
213 let exp = (Utc::now() + chrono::Duration::hours(1)).timestamp();
214 let claims = IdTokenClaims::new(
215 "https://issuer.example.com".to_string(),
216 "user123".to_string(),
217 "client_id".to_string(),
218 exp,
219 Utc::now().timestamp(),
220 );
221 assert!(!claims.is_expired(), "future exp must not be expired");
222 }
223
224 #[test]
225 fn test_id_token_claims_expired() {
226 let exp = (Utc::now() - chrono::Duration::hours(1)).timestamp();
227 let claims = IdTokenClaims::new(
228 "https://issuer.example.com".to_string(),
229 "user123".to_string(),
230 "client_id".to_string(),
231 exp,
232 Utc::now().timestamp(),
233 );
234 assert!(claims.is_expired(), "past exp must be expired");
235 }
236
237 #[test]
238 fn test_id_token_claims_expiring_soon() {
239 let exp = (Utc::now() + chrono::Duration::seconds(30)).timestamp();
240 let claims = IdTokenClaims::new(
241 "https://issuer.example.com".to_string(),
242 "user123".to_string(),
243 "client_id".to_string(),
244 exp,
245 Utc::now().timestamp(),
246 );
247 assert!(
248 claims.is_expiring_soon(60),
249 "token expiring in 30s must be considered expiring soon with grace=60s"
250 );
251 assert!(
252 !claims.is_expiring_soon(10),
253 "token expiring in 30s must not be considered expiring soon with grace=10s"
254 );
255 }
256
257 #[test]
260 fn test_userinfo_creation() {
261 let user = UserInfo::new("sub_123".to_string());
262 assert_eq!(user.sub, "sub_123");
263 assert!(user.email.is_none());
264 assert!(user.name.is_none());
265 }
266
267 #[test]
268 fn test_userinfo_deserializes_from_json() {
269 let json = r#"{
270 "sub": "user_789",
271 "email": "user@example.com",
272 "email_verified": true,
273 "name": "Test User"
274 }"#;
275 let user: UserInfo =
276 serde_json::from_str(json).expect("valid userinfo JSON must deserialize");
277 assert_eq!(user.sub, "user_789");
278 assert_eq!(user.email, Some("user@example.com".to_string()));
279 assert_eq!(user.email_verified, Some(true));
280 }
281}