rocket_oidc/client.rs
1use crate::{AddClaims, PronounClaim};
2use jsonwebtoken::*;
3use openidconnect::core::CoreGenderClaim;
4use openidconnect::core::*;
5use reqwest::Url;
6use serde_derive::*;
7use std::str::FromStr;
8
9use crate::CoreClaims;
10use crate::Error;
11use crate::OIDCConfig;
12use serde_json::Value;
13use std::collections::HashMap;
14use std::fmt::Debug;
15
16use crate::token::*;
17use serde::de::DeserializeOwned;
18use std::collections::HashSet;
19use std::path::Path;
20use tokio::{fs::File, io::AsyncReadExt};
21
22use openidconnect::reqwest;
23use openidconnect::*;
24use serde::Serialize;
25
26pub type OpenIDClient<
27 HasDeviceAuthUrl = EndpointNotSet,
28 HasIntrospectionUrl = EndpointNotSet,
29 HasRevocationUrl = EndpointNotSet,
30 HasAuthUrl = EndpointSet,
31 HasTokenUrl = EndpointMaybeSet,
32 HasUserInfoUrl = EndpointMaybeSet,
33> = openidconnect::Client<
34 EmptyAdditionalClaims,
35 CoreAuthDisplay,
36 CoreGenderClaim,
37 CoreJweContentEncryptionAlgorithm,
38 CoreJsonWebKey,
39 CoreAuthPrompt,
40 StandardErrorResponse<CoreErrorResponseType>,
41 CoreTokenResponse,
42 CoreTokenIntrospectionResponse,
43 CoreRevocableToken,
44 CoreRevocationErrorResponse,
45 HasAuthUrl,
46 HasDeviceAuthUrl,
47 HasIntrospectionUrl,
48 HasRevocationUrl,
49 HasTokenUrl,
50 HasUserInfoUrl,
51>;
52
53fn join_url(root: &str, route: &str) -> Result<String, url::ParseError> {
54 let base = Url::parse(root)?;
55 let joined = base.join(route)?;
56 Ok(joined.into())
57}
58
59fn trim_trailing_whitespace(s: &str) -> String {
60 s.trim_end().to_string()
61}
62
63async fn laod_client_secret<P: AsRef<Path>>(
64 secret_file: P,
65) -> Result<ClientSecret, std::io::Error> {
66 let mut file = File::open(secret_file.as_ref()).await?;
67 let mut contents = String::new();
68
69 file.read_to_string(&mut contents).await?;
70 let secret = trim_trailing_whitespace(&contents);
71 #[cfg(debug_assertions)]
72 println!("using secret: {}", secret);
73 Ok(ClientSecret::new(secret))
74}
75
76fn hashset_from<T: std::cmp::Eq + std::hash::Hash>(vals: Vec<T>) -> HashSet<T> {
77 let mut set = HashSet::new();
78 for val in vals.into_iter() {
79 set.insert(val);
80 }
81 set
82}
83
84/// Configuration used internally by the OIDC client to manage static values.
85///
86/// Contains client credentials and metadata loaded from a higher-level `OIDCConfig`.
87#[derive(Debug, Clone, Serialize)]
88pub struct WorkingConfig {
89 client_secret: ClientSecret,
90 client_id: ClientId,
91 issuer_url: IssuerUrl,
92 redirect: String,
93}
94
95impl WorkingConfig {
96 /// Constructs a new `WorkingConfig` from a high-level `OIDCConfig`.
97 ///
98 /// Loads the client secret asynchronously (e.g., from a file or secure vault).
99 ///
100 /// # Arguments
101 /// * `config` - The high-level configuration containing static strings and secret references.
102 ///
103 /// # Returns
104 /// * `Ok(WorkingConfig)` on success.
105 /// * `Err` if loading or parsing fails.
106 pub async fn from_oidc_config(config: &OIDCConfig) -> Result<Self, Box<dyn std::error::Error>> {
107 let client_id = config.client_id.clone();
108 let issuer_url = config.issuer_url.clone();
109
110 let client_id = ClientId::new(client_id);
111 let client_secret = laod_client_secret(&config.client_secret).await?;
112 let issuer_url = IssuerUrl::new(issuer_url)?;
113
114 Ok(Self {
115 client_id,
116 client_secret,
117 issuer_url,
118 redirect: config.redirect.clone(),
119 })
120 }
121}
122
123/// Identifier for a public key used in token validation.
124///
125/// Combines the issuer and algorithm to uniquely reference a key in a multi-issuer or multi-algorithm environment.
126/// This is not that same as what's used within openidconnect crate, just used by this crates validator.
127#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
128pub struct KeyID {
129 issuer: String,
130 alg: String,
131}
132
133impl KeyID {
134 /// Creates a new `KeyID` from issuer and algorithm strings.
135 ///
136 /// # Arguments
137 /// * `issuer` - The key issuer.
138 /// * `alg` - The signing algorithm.
139 pub fn new(issuer: &str, alg: &str) -> Self {
140 KeyID {
141 issuer: issuer.to_string(),
142 alg: alg.to_string(),
143 }
144 }
145}
146
147/// Represents a manually configured public key endpoint for token validation.
148///
149/// Contains validation rules and the decoding key used to verify signatures.
150#[derive(Clone, Debug)]
151pub struct Endpoint {
152 validation: Validation,
153 pubkey: DecodingKey,
154}
155
156impl Endpoint {
157 /// Creates a new `Endpoint` with the given validation rules and decoding key.
158 ///
159 /// # Arguments
160 /// * `validation` - Validation settings (audience, issuer, leeway, etc.).
161 /// * `pubkey` - The public key used to verify signatures.
162 pub fn new(validation: Validation, pubkey: DecodingKey) -> Self {
163 Self { validation, pubkey }
164 }
165}
166
167/// A helper type for validating JSON Web Tokens (JWTs) against multiple issuers and algorithms.
168///
169/// `Validator` manages a collection of public keys and associated validation rules
170/// (wrapped in `Endpoint` structs) that can be used to decode and verify tokens.
171/// It supports loading keys dynamically from JWKS endpoints or inserting them manually.
172///
173/// Typically used when your application needs to accept tokens from multiple providers
174/// (e.g., multiple OpenID Connect issuers) or support multiple signing algorithms.
175#[derive(Clone, Debug)]
176pub struct Validator {
177 // A mapping from composite key identifiers (`KeyID`) — usually derived from issuer and algorithm —
178 // to the corresponding validation endpoint (`Endpoint`) containing the decoding key and validation rules.
179 pubkeys: HashMap<KeyID, Endpoint>,
180
181 // Default issuer URL, used by legacy or simplified decoding methods.
182 // Note: this may not always be correct if your validator handles multiple issuers.
183 default_iss: String,
184}
185
186fn parse_jwks(
187 issuer: &str,
188 jwks_json: &str,
189 mut validation: Validation,
190) -> Result<HashMap<KeyID, Endpoint>, Box<dyn std::error::Error>> {
191 let jwks: Value = serde_json::from_str(jwks_json)?;
192 let keys_array = jwks["keys"]
193 .as_array()
194 .ok_or("JWKS does not contain a 'keys' array")?;
195
196 let mut keys = HashMap::new();
197
198 for jwk in keys_array {
199 let alg = jwk["alg"].as_str().ok_or("Missing 'alg' in JWK")?;
200 let kid = jwk["kid"].as_str().unwrap_or("default");
201
202 let decoding_key = match alg {
203 "RS256" | "RS384" | "RS512" => {
204 let n = jwk["n"].as_str().ok_or("Missing 'n' in RSA JWK")?;
205 let e = jwk["e"].as_str().ok_or("Missing 'e' in RSA JWK")?;
206 DecodingKey::from_rsa_components(n, e)?
207 }
208
209 "ES256" | "ES384" | "ES512" => {
210 let x = jwk["x"].as_str().ok_or("Missing 'x' in EC JWK")?;
211 let y = jwk["y"].as_str().ok_or("Missing 'y' in EC JWK")?;
212 DecodingKey::from_ec_components(x, y)?
213 }
214
215 "HS256" | "HS384" | "HS512" => {
216 let k = jwk["k"].as_str().ok_or("Missing 'k' in symmetric JWK")?;
217 DecodingKey::from_base64_secret(k)?
218 }
219
220 other => {
221 eprintln!("Unsupported algorithm: {}", other);
222 continue; // skip this key
223 }
224 };
225
226 let key_id = KeyID::new(issuer, alg);
227 validation.algorithms = vec![Algorithm::from_str(alg)?];
228 keys.insert(key_id, Endpoint::new(validation.clone(), decoding_key));
229 }
230
231 Ok(keys)
232}
233
234impl Validator {
235 /// Creates a new `Validator` from a single public key.
236 ///
237 /// This is useful when you already have a known key (for example, configured statically)
238 /// and want to build a validator around it.
239 ///
240 /// * `url` - Issuer URL.
241 /// * `audiance` - Expected audience claim (usually your client ID).
242 /// * `algorithm` - Signing algorithm (e.g., "RS256").
243 /// * `public_key` - Decoding key used to verify signatures.
244 pub fn from_pubkey(
245 url: String,
246 audiance: String,
247 algorithm: String,
248 public_key: DecodingKey,
249 ) -> Result<Self, Box<dyn std::error::Error>> {
250 let pubkeys = HashMap::new();
251 let algo = Algorithm::from_str(&algorithm)?;
252 //let validation = Validation::new(algo);
253 //validation.insecure_disable_signature_validation();
254 let mut validator = Self {
255 pubkeys,
256 default_iss: url.clone(),
257 };
258
259 validator.insert_pubkey(url, audiance, algorithm, public_key)?;
260 Ok(validator)
261 }
262
263 /// Creates a new `Validator` from an RSA PEM encoded public key.
264 ///
265 /// This is a convenience wrapper around `from_pubkey` that accepts a PEM string
266 /// (PKCS#1 / PKCS#8 public key) and builds the `DecodingKey` for you.
267 ///
268 /// * `url` - Issuer URL (used to construct the KeyID).
269 /// * `audiance` - Expected audience claim.
270 /// * `algorithm` - Signing algorithm (e.g., "RS256").
271 /// * `pem` - RSA public key in PEM format.
272 pub fn from_rsa_pem(
273 url: String,
274 audiance: String,
275 algorithm: String,
276 pem: &str,
277 ) -> Result<Self, Box<dyn std::error::Error>> {
278 let decoding_key = DecodingKey::from_rsa_pem(pem.as_bytes())
279 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
280 Self::from_pubkey(url, audiance, algorithm, decoding_key)
281 }
282
283 /// Returns a sorted list of unique algorithms supported for the given issuer,
284 /// based on the pubkeys map.
285 pub fn get_supported_algorithms_for_issuer(&self, issuer: &str) -> Option<Vec<String>> {
286 let mut algs: Vec<String> = self
287 .pubkeys
288 .keys()
289 .filter(|key_id| key_id.issuer == issuer)
290 .map(|key_id| key_id.alg.clone())
291 .collect();
292
293 // Remove duplicates & sort
294 algs.sort();
295 algs.dedup();
296
297 if algs.is_empty() { None } else { Some(algs) }
298 }
299
300 pub fn empty() -> Self {
301 Self {
302 pubkeys: HashMap::new(),
303 default_iss: "".to_string(),
304 }
305 }
306
307 /// Loads public keys dynamically from a JWKS endpoint discovered from provider metadata.
308 ///
309 /// Fetches the JWKS, parses it, and builds validation rules for each key.
310 ///
311 /// * `validation` - Template validation rules to apply for each key.
312 /// * `provider_metadata` - OpenID Connect provider metadata (must include JWKS URI).
313 /// * `issuer_url` - The expected issuer URL.
314 pub async fn new(
315 validation: Validation,
316 provider_metadata: &CoreProviderMetadata,
317 issuer_url: String,
318 ) -> Result<Self, Box<dyn std::error::Error>> {
319 let jwks_uri = provider_metadata.jwks_uri().to_string();
320
321 let jwks_json = reqwest::get(jwks_uri).await?.text().await?;
322 let keys = parse_jwks(&issuer_url, &jwks_json, validation.clone())?;
323 Ok(Self {
324 pubkeys: keys,
325 default_iss: issuer_url,
326 })
327 }
328
329 fn default_validation(
330 url: &str,
331 audiance: &str,
332 algorithm: &str,
333 ) -> Result<Validation, Box<dyn std::error::Error>> {
334 let algo = Algorithm::from_str(&algorithm)?;
335 let mut validation = Validation::new(algo);
336 //validation.insecure_disable_signature_validation();
337 {
338 validation.leeway = 100; // Optionally, allow some leeway
339 validation.validate_exp = true;
340 validation.validate_aud = true;
341 validation.validate_nbf = true;
342 validation.aud = Some(hashset_from(vec![audiance.to_string()])); // The audience should match your client ID
343 validation.iss = Some(hashset_from(vec![url.to_string()])); // Validate the issuer
344 validation.algorithms = vec![algo];
345 };
346
347 Ok(validation)
348 }
349
350 /// Extends the validator by dynamically discovering and importing public keys (JWKS)
351 /// from the OpenID Connect (OIDC) discovery endpoint of the given issuer.
352 ///
353 /// This method performs the following steps:
354 /// 1. Initializes an HTTP client with redirect-following disabled for security reasons.
355 /// 2. Fetches the OpenID Connect provider metadata from the issuer's well-known discovery endpoint.
356 /// 3. Retrieves the JWKS (JSON Web Key Set) URI from the provider metadata.
357 /// 4. Downloads the JWKS document and parses the keys.
358 /// 5. Inserts the discovered keys into the validator's `pubkeys` map, associating them with the issuer.
359 ///
360 /// # Parameters
361 /// - `issuer_url`: The base URL of the OIDC issuer (e.g., `https://accounts.example.com`).
362 /// - `validation`: The validation params used for this endpoint (make sure iss, aud, alg are set correctly)
363 /// # Returns
364 /// - `Ok(())` if the keys were successfully fetched and added.
365 /// - `Err(Box<dyn std::error::Error>)` if any network, parsing, or validation step fails.
366 ///
367 /// # Errors
368 /// Returns an error if:
369 /// - The HTTP client could not be created.
370 /// - The issuer URL is invalid.
371 /// - The provider metadata discovery fails.
372 /// - The JWKS document cannot be fetched or parsed.
373 ///
374 /// # Security
375 /// - Redirects are explicitly disabled to prevent SSRF attacks when contacting the discovery endpoint.
376 ///
377 /// # Example
378 /// ```ignore
379 /// use rocket_oidc::client::Validator;
380 /// let mut validator = Validator::empty();
381 /// validator.extend_from_oidc("https://accounts.example.com").await?;
382 /// ```
383 pub async fn extend_from_oidc(
384 &mut self,
385 issuer_url: &str,
386 audiance: &str,
387 default_algorithm: &str,
388 ) -> Result<(), Box<dyn std::error::Error>> {
389 let http_client = match reqwest::ClientBuilder::new()
390 // Following redirects opens the client up to SSRF vulnerabilities.
391 .redirect(reqwest::redirect::Policy::none())
392 .build()
393 {
394 Ok(client) => client,
395 Err(e) => return Err(Box::new(e)),
396 };
397
398 let provider_metadata = match CoreProviderMetadata::discover_async(
399 IssuerUrl::new(issuer_url.to_string())?,
400 &http_client,
401 )
402 .await
403 {
404 Ok(provider_metadata) => provider_metadata,
405 Err(e) => return Err(Box::new(e)),
406 };
407 let validation = Self::default_validation(issuer_url, audiance, default_algorithm)?;
408
409 let jwks_uri = provider_metadata.jwks_uri().to_string();
410 let jwks_json = reqwest::get(jwks_uri).await?.text().await?;
411 let keys = parse_jwks(&issuer_url, &jwks_json, validation.clone())?;
412 for (key, value) in keys.into_iter() {
413 self.pubkeys.insert(key, value);
414 }
415 Ok(())
416 }
417
418 /// Extends the validator by parsing and adding public keys from a JWKS JSON document
419 /// associated with the given issuer.
420 ///
421 /// # Parameters
422 /// - `issuer_url`: The base URL of the OIDC issuer (e.g., `https://accounts.example.com`).
423 /// - `jwks_json`: The raw JWKS JSON string.
424 ///
425 /// # Returns
426 /// - `Ok(())` if the keys were successfully parsed and added.
427 /// - `Err(crate::Error)` if parsing fails.
428 ///
429 /// # Example
430 /// ```ignore
431 /// let mut validator = Validator::empty();
432 /// let jwks_json = std::fs::read_to_string("keys.json")?;
433 /// validator.extend_from_jwks("https://accounts.example.com", &jwks_json)?;
434 /// ```
435 pub fn extend_from_jwks(
436 &mut self,
437 issuer_url: &str,
438 jwks_json: &str,
439 validation: Validation,
440 ) -> Result<(), Box<dyn std::error::Error>> {
441 // Parse the keys, associating them with the issuer and current validation config
442 let keys = parse_jwks(issuer_url, jwks_json, validation.clone())?;
443
444 // Insert them into the validator's pubkeys map
445 for (key_id, endpoint) in keys {
446 self.pubkeys.insert(key_id, endpoint);
447 }
448
449 Ok(())
450 }
451
452 /// Inserts a new validation endpoint directly by its `KeyID`.
453 ///
454 /// Useful when you already constructed an `Endpoint` yourself.
455 pub fn insert_endpoint(&mut self, keyid: KeyID, endpoint: Endpoint) {
456 self.pubkeys.insert(keyid, endpoint);
457 }
458
459 /// Inserts a new public key and automatically builds its validation rules.
460 ///
461 /// * `url` - Issuer URL.
462 /// * `audiance` - Expected audience claim.
463 /// * `algorithm` - Signing algorithm.
464 /// * `public_key` - Decoding key.
465 pub fn insert_pubkey(
466 &mut self,
467 url: String,
468 audiance: String,
469 algorithm: String,
470 public_key: DecodingKey,
471 ) -> Result<(), Box<dyn std::error::Error>> {
472 let algo = Algorithm::from_str(&algorithm)?;
473 let validation = Self::default_validation(&url, &audiance, &algorithm)?;
474
475 let keyid = KeyID::new(&url, &algorithm);
476 self.pubkeys
477 .insert(keyid, Endpoint::new(validation, public_key));
478 Ok(())
479 }
480
481 /// Decodes and validates an access token for a specific issuer and algorithm.
482 ///
483 /// * `issuer` - Issuer URL.
484 /// * `algorithm` - Signing algorithm (e.g., "RS256").
485 /// * `access_token` - The JWT string to decode.
486 ///
487 /// Returns the token's claims if valid, or an error otherwise.
488 pub fn decode_with_iss_alg<
489 T: Serialize + Debug + DeserializeOwned + std::marker::Send + CoreClaims + Clone,
490 >(
491 &self,
492 issuer: &str,
493 algorithm: &str,
494 access_token: &str,
495 ) -> Result<TokenData<T>, crate::Error> {
496 let keyid = KeyID::new(issuer, algorithm);
497 if let Some(endpoint) = self.pubkeys.get(&keyid) {
498 #[cfg(debug_assertions)]
499 {
500 let mut emptyvalidation = Validation::new(Algorithm::from_str(algorithm)?);
501 emptyvalidation.validate_aud = false;
502 emptyvalidation.validate_exp = false;
503 emptyvalidation.validate_nbf = false;
504 match jsonwebtoken::decode::<serde_json::Value>(
505 access_token,
506 &endpoint.pubkey,
507 &emptyvalidation,
508 ) {
509 Ok(data) => {
510 eprintln!("DEBUG: Unvalidated token claims: {:#?}", data.claims);
511 }
512 Err(e) => {
513 eprintln!("DEBUG: Failed to decode unvalidated token: {:?}", e);
514 }
515 }
516 }
517 Ok(decode::<T>(
518 access_token,
519 &endpoint.pubkey,
520 &endpoint.validation,
521 )?)
522 } else {
523 Err(Error::PubKeyNotFound(keyid))
524 }
525 }
526
527 /// Decodes and validates an access token using the default issuer and a default algorithm ("RS256").
528 ///
529 /// ⚠️ **Deprecated:** May not be correct if you handle multiple issuers or algorithms.
530 #[deprecated]
531 pub fn decode<
532 T: Serialize + Debug + DeserializeOwned + std::marker::Send + CoreClaims + Clone,
533 >(
534 &self,
535 access_token: &str,
536 ) -> Result<TokenData<T>, crate::Error> {
537 self.decode_with_iss_alg::<T>(&self.default_iss, "RS256", access_token)
538 }
539}
540
541/// A high-level OpenID Connect (OIDC) client abstraction for performing common flows:
542/// - Discovering provider metadata
543/// - Exchanging authorization codes for tokens
544/// - Fetching user information
545/// - Performing token exchange
546///
547/// Internally, `OIDCClient` combines:
548/// - An OpenID Connect client (`OpenIDClient`)
549/// - A reqwest HTTP client (`reqwest::Client`)
550/// - Local configuration (`WorkingConfig`)
551///
552/// This design allows dynamic discovery from OIDC configuration,
553/// while keeping a ready-to-use validator for verifying ID tokens or access tokens.
554#[derive(Debug, Clone)]
555pub struct OIDCClient {
556 // The OpenID Connect client instance, created from discovered provider metadata.
557 pub client: OpenIDClient,
558
559 // The reqwest HTTP client used for token and userinfo requests.
560 reqwest_client: reqwest::Client,
561
562 // Local, working configuration values (e.g., client ID, secret, redirect URL, issuer).
563 config: WorkingConfig,
564}
565
566impl OIDCClient {
567 /// Creates a new `OIDCClient` by dynamically discovering the provider metadata
568 /// and preparing a `Validator` to verify tokens.
569 ///
570 /// This method:
571 /// - Builds a safe reqwest HTTP client (with redirect following disabled).
572 /// - Discovers the OpenID provider metadata from the issuer URL.
573 /// - Constructs default validation rules (audience, issuer, leeway).
574 /// - Loads the JWKS keys into a `Validator`.
575 /// - Initializes the OpenID Connect client with the discovered metadata.
576 ///
577 /// # Arguments
578 /// * `config` - High-level OIDC configuration.
579 ///
580 /// # Returns
581 /// A tuple of:
582 /// - `OIDCClient` (for performing login and userinfo flows)
583 /// - `Validator` (for verifying ID tokens or access tokens)
584 ///
585 /// # Errors
586 /// Returns an error if discovery fails, the JWKS endpoint cannot be fetched,
587 /// or if the HTTP client cannot be built.
588 pub async fn from_oidc_config(
589 config: &OIDCConfig,
590 ) -> Result<(Self, Validator), Box<dyn std::error::Error>> {
591 let config = WorkingConfig::from_oidc_config(config).await?;
592
593 let http_client = match reqwest::ClientBuilder::new()
594 // Following redirects opens the client up to SSRF vulnerabilities.
595 .redirect(reqwest::redirect::Policy::none())
596 .build()
597 {
598 Ok(client) => client,
599 Err(e) => return Err(Box::new(e)),
600 };
601
602 let provider_metadata =
603 match CoreProviderMetadata::discover_async(config.issuer_url.clone(), &http_client)
604 .await
605 {
606 Ok(provider_metadata) => provider_metadata,
607 Err(e) => return Err(Box::new(e)),
608 };
609
610 // Decode and verify the JWT
611 let mut validation = Validation::new(Algorithm::RS256);
612 //validation.insecure_disable_signature_validation();
613 {
614 validation.leeway = 100; // Optionally, allow some leeway
615 validation.validate_exp = true;
616 validation.validate_aud = true;
617 validation.validate_nbf = true;
618 validation.aud = Some(hashset_from(vec!["account".to_string()])); // The audience should match your client ID
619 validation.iss = Some(hashset_from(vec![config.issuer_url.to_string()])); // Validate the issuer
620 };
621
622 let validator = Validator::new(
623 validation,
624 &provider_metadata,
625 config.issuer_url.to_string(),
626 )
627 .await?;
628
629 // Set up the config for the GitLab OAuth2 process.
630 let client = CoreClient::from_provider_metadata(
631 provider_metadata,
632 config.client_id.clone(),
633 Some(config.client_secret.clone()),
634 )
635 // This example will be running its own server at localhost:8080.
636 // See below for the server implementation.
637 .set_redirect_uri(
638 RedirectUrl::new(join_url(&config.redirect, "/auth/callback/").unwrap())
639 .unwrap_or_else(|_err| {
640 unreachable!();
641 }),
642 );
643
644 Ok((
645 Self {
646 client,
647 config,
648 reqwest_client: http_client,
649 },
650 validator,
651 ))
652 }
653
654 /// Fetches user information from the provider's UserInfo endpoint.
655 ///
656 /// # Arguments
657 /// * `access_token` - The access token obtained after login.
658 /// * `subject` - Optionally, the subject (user ID) to query.
659 ///
660 /// # Returns
661 /// The claims returned by the UserInfo endpoint.
662 ///
663 /// # Errors
664 /// Returns an error if the request fails or the response is invalid.
665 pub async fn user_info(
666 &self,
667 access_token: AccessToken,
668 subject: Option<SubjectIdentifier>,
669 ) -> Result<
670 UserInfoClaims<AddClaims, PronounClaim>,
671 UserInfoError<openidconnect::HttpClientError<reqwest::Error>>,
672 > {
673 self.client
674 .user_info(
675 access_token, // AccessToken::new(access_token.value().to_string())
676 subject, //Some(SubjectIdentifier::new(data.claims.subject().to_string()))
677 )
678 .unwrap()
679 .request_async(&self.reqwest_client)
680 .await
681 }
682
683 /// Exchanges an authorization code (received after user login) for a token response.
684 ///
685 /// # Arguments
686 /// * `code` - The authorization code.
687 ///
688 /// # Returns
689 /// The token response, including access token and optionally ID token or refresh token.
690 ///
691 /// # Errors
692 /// Returns an error if the token request fails or the response is invalid.
693 pub async fn exchange_code(
694 &self,
695 code: AuthorizationCode,
696 ) -> Result<CoreTokenResponse, crate::Error> {
697 Ok(self
698 .client
699 .exchange_code(code)?
700 .request_async(&self.reqwest_client)
701 .await?)
702 }
703
704 /// Performs OAuth2 token exchange to obtain a token scoped for a different audience.
705 ///
706 /// # Arguments
707 /// * `subject_token` - The current access token or ID token.
708 /// * `audience` - The target audience for the exchanged token.
709 ///
710 /// # Returns
711 /// The token exchange response, containing the new token.
712 ///
713 /// # Errors
714 /// Returns a reqwest error if the request fails.
715 ///
716 /// # Note
717 /// I haven't tested this in a full flow.
718 pub async fn exchange_token_for_audience(
719 &self,
720 subject_token: &str,
721 audience: &str,
722 ) -> Result<TokenExchangeResponse, reqwest::Error> {
723 crate::token::perform_token_exchange(
724 self.client.token_uri().unwrap().as_str(),
725 self.config.client_id.as_str(),
726 self.config.client_secret.secret().as_str(),
727 subject_token,
728 audience,
729 )
730 .await
731 }
732
733 /// Like `from_oidc_config`, but allows the caller to provide a custom `Validation` template.
734 ///
735 /// This can be used to:
736 /// - Disable signature verification for testing.
737 /// - Adjust expiration leeway, audience, issuer, etc.
738 /// - Support different algorithms.
739 pub async fn from_oidc_config_with_validation(
740 config: &OIDCConfig,
741 custom_validation: Validation,
742 ) -> Result<(Self, Validator), Box<dyn std::error::Error>> {
743 let config = WorkingConfig::from_oidc_config(config).await?;
744 let http_client = reqwest::ClientBuilder::new()
745 .redirect(reqwest::redirect::Policy::none())
746 .build()?;
747
748 let provider_metadata =
749 CoreProviderMetadata::discover_async(config.issuer_url.clone(), &http_client).await?;
750
751 let validator = Validator::new(
752 custom_validation,
753 &provider_metadata,
754 config.issuer_url.to_string(),
755 )
756 .await?;
757
758 let client = CoreClient::from_provider_metadata(
759 provider_metadata,
760 config.client_id.clone(),
761 Some(config.client_secret.clone()),
762 )
763 .set_redirect_uri(
764 RedirectUrl::new(join_url(&config.redirect, "/auth/callback/").unwrap()).unwrap(),
765 );
766
767 Ok((
768 Self {
769 client,
770 config,
771 reqwest_client: http_client,
772 },
773 validator,
774 ))
775 }
776}
777
778/// this struct provides extra data stored in a cookie that's used to identify
779/// which issuer provided the json web token so it can be properly constructed.
780#[derive(Debug, Clone, Serialize, Deserialize)]
781pub struct IssuerData {
782 pub issuer: String,
783 pub algorithm: String,
784}