Skip to content

Commit 3f53259

Browse files
committed
Apply tls-roots cargo feature to reqwest
The tls-roots feature makes axum discover system TLS certificates through rustls-native-certs, but not reqwest. This PR changes that so that the corresponding feature on reqwest is activated as well, so e.g. oidc discovery requests behave the same as gRPC requests with regards to TLS certificates.
1 parent c349114 commit 3f53259

File tree

3 files changed

+87
-62
lines changed

3 files changed

+87
-62
lines changed

crates/zitadel/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ api-settings-v2 = ["api-common", "zitadel-gen/zitadel-settings-v2" ]
5555
api-user-v2 = ["api-common", "zitadel-gen/zitadel-user-v2" ]
5656

5757

58-
tls-roots = ["tonic/tls-roots"]
58+
tls-roots = ["tonic/tls-roots", "reqwest/rustls-tls-native-roots"]
5959
tls-webpki-roots = ["tonic/tls-webpki-roots"]
6060

6161

crates/zitadel/src/oidc/introspection/mod.rs

Lines changed: 86 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1+
use crate::credentials::{Application, ApplicationError};
2+
use crate::oidc::discovery::{discover, DiscoveryError};
13
use custom_error::custom_error;
4+
use jsonwebtoken::jwk::{AlgorithmParameters, JwkSet};
5+
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Header, TokenData, Validation};
26
use openidconnect::url::{ParseError, Url};
3-
use openidconnect::{
4-
core::CoreTokenType, ExtraTokenFields, StandardTokenIntrospectionResponse,
5-
};
7+
use openidconnect::{core::CoreTokenType, ExtraTokenFields, StandardTokenIntrospectionResponse};
68
use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION, CONTENT_TYPE};
9+
use reqwest::Client;
10+
use serde::de::DeserializeOwned;
711
use serde::{Deserialize, Serialize};
12+
use serde_json::Value as JsonValue;
813
use std::collections::HashMap;
914
use std::error::Error;
1015
use std::fmt::{Debug, Display};
11-
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Header, TokenData, Validation};
12-
use jsonwebtoken::jwk::{AlgorithmParameters, JwkSet};
1316
use std::str::FromStr;
14-
use reqwest::{Client};
15-
use serde::de::DeserializeOwned;
16-
use serde_json::Value as JsonValue;
17-
use crate::credentials::{Application, ApplicationError};
18-
use crate::oidc::discovery::{discover, DiscoveryError};
1917

2018
#[cfg(feature = "introspection_cache")]
2119
pub mod cache;
@@ -49,8 +47,7 @@ custom_error! {
4947
/// if requested by scope:
5048
/// - When scope contains `urn:zitadel:iam:user:resourceowner`, the fields prefixed with
5149
/// `resource_owner_` are set.
52-
/// - When scope contains `urn:zitadel:iam:user:metadata`, the metadata hashmap will be
53-
/// filled with the user metadata.
50+
/// - When scope contains `urn:zitadel:iam:user:metadata`, the metadata hashmap will be filled with the user metadata.
5451
/// - When scope contains `urn:zitadel:iam:org:projects:roles`, the project_roles hashmap will be
5552
/// filled with the project roles.
5653
/// - When using custom claims through Zitadel Actions, the custom_claims hashmap will be filled with
@@ -140,7 +137,7 @@ pub struct ZitadelIntrospectionExtraTokenFields {
140137
#[serde(rename = "urn:zitadel:iam:user:metadata")]
141138
pub metadata: Option<HashMap<String, String>>,
142139
#[serde(flatten)]
143-
custom_claims: Option<HashMap<String, JsonValue>>
140+
custom_claims: Option<HashMap<String, JsonValue>>,
144141
}
145142

146143
impl ExtraTokenFields for ZitadelIntrospectionExtraTokenFields {}
@@ -269,17 +266,22 @@ pub async fn introspect(
269266
authentication: &AuthorityAuthentication,
270267
token: &str,
271268
) -> Result<ZitadelIntrospectionResponse, IntrospectionError> {
272-
let async_http_client = reqwest::ClientBuilder::new().redirect(reqwest::redirect::Policy::none()).build()?;
269+
let async_http_client = reqwest::ClientBuilder::new()
270+
.redirect(reqwest::redirect::Policy::none())
271+
.build()?;
273272

274-
let url= Url::parse(introspection_uri)
275-
.map_err(|source| IntrospectionError::ParseUrl { source })?;
273+
let url =
274+
Url::parse(introspection_uri).map_err(|source| IntrospectionError::ParseUrl { source })?;
276275
let response = async_http_client
277276
.post(url)
278277
.headers(headers(authentication))
279278
.body(payload(authority, authentication, token)?)
280279
.send()
281280
.await
282-
.map_err(|source| IntrospectionError::RequestFailed {origin: "The introspection".to_string(), source })?;
281+
.map_err(|source| IntrospectionError::RequestFailed {
282+
origin: "The introspection".to_string(),
283+
source,
284+
})?;
283285

284286
if !response.status().is_success() {
285287
let status = response.status();
@@ -303,12 +305,12 @@ struct ZitadelResponseError {
303305
body: String,
304306
}
305307
impl ZitadelResponseError {
306-
fn new(status_code: reqwest::StatusCode, body: &[u8]) -> Self {
307-
Self {
308-
status_code: status_code.to_string(),
309-
body: String::from_utf8_lossy(body).to_string(),
310-
}
308+
fn new(status_code: reqwest::StatusCode, body: &[u8]) -> Self {
309+
Self {
310+
status_code: status_code.to_string(),
311+
body: String::from_utf8_lossy(body).to_string(),
311312
}
313+
}
312314
}
313315

314316
impl Display for ZitadelResponseError {
@@ -335,32 +337,35 @@ fn decode_metadata(response: &mut ZitadelIntrospectionResponse) -> Result<(), In
335337
Ok(())
336338
}
337339

338-
339340
pub async fn fetch_jwks(idm_url: &str) -> Result<JwkSet, IntrospectionError> {
340341
let client: Client = Client::new();
341-
let openid_config = discover(idm_url).await.map_err(|err| {
342-
IntrospectionError::DiscoveryError { source: err }
343-
})?;
342+
let openid_config = discover(idm_url)
343+
.await
344+
.map_err(|err| IntrospectionError::DiscoveryError { source: err })?;
344345
let jwks_url = openid_config.jwks_uri().url().as_ref();
345-
let response = client
346-
.get(jwks_url)
347-
.send()
348-
.await?;
349-
let jwks_keys: JwkSet = response.json::<JwkSet>().await.map_err(|err| IntrospectionError::RequestFailed {origin: "Could not fetch jwks keys because ".to_string(), source: err })?;
346+
let response = client.get(jwks_url).send().await?;
347+
let jwks_keys: JwkSet =
348+
response
349+
.json::<JwkSet>()
350+
.await
351+
.map_err(|err| IntrospectionError::RequestFailed {
352+
origin: "Could not fetch jwks keys because ".to_string(),
353+
source: err,
354+
})?;
350355
Ok(jwks_keys)
351356
}
352357

353-
354-
pub async fn local_jwt_validation<U>(issuers: &[&str],
355-
audiences: &[&str],
356-
jwks_keys: JwkSet,
357-
token: &str, ) -> Result<U, IntrospectionError>
358-
358+
pub async fn local_jwt_validation<U>(
359+
issuers: &[&str],
360+
audiences: &[&str],
361+
jwks_keys: JwkSet,
362+
token: &str,
363+
) -> Result<U, IntrospectionError>
359364
where
360365
U: DeserializeOwned,
361366
{
362-
363-
let unverified_token_header: Header = decode_header(token).map_err(|source| IntrospectionError::JsonWebTokenErrors { source })?;
367+
let unverified_token_header: Header =
368+
decode_header(token).map_err(|source| IntrospectionError::JsonWebTokenErrors { source })?;
364369
let user_kid = match unverified_token_header.kid {
365370
Some(k) => k,
366371
None => return Err(IntrospectionError::MissingJwksKey),
@@ -369,16 +374,21 @@ where
369374
match &j.algorithm {
370375
AlgorithmParameters::RSA(rsa) => {
371376
let decoding_key = DecodingKey::from_rsa_components(&rsa.n, &rsa.e)?;
372-
let algorithm_key = j.common.key_algorithm.ok_or(IntrospectionError::JWTUnsupportedAlgorithm)?;
377+
let algorithm_key = j
378+
.common
379+
.key_algorithm
380+
.ok_or(IntrospectionError::JWTUnsupportedAlgorithm)?;
373381
let algorithm_str = format!("{}", algorithm_key);
374-
let algorithm = Algorithm::from_str(&algorithm_str).map_err(|source| IntrospectionError::JsonWebTokenErrors { source })?;
382+
let algorithm = Algorithm::from_str(&algorithm_str)
383+
.map_err(|source| IntrospectionError::JsonWebTokenErrors { source })?;
375384
let mut validation = Validation::new(algorithm);
376385
validation.set_audience(audiences);
377386
validation.leeway = 5;
378387
validation.set_issuer(issuers);
379388
validation.validate_exp = true;
380389

381-
let decoded_token: TokenData<U> = decode::<U>(token, &decoding_key, &validation).map_err(|source| IntrospectionError::JsonWebTokenErrors { source })?;
390+
let decoded_token: TokenData<U> = decode::<U>(token, &decoding_key, &validation)
391+
.map_err(|source| IntrospectionError::JsonWebTokenErrors { source })?;
382392
Ok(decoded_token.claims)
383393
}
384394
_ => unreachable!("Not yet Implemented or supported by Zitadel"),
@@ -388,15 +398,14 @@ where
388398
}
389399
}
390400

391-
392401
#[cfg(test)]
393402
mod tests {
394403
#![allow(clippy::all)]
395404

405+
use super::*;
406+
use crate::credentials::{AuthenticationOptions, ServiceAccount};
396407
use crate::oidc::discovery::discover;
397408
use openidconnect::TokenIntrospectionResponse;
398-
use crate::credentials::{AuthenticationOptions, ServiceAccount};
399-
use super::*;
400409

401410
const ZITADEL_URL: &str = "https://zitadel-libraries-l8boqa.zitadel.cloud";
402411
const ZITADEL_URL_ALTER: &str = "https://ferris-hk3otq.us1.zitadel.cloud";
@@ -413,7 +422,7 @@ mod tests {
413422
const PERSONAL_ACCESS_TOKEN: &str =
414423
"dEnGhIFs3VnqcQU5D2zRSeiarB1nwH6goIKY0J8MWZbsnWcTuu1C59lW9DgCq1y096GYdXA";
415424

416-
const PERSONAL_ACCESS_TOKEN_ALTER : &str =
425+
const PERSONAL_ACCESS_TOKEN_ALTER: &str =
417426
"KyX1Pw1bVfYFSE0g6s3Io12I4sC-feEtkaShWstZJ0h34JHfE29q4oIOJFF0PZlfMDvaCvk";
418427

419428
#[derive(Debug, serde::Deserialize, serde::Serialize)]
@@ -437,18 +446,18 @@ mod tests {
437446
pub taste: Option<String>,
438447
#[serde(rename = "year")]
439448
pub anum: Option<i32>,
440-
}
449+
}
441450

442-
pub trait ExtIntrospectedUser {
451+
pub trait ExtIntrospectedUser {
443452
fn custom_claims(&self) -> Result<CustomClaims, serde_json::Error>;
444-
}
445-
impl ExtIntrospectedUser for ZitadelIntrospectionResponse {
446-
fn custom_claims(&self) -> Result<CustomClaims, serde_json::Error> {
453+
}
454+
impl ExtIntrospectedUser for ZitadelIntrospectionResponse {
455+
fn custom_claims(&self) -> Result<CustomClaims, serde_json::Error> {
447456
let as_value = serde_json::to_value(self)?;
448-
let custom_claims: CustomClaims = serde_json::from_value(as_value)?;
457+
let custom_claims: CustomClaims = serde_json::from_value(as_value)?;
449458
Ok(custom_claims)
450459
}
451-
}
460+
}
452461

453462
#[tokio::test]
454463
async fn introspect_fails_with_invalid_url() {
@@ -536,13 +545,30 @@ mod tests {
536545
//
537546

538547
let sa = ServiceAccount::load_from_json(SERVICE_ACCOUNT).unwrap();
539-
let access_token = sa.authenticate_with_options(ZITADEL_URL_ALTER, &AuthenticationOptions {
540-
scopes: vec!["profile".to_string(), "email".to_string(), "urn:zitadel:iam:user:resourceowner".to_string()],
541-
..Default::default()
542-
}).await.unwrap();
548+
let access_token = sa
549+
.authenticate_with_options(
550+
ZITADEL_URL_ALTER,
551+
&AuthenticationOptions {
552+
scopes: vec![
553+
"profile".to_string(),
554+
"email".to_string(),
555+
"urn:zitadel:iam:user:resourceowner".to_string(),
556+
],
557+
..Default::default()
558+
},
559+
)
560+
.await
561+
.unwrap();
543562
// move fetch_jwks after login has jwks can be purged after 30 hours of no login
544563
let jwks: JwkSet = fetch_jwks(ZITADEL_URL_ALTER).await.unwrap();
545-
let result: CustomClaims = local_jwt_validation::<CustomClaims>(&ZITADEL_ISSUERS, &ZITADEL_AUDIENCES, jwks, &access_token).await.unwrap();
564+
let result: CustomClaims = local_jwt_validation::<CustomClaims>(
565+
&ZITADEL_ISSUERS,
566+
&ZITADEL_AUDIENCES,
567+
jwks,
568+
&access_token,
569+
)
570+
.await
571+
.unwrap();
546572
assert_eq!(result.taste.unwrap(), "funk");
547573
assert_eq!(result.anum.unwrap(), 2025);
548574
}
@@ -565,8 +591,8 @@ mod tests {
565591
},
566592
PERSONAL_ACCESS_TOKEN_ALTER,
567593
)
568-
.await
569-
.unwrap();
594+
.await
595+
.unwrap();
570596

571597
let custom_claims = result.custom_claims().unwrap();
572598

crates/zitadel/src/rocket/introspection/guard.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ use rocket_okapi::{
2020
#[cfg(feature = "rocket_okapi")]
2121
use schemars::schema::{InstanceType, ObjectValidation, Schema, SchemaObject};
2222
#[cfg(feature = "rocket_okapi")]
23-
2423
use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse};
2524
use crate::rocket::introspection::IntrospectionConfig;
2625

0 commit comments

Comments
 (0)