1+ use crate :: credentials:: { Application , ApplicationError } ;
2+ use crate :: oidc:: discovery:: { discover, DiscoveryError } ;
13use custom_error:: custom_error;
4+ use jsonwebtoken:: jwk:: { AlgorithmParameters , JwkSet } ;
5+ use jsonwebtoken:: { decode, decode_header, Algorithm , DecodingKey , Header , TokenData , Validation } ;
26use openidconnect:: url:: { ParseError , Url } ;
3- use openidconnect:: {
4- core:: CoreTokenType , ExtraTokenFields , StandardTokenIntrospectionResponse ,
5- } ;
7+ use openidconnect:: { core:: CoreTokenType , ExtraTokenFields , StandardTokenIntrospectionResponse } ;
68use reqwest:: header:: { HeaderMap , ACCEPT , AUTHORIZATION , CONTENT_TYPE } ;
9+ use reqwest:: Client ;
10+ use serde:: de:: DeserializeOwned ;
711use serde:: { Deserialize , Serialize } ;
12+ use serde_json:: Value as JsonValue ;
813use std:: collections:: HashMap ;
914use std:: error:: Error ;
1015use std:: fmt:: { Debug , Display } ;
11- use jsonwebtoken:: { decode, decode_header, Algorithm , DecodingKey , Header , TokenData , Validation } ;
12- use jsonwebtoken:: jwk:: { AlgorithmParameters , JwkSet } ;
1316use 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" ) ]
2119pub 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
146143impl ExtraTokenFields for ZitadelIntrospectionExtraTokenFields { }
@@ -188,7 +185,7 @@ fn headers(auth: &AuthorityAuthentication) -> HeaderMap {
188185 AUTHORIZATION ,
189186 format ! (
190187 "Basic {}" ,
191- base64:: encode( & format!( "{}:{}" , client_id , client_secret ) )
188+ base64:: encode( & format!( "{client_id }:{client_secret}" ) )
192189 )
193190 . parse ( )
194191 . unwrap ( ) ,
@@ -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}
305307impl 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
314316impl Display for ZitadelResponseError {
@@ -335,32 +337,35 @@ fn decode_metadata(response: &mut ZitadelIntrospectionResponse) -> Result<(), In
335337 Ok ( ( ) )
336338}
337339
338-
339340pub 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 >
359364where
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 ) ?;
373- let algorithm_str = format ! ( "{}" , algorithm_key) ;
374- let algorithm = Algorithm :: from_str ( & algorithm_str) . map_err ( |source| IntrospectionError :: JsonWebTokenErrors { source } ) ?;
377+ let algorithm_key = j
378+ . common
379+ . key_algorithm
380+ . ok_or ( IntrospectionError :: JWTUnsupportedAlgorithm ) ?;
381+ let algorithm_str = format ! ( "{algorithm_key}" ) ;
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) ]
393402mod 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
0 commit comments