xjp-oidc 1.1.0

OIDC/OAuth2 SDK for Rust - Server and WASM support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
//! Public types for the xjp-oidc SDK

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// OAuth2/OIDC Provider metadata from discovery endpoint
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OidcProviderMetadata {
    /// Issuer identifier
    pub issuer: String,
    /// Authorization endpoint URL
    pub authorization_endpoint: String,
    /// Token endpoint URL
    pub token_endpoint: String,
    /// JWKS URI for key discovery
    pub jwks_uri: String,
    /// UserInfo endpoint URL (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub userinfo_endpoint: Option<String>,
    /// End session endpoint URL (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end_session_endpoint: Option<String>,
    /// Registration endpoint URL (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub registration_endpoint: Option<String>,
    /// Supported response types
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_types_supported: Option<Vec<String>>,
    /// Supported grant types
    #[serde(skip_serializing_if = "Option::is_none")]
    pub grant_types_supported: Option<Vec<String>>,
    /// Supported scopes
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scopes_supported: Option<Vec<String>>,
    /// Supported token endpoint auth methods
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
    /// Supported ID token signing algorithms
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id_token_signing_alg_values_supported: Option<Vec<String>>,
    /// PKCE code challenge methods supported
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code_challenge_methods_supported: Option<Vec<String>>,
    /// Subject identifier types supported (REQUIRED by OIDC spec)
    #[serde(default = "default_subject_types_supported")]
    pub subject_types_supported: Vec<String>,
    /// Token introspection endpoint URL (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub introspection_endpoint: Option<String>,
    /// Token revocation endpoint URL (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub revocation_endpoint: Option<String>,
    /// Frontchannel logout supported
    #[serde(skip_serializing_if = "Option::is_none")]
    pub frontchannel_logout_supported: Option<bool>,
    /// Frontchannel logout session supported
    #[serde(skip_serializing_if = "Option::is_none")]
    pub frontchannel_logout_session_supported: Option<bool>,
    /// Backchannel logout supported
    #[serde(skip_serializing_if = "Option::is_none")]
    pub backchannel_logout_supported: Option<bool>,
    /// Backchannel logout session supported
    #[serde(skip_serializing_if = "Option::is_none")]
    pub backchannel_logout_session_supported: Option<bool>,
    /// Tenant ID (for multi-tenant setups)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tenant_id: Option<i64>,
    /// Tenant slug (for multi-tenant setups)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tenant_slug: Option<String>,
}

/// Default value for subject_types_supported
fn default_subject_types_supported() -> Vec<String> {
    vec!["public".to_string()]
}

/// Token response from token endpoint
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenResponse {
    /// Access token for API calls
    pub access_token: String,
    /// Token type (typically "Bearer")
    pub token_type: String,
    /// Token lifetime in seconds
    pub expires_in: i64,
    /// Refresh token (if offline_access scope granted)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub refresh_token: Option<String>,
    /// Granted scopes space-delimited
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scope: Option<String>,
    /// ID token (if openid scope granted)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id_token: Option<String>,
}

/// Verified ID Token with parsed claims
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifiedIdToken {
    // Standard OIDC claims
    /// Issuer
    pub iss: String,
    /// Subject (user ID)
    pub sub: String,
    /// Audience
    pub aud: String,
    /// Expiration time (Unix timestamp)
    pub exp: i64,
    /// Issued at time (Unix timestamp)
    pub iat: i64,
    /// Nonce (if provided)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub nonce: Option<String>,
    /// Session ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sid: Option<String>,

    // Profile claims
    /// User's full name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// User's email
    #[serde(skip_serializing_if = "Option::is_none")]
    pub email: Option<String>,
    /// User's picture URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub picture: Option<String>,

    // Custom claims
    /// Authentication methods reference (e.g., ["wechat_qr"])
    #[serde(skip_serializing_if = "Option::is_none")]
    pub amr: Option<Vec<String>>,
    /// Authentication time (Unix timestamp)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub auth_time: Option<i64>,
    /// Admin flag for XiaojinPro admin users
    #[serde(skip_serializing_if = "Option::is_none")]
    pub xjp_admin: Option<bool>,
}

/// Result from building an authorization URL
#[derive(Debug, Clone)]
pub struct AuthUrlResult {
    /// The generated authorization URL
    pub url: url::Url,
    /// The state parameter (either provided or generated)
    pub state: String,
    /// The nonce parameter (either provided or generated, if openid scope requested)
    pub nonce: Option<String>,
}

/// Parameters for building authorization URL
#[derive(Debug, Clone, Default)]
pub struct BuildAuthUrl {
    /// Issuer URL
    pub issuer: String,
    /// Client ID
    pub client_id: String,
    /// Redirect URI
    pub redirect_uri: String,
    /// Requested scopes (space-separated)
    pub scope: String,
    /// State parameter (will be auto-generated if not provided)
    pub state: Option<String>,
    /// Nonce for ID token (will be auto-generated if not provided)
    pub nonce: Option<String>,
    /// Prompt parameter (e.g., "login", "consent")
    pub prompt: Option<String>,
    /// PKCE code challenge
    pub code_challenge: String,
    /// Extra query parameters
    pub extra_params: Option<HashMap<String, String>>,
    /// Tenant identifier (for multi-tenant setups)
    pub tenant: Option<String>,
    /// Optional authorization endpoint from discovery metadata
    #[cfg_attr(not(feature = "verifier"), serde(skip_serializing_if = "Option::is_none"))]
    pub authorization_endpoint: Option<String>,
}

/// Parameters for exchanging authorization code
#[derive(Debug, Clone)]
pub struct ExchangeCode {
    /// Issuer URL
    pub issuer: String,
    /// Client ID
    pub client_id: String,
    /// Authorization code
    pub code: String,
    /// Redirect URI (must match the one used in authorization)
    pub redirect_uri: String,
    /// PKCE code verifier (optional for confidential clients)
    pub code_verifier: Option<String>,
    /// Client secret (for confidential clients)
    pub client_secret: Option<String>,
    /// Token endpoint authentication method from discovery metadata
    /// (e.g., "client_secret_basic", "client_secret_post", "none")
    pub token_endpoint_auth_method: Option<String>,
}

/// Parameters for end session (logout)
#[derive(Debug, Clone, Default)]
pub struct EndSession {
    /// Issuer URL
    pub issuer: String,
    /// ID token hint
    pub id_token_hint: String,
    /// Post-logout redirect URI
    pub post_logout_redirect_uri: Option<String>,
    /// State parameter
    pub state: Option<String>,
    /// End session endpoint from discovery metadata
    /// If not provided, will use default path: issuer + "/oidc/end_session"
    pub end_session_endpoint: Option<String>,
}

/// Callback parameters from authorization response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallbackParams {
    /// Authorization code
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code: Option<String>,
    /// State parameter
    #[serde(skip_serializing_if = "Option::is_none")]
    pub state: Option<String>,
    /// Error code
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    /// Error description
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_description: Option<String>,
}

/// Dynamic Client Registration request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterRequest {
    /// Application type (web, native)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub application_type: Option<String>,
    /// Redirect URIs
    pub redirect_uris: Vec<String>,
    /// Post-logout redirect URIs
    #[serde(skip_serializing_if = "Option::is_none")]
    pub post_logout_redirect_uris: Option<Vec<String>>,
    /// Grant types
    pub grant_types: Vec<String>,
    /// Token endpoint auth method
    pub token_endpoint_auth_method: String,
    /// Requested scopes
    pub scope: String,
    /// Contact emails
    #[serde(skip_serializing_if = "Option::is_none")]
    pub contacts: Option<Vec<String>>,
    /// Software ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub software_id: Option<String>,
    /// Client name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_name: Option<String>,
}

/// Client registration result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientRegistrationResult {
    /// Client ID
    pub client_id: String,
    /// Client secret (for confidential clients)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_secret: Option<String>,
    /// Registration status
    pub status: ClientStatus,
    /// Client name
    pub client_name: String,
    /// Redirect URIs
    pub redirect_uris: Vec<String>,
    /// Post-logout redirect URIs
    #[serde(skip_serializing_if = "Option::is_none")]
    pub post_logout_redirect_uris: Option<Vec<String>>,
    /// Grant types
    pub grant_types: Vec<String>,
    /// Token endpoint auth method
    pub token_endpoint_auth_method: String,
    /// Allowed scopes
    pub scope: String,
}

/// Client registration status
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ClientStatus {
    /// Client is active and can be used
    Active,
    /// Client is pending approval
    Pending,
    /// Client is suspended
    Suspended,
}

/// Verified claims from access token (for Resource Server)
#[cfg(feature = "verifier")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifiedClaims {
    /// Issuer
    pub iss: String,
    /// Subject
    pub sub: String,
    /// Audience
    pub aud: String,
    /// Expiration
    pub exp: i64,
    /// Issued at
    pub iat: i64,
    /// JWT ID
    pub jti: String,
    /// Scopes
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scope: Option<String>,
    /// Admin flag
    #[serde(skip_serializing_if = "Option::is_none")]
    pub xjp_admin: Option<bool>,
    /// Authentication methods
    #[serde(skip_serializing_if = "Option::is_none")]
    pub amr: Option<Vec<String>>,
    /// Authentication time
    #[serde(skip_serializing_if = "Option::is_none")]
    pub auth_time: Option<i64>,
}

/// Options for verifying ID tokens
#[derive(Clone)]
pub struct VerifyOptions<'a> {
    /// Expected issuer
    pub issuer: &'a str,
    /// Expected audience
    pub audience: &'a str,
    /// Expected nonce (if any)
    pub nonce: Option<&'a str>,
    /// Maximum age in seconds (for auth_time validation)
    pub max_age_sec: Option<i64>,
    /// Clock skew tolerance in seconds
    pub clock_skew_sec: Option<i64>,
    /// HTTP client for fetching JWKS
    pub http: &'a dyn crate::http::HttpClient,
    /// Cache for JWKS
    pub cache: &'a dyn crate::cache::Cache<String, crate::jwks::Jwks>,
}

impl Default for VerifyOptions<'_> {
    fn default() -> Self {
        panic!("VerifyOptions requires explicit construction with required fields")
    }
}

/// Token introspection request
#[derive(Debug, Clone)]
pub struct IntrospectRequest {
    /// Issuer URL
    pub issuer: String,
    /// Client ID
    pub client_id: String,
    /// Client secret (for confidential clients)
    pub client_secret: Option<String>,
    /// Token to introspect
    pub token: String,
    /// Hint about the type of the token
    pub token_type_hint: Option<String>,
    /// Token endpoint authentication method
    pub token_endpoint_auth_method: Option<String>,
}

/// Token introspection response (RFC 7662)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntrospectResponse {
    /// Whether the token is active
    pub active: bool,
    /// Space-separated list of scopes
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scope: Option<String>,
    /// Client identifier
    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_id: Option<String>,
    /// Human-readable identifier for the resource owner
    #[serde(skip_serializing_if = "Option::is_none")]
    pub username: Option<String>,
    /// Type of the token
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token_type: Option<String>,
    /// Expiration time (Unix timestamp)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exp: Option<i64>,
    /// Issued at time (Unix timestamp)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iat: Option<i64>,
    /// Not before time (Unix timestamp)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub nbf: Option<i64>,
    /// Subject of the token
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sub: Option<String>,
    /// Audience of the token
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aud: Option<Vec<String>>,
    /// Issuer of the token
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iss: Option<String>,
    /// Unique identifier for the token
    #[serde(skip_serializing_if = "Option::is_none")]
    pub jti: Option<String>,
}

/// Refresh token request
#[derive(Debug, Clone)]
pub struct RefreshTokenRequest {
    /// Issuer URL
    pub issuer: String,
    /// Client ID
    pub client_id: String,
    /// Client secret (for confidential clients)
    pub client_secret: Option<String>,
    /// Refresh token
    pub refresh_token: String,
    /// Requested scope (optional)
    pub scope: Option<String>,
    /// Token endpoint authentication method
    pub token_endpoint_auth_method: Option<String>,
}

/// UserInfo response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
    /// Subject - Identifier for the End-User
    pub sub: String,
    /// End-User's full name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Given name(s) or first name(s)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub given_name: Option<String>,
    /// Surname(s) or last name(s)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub family_name: Option<String>,
    /// Middle name(s)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub middle_name: Option<String>,
    /// Casual name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub nickname: Option<String>,
    /// Preferred username
    #[serde(skip_serializing_if = "Option::is_none")]
    pub preferred_username: Option<String>,
    /// Profile page URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub profile: Option<String>,
    /// Profile picture URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub picture: Option<String>,
    /// Web page or blog URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub website: Option<String>,
    /// Preferred e-mail address
    #[serde(skip_serializing_if = "Option::is_none")]
    pub email: Option<String>,
    /// True if e-mail address has been verified
    #[serde(skip_serializing_if = "Option::is_none")]
    pub email_verified: Option<bool>,
    /// Gender
    #[serde(skip_serializing_if = "Option::is_none")]
    pub gender: Option<String>,
    /// Birthday (ISO 8601:2004 YYYY-MM-DD format)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub birthdate: Option<String>,
    /// Time zone (e.g., Europe/Paris, America/Los_Angeles)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub zoneinfo: Option<String>,
    /// Locale (e.g., en-US, fr-CA)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub locale: Option<String>,
    /// Preferred telephone number
    #[serde(skip_serializing_if = "Option::is_none")]
    pub phone_number: Option<String>,
    /// True if phone number has been verified
    #[serde(skip_serializing_if = "Option::is_none")]
    pub phone_number_verified: Option<bool>,
    /// Preferred postal address
    #[serde(skip_serializing_if = "Option::is_none")]
    pub address: Option<serde_json::Value>,
    /// Time the information was last updated
    #[serde(skip_serializing_if = "Option::is_none")]
    pub updated_at: Option<i64>,
    // Custom claims for XiaojinPro
    /// Admin flag for XiaojinPro admin users
    #[serde(skip_serializing_if = "Option::is_none")]
    pub xjp_admin: Option<bool>,
    /// Authentication methods reference
    #[serde(skip_serializing_if = "Option::is_none")]
    pub amr: Option<Vec<String>>,
    /// Authentication time
    #[serde(skip_serializing_if = "Option::is_none")]
    pub auth_time: Option<i64>,
}

/// Client configuration from DCR GET endpoint
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
    /// Client ID
    pub client_id: String,
    /// Client secret (for confidential clients)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_secret: Option<String>,
    /// Client name
    pub client_name: String,
    /// Redirect URIs
    pub redirect_uris: Vec<String>,
    /// Post-logout redirect URIs
    #[serde(skip_serializing_if = "Option::is_none")]
    pub post_logout_redirect_uris: Option<Vec<String>>,
    /// Grant types
    pub grant_types: Vec<String>,
    /// Response types
    pub response_types: Vec<String>,
    /// Token endpoint auth method
    pub token_endpoint_auth_method: String,
    /// Allowed scopes
    pub scope: String,
    /// Client secret expiration time
    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_secret_expires_at: Option<i64>,
}