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;
@@ -50,11 +48,11 @@ custom_error! {
5048/// - When scope contains `urn:zitadel:iam:user:resourceowner`, the fields prefixed with
5149/// `resource_owner_` are set.
5250/// - When scope contains `urn:zitadel:iam:user:metadata`, the metadata hashmap will be
53- /// filled with the user metadata.
54- /// - When scope contains `urn:zitadel:iam:org:projects:roles`, the project_roles hashmap will be
55- /// filled with the project roles.
56- /// - When using custom claims through Zitadel Actions, the custom_claims hashmap will be filled with
57- /// the custom claims. [custom claims](https://zitadel.com/docs/apis/openidoauth/claims#custom-claims)
51+ /// filled with the user metadata.
52+ /// - When scope contains `urn:zitadel:iam:org:projects:roles`, the project_roles hashmap will be
53+ /// filled with the project roles.
54+ /// - When using custom claims through Zitadel Actions, the custom_claims hashmap will be filled with
55+ /// the custom claims. [custom claims](https://zitadel.com/docs/apis/openidoauth/claims#custom-claims)
5856///
5957/// It can be used as a basis for further customized authorization checks, for example:
6058/// ```
@@ -140,7 +138,7 @@ pub struct ZitadelIntrospectionExtraTokenFields {
140138 #[ serde( rename = "urn:zitadel:iam:user:metadata" ) ]
141139 pub metadata : Option < HashMap < String , String > > ,
142140 #[ serde( flatten) ]
143- custom_claims : Option < HashMap < String , JsonValue > >
141+ custom_claims : Option < HashMap < String , JsonValue > > ,
144142}
145143
146144impl ExtraTokenFields for ZitadelIntrospectionExtraTokenFields { }
@@ -269,17 +267,22 @@ pub async fn introspect(
269267 authentication : & AuthorityAuthentication ,
270268 token : & str ,
271269) -> Result < ZitadelIntrospectionResponse , IntrospectionError > {
272- let async_http_client = reqwest:: ClientBuilder :: new ( ) . redirect ( reqwest:: redirect:: Policy :: none ( ) ) . build ( ) ?;
270+ let async_http_client = reqwest:: ClientBuilder :: new ( )
271+ . redirect ( reqwest:: redirect:: Policy :: none ( ) )
272+ . build ( ) ?;
273273
274- let url= Url :: parse ( introspection_uri )
275- . map_err ( |source| IntrospectionError :: ParseUrl { source } ) ?;
274+ let url =
275+ Url :: parse ( introspection_uri ) . map_err ( |source| IntrospectionError :: ParseUrl { source } ) ?;
276276 let response = async_http_client
277277 . post ( url)
278278 . headers ( headers ( authentication) )
279279 . body ( payload ( authority, authentication, token) ?)
280280 . send ( )
281281 . await
282- . map_err ( |source| IntrospectionError :: RequestFailed { origin : "The introspection" . to_string ( ) , source } ) ?;
282+ . map_err ( |source| IntrospectionError :: RequestFailed {
283+ origin : "The introspection" . to_string ( ) ,
284+ source,
285+ } ) ?;
283286
284287 if !response. status ( ) . is_success ( ) {
285288 let status = response. status ( ) ;
@@ -303,12 +306,12 @@ struct ZitadelResponseError {
303306 body : String ,
304307}
305308impl 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- }
309+ fn new ( status_code : reqwest:: StatusCode , body : & [ u8 ] ) -> Self {
310+ Self {
311+ status_code : status_code. to_string ( ) ,
312+ body : String :: from_utf8_lossy ( body) . to_string ( ) ,
311313 }
314+ }
312315}
313316
314317impl Display for ZitadelResponseError {
@@ -335,32 +338,35 @@ fn decode_metadata(response: &mut ZitadelIntrospectionResponse) -> Result<(), In
335338 Ok ( ( ) )
336339}
337340
338-
339341pub async fn fetch_jwks ( idm_url : & str ) -> Result < JwkSet , IntrospectionError > {
340342 let client: Client = Client :: new ( ) ;
341- let openid_config = discover ( idm_url) . await . map_err ( |err| {
342- IntrospectionError :: DiscoveryError { source : err }
343- } ) ?;
343+ let openid_config = discover ( idm_url)
344+ . await
345+ . map_err ( |err| IntrospectionError :: DiscoveryError { source : err } ) ?;
344346 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 } ) ?;
347+ let response = client. get ( jwks_url) . send ( ) . await ?;
348+ let jwks_keys: JwkSet =
349+ response
350+ . json :: < JwkSet > ( )
351+ . await
352+ . map_err ( |err| IntrospectionError :: RequestFailed {
353+ origin : "Could not fetch jwks keys because " . to_string ( ) ,
354+ source : err,
355+ } ) ?;
350356 Ok ( jwks_keys)
351357}
352358
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-
359+ pub async fn local_jwt_validation < U > (
360+ issuers : & [ & str ] ,
361+ audiences : & [ & str ] ,
362+ jwks_keys : JwkSet ,
363+ token : & str ,
364+ ) -> Result < U , IntrospectionError >
359365where
360366 U : DeserializeOwned ,
361367{
362-
363- let unverified_token_header : Header = decode_header ( token) . map_err ( |source| IntrospectionError :: JsonWebTokenErrors { source } ) ?;
368+ let unverified_token_header : Header =
369+ decode_header ( token) . map_err ( |source| IntrospectionError :: JsonWebTokenErrors { source } ) ?;
364370 let user_kid = match unverified_token_header. kid {
365371 Some ( k) => k,
366372 None => return Err ( IntrospectionError :: MissingJwksKey ) ,
@@ -369,16 +375,21 @@ where
369375 match & j. algorithm {
370376 AlgorithmParameters :: RSA ( rsa) => {
371377 let decoding_key = DecodingKey :: from_rsa_components ( & rsa. n , & rsa. e ) ?;
372- let algorithm_key = j. common . key_algorithm . ok_or ( IntrospectionError :: JWTUnsupportedAlgorithm ) ?;
378+ let algorithm_key = j
379+ . common
380+ . key_algorithm
381+ . ok_or ( IntrospectionError :: JWTUnsupportedAlgorithm ) ?;
373382 let algorithm_str = format ! ( "{}" , algorithm_key) ;
374- let algorithm = Algorithm :: from_str ( & algorithm_str) . map_err ( |source| IntrospectionError :: JsonWebTokenErrors { source } ) ?;
383+ let algorithm = Algorithm :: from_str ( & algorithm_str)
384+ . map_err ( |source| IntrospectionError :: JsonWebTokenErrors { source } ) ?;
375385 let mut validation = Validation :: new ( algorithm) ;
376386 validation. set_audience ( audiences) ;
377387 validation. leeway = 5 ;
378388 validation. set_issuer ( issuers) ;
379389 validation. validate_exp = true ;
380390
381- let decoded_token: TokenData < U > = decode :: < U > ( token, & decoding_key, & validation) . map_err ( |source| IntrospectionError :: JsonWebTokenErrors { source } ) ?;
391+ let decoded_token: TokenData < U > = decode :: < U > ( token, & decoding_key, & validation)
392+ . map_err ( |source| IntrospectionError :: JsonWebTokenErrors { source } ) ?;
382393 Ok ( decoded_token. claims )
383394 }
384395 _ => unreachable ! ( "Not yet Implemented or supported by Zitadel" ) ,
@@ -388,15 +399,14 @@ where
388399 }
389400}
390401
391-
392402#[ cfg( test) ]
393403mod tests {
394404 #![ allow( clippy:: all) ]
395405
406+ use super :: * ;
407+ use crate :: credentials:: { AuthenticationOptions , ServiceAccount } ;
396408 use crate :: oidc:: discovery:: discover;
397409 use openidconnect:: TokenIntrospectionResponse ;
398- use crate :: credentials:: { AuthenticationOptions , ServiceAccount } ;
399- use super :: * ;
400410
401411 const ZITADEL_URL : & str = "https://zitadel-libraries-l8boqa.zitadel.cloud" ;
402412 const ZITADEL_URL_ALTER : & str = "https://ferris-hk3otq.us1.zitadel.cloud" ;
@@ -413,7 +423,7 @@ mod tests {
413423 const PERSONAL_ACCESS_TOKEN : & str =
414424 "dEnGhIFs3VnqcQU5D2zRSeiarB1nwH6goIKY0J8MWZbsnWcTuu1C59lW9DgCq1y096GYdXA" ;
415425
416- const PERSONAL_ACCESS_TOKEN_ALTER : & str =
426+ const PERSONAL_ACCESS_TOKEN_ALTER : & str =
417427 "KyX1Pw1bVfYFSE0g6s3Io12I4sC-feEtkaShWstZJ0h34JHfE29q4oIOJFF0PZlfMDvaCvk" ;
418428
419429 #[ derive( Debug , serde:: Deserialize , serde:: Serialize ) ]
@@ -437,18 +447,18 @@ mod tests {
437447 pub taste : Option < String > ,
438448 #[ serde( rename = "year" ) ]
439449 pub anum : Option < i32 > ,
440- }
450+ }
441451
442- pub trait ExtIntrospectedUser {
452+ pub trait ExtIntrospectedUser {
443453 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 > {
454+ }
455+ impl ExtIntrospectedUser for ZitadelIntrospectionResponse {
456+ fn custom_claims ( & self ) -> Result < CustomClaims , serde_json:: Error > {
447457 let as_value = serde_json:: to_value ( self ) ?;
448- let custom_claims: CustomClaims = serde_json:: from_value ( as_value) ?;
458+ let custom_claims: CustomClaims = serde_json:: from_value ( as_value) ?;
449459 Ok ( custom_claims)
450460 }
451- }
461+ }
452462
453463 #[ tokio:: test]
454464 async fn introspect_fails_with_invalid_url ( ) {
@@ -536,13 +546,30 @@ mod tests {
536546 //
537547
538548 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 ( ) ;
549+ let access_token = sa
550+ . authenticate_with_options (
551+ ZITADEL_URL_ALTER ,
552+ & AuthenticationOptions {
553+ scopes : vec ! [
554+ "profile" . to_string( ) ,
555+ "email" . to_string( ) ,
556+ "urn:zitadel:iam:user:resourceowner" . to_string( ) ,
557+ ] ,
558+ ..Default :: default ( )
559+ } ,
560+ )
561+ . await
562+ . unwrap ( ) ;
543563 // move fetch_jwks after login has jwks can be purged after 30 hours of no login
544564 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 ( ) ;
565+ let result: CustomClaims = local_jwt_validation :: < CustomClaims > (
566+ & ZITADEL_ISSUERS ,
567+ & ZITADEL_AUDIENCES ,
568+ jwks,
569+ & access_token,
570+ )
571+ . await
572+ . unwrap ( ) ;
546573 assert_eq ! ( result. taste. unwrap( ) , "funk" ) ;
547574 assert_eq ! ( result. anum. unwrap( ) , 2025 ) ;
548575 }
@@ -565,8 +592,8 @@ mod tests {
565592 } ,
566593 PERSONAL_ACCESS_TOKEN_ALTER ,
567594 )
568- . await
569- . unwrap ( ) ;
595+ . await
596+ . unwrap ( ) ;
570597
571598 let custom_claims = result. custom_claims ( ) . unwrap ( ) ;
572599
0 commit comments