1
+ use crate :: credentials:: { Application , ApplicationError } ;
2
+ use crate :: oidc:: discovery:: { discover, DiscoveryError } ;
1
3
use custom_error:: custom_error;
4
+ use jsonwebtoken:: jwk:: { AlgorithmParameters , JwkSet } ;
5
+ use jsonwebtoken:: { decode, decode_header, Algorithm , DecodingKey , Header , TokenData , Validation } ;
2
6
use openidconnect:: url:: { ParseError , Url } ;
3
- use openidconnect:: {
4
- core:: CoreTokenType , ExtraTokenFields , StandardTokenIntrospectionResponse ,
5
- } ;
7
+ use openidconnect:: { core:: CoreTokenType , ExtraTokenFields , StandardTokenIntrospectionResponse } ;
6
8
use reqwest:: header:: { HeaderMap , ACCEPT , AUTHORIZATION , CONTENT_TYPE } ;
9
+ use reqwest:: Client ;
10
+ use serde:: de:: DeserializeOwned ;
7
11
use serde:: { Deserialize , Serialize } ;
12
+ use serde_json:: Value as JsonValue ;
8
13
use std:: collections:: HashMap ;
9
14
use std:: error:: Error ;
10
15
use std:: fmt:: { Debug , Display } ;
11
- use jsonwebtoken:: { decode, decode_header, Algorithm , DecodingKey , Header , TokenData , Validation } ;
12
- use jsonwebtoken:: jwk:: { AlgorithmParameters , JwkSet } ;
13
16
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 } ;
19
17
20
18
#[ cfg( feature = "introspection_cache" ) ]
21
19
pub mod cache;
@@ -49,8 +47,7 @@ custom_error! {
49
47
/// if requested by scope:
50
48
/// - When scope contains `urn:zitadel:iam:user:resourceowner`, the fields prefixed with
51
49
/// `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.
54
51
/// - When scope contains `urn:zitadel:iam:org:projects:roles`, the project_roles hashmap will be
55
52
/// filled with the project roles.
56
53
/// - When using custom claims through Zitadel Actions, the custom_claims hashmap will be filled with
@@ -140,7 +137,7 @@ pub struct ZitadelIntrospectionExtraTokenFields {
140
137
#[ serde( rename = "urn:zitadel:iam:user:metadata" ) ]
141
138
pub metadata : Option < HashMap < String , String > > ,
142
139
#[ serde( flatten) ]
143
- custom_claims : Option < HashMap < String , JsonValue > >
140
+ custom_claims : Option < HashMap < String , JsonValue > > ,
144
141
}
145
142
146
143
impl ExtraTokenFields for ZitadelIntrospectionExtraTokenFields { }
@@ -269,17 +266,22 @@ pub async fn introspect(
269
266
authentication : & AuthorityAuthentication ,
270
267
token : & str ,
271
268
) -> 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 ( ) ?;
273
272
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 } ) ?;
276
275
let response = async_http_client
277
276
. post ( url)
278
277
. headers ( headers ( authentication) )
279
278
. body ( payload ( authority, authentication, token) ?)
280
279
. send ( )
281
280
. 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
+ } ) ?;
283
285
284
286
if !response. status ( ) . is_success ( ) {
285
287
let status = response. status ( ) ;
@@ -303,12 +305,12 @@ struct ZitadelResponseError {
303
305
body : String ,
304
306
}
305
307
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 ( ) ,
311
312
}
313
+ }
312
314
}
313
315
314
316
impl Display for ZitadelResponseError {
@@ -335,32 +337,35 @@ fn decode_metadata(response: &mut ZitadelIntrospectionResponse) -> Result<(), In
335
337
Ok ( ( ) )
336
338
}
337
339
338
-
339
340
pub async fn fetch_jwks ( idm_url : & str ) -> Result < JwkSet , IntrospectionError > {
340
341
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 } ) ?;
344
345
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
+ } ) ?;
350
355
Ok ( jwks_keys)
351
356
}
352
357
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 >
359
364
where
360
365
U : DeserializeOwned ,
361
366
{
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 } ) ?;
364
369
let user_kid = match unverified_token_header. kid {
365
370
Some ( k) => k,
366
371
None => return Err ( IntrospectionError :: MissingJwksKey ) ,
@@ -369,16 +374,21 @@ where
369
374
match & j. algorithm {
370
375
AlgorithmParameters :: RSA ( rsa) => {
371
376
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 ) ?;
373
381
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 } ) ?;
375
384
let mut validation = Validation :: new ( algorithm) ;
376
385
validation. set_audience ( audiences) ;
377
386
validation. leeway = 5 ;
378
387
validation. set_issuer ( issuers) ;
379
388
validation. validate_exp = true ;
380
389
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 } ) ?;
382
392
Ok ( decoded_token. claims )
383
393
}
384
394
_ => unreachable ! ( "Not yet Implemented or supported by Zitadel" ) ,
@@ -388,15 +398,14 @@ where
388
398
}
389
399
}
390
400
391
-
392
401
#[ cfg( test) ]
393
402
mod tests {
394
403
#![ allow( clippy:: all) ]
395
404
405
+ use super :: * ;
406
+ use crate :: credentials:: { AuthenticationOptions , ServiceAccount } ;
396
407
use crate :: oidc:: discovery:: discover;
397
408
use openidconnect:: TokenIntrospectionResponse ;
398
- use crate :: credentials:: { AuthenticationOptions , ServiceAccount } ;
399
- use super :: * ;
400
409
401
410
const ZITADEL_URL : & str = "https://zitadel-libraries-l8boqa.zitadel.cloud" ;
402
411
const ZITADEL_URL_ALTER : & str = "https://ferris-hk3otq.us1.zitadel.cloud" ;
@@ -413,7 +422,7 @@ mod tests {
413
422
const PERSONAL_ACCESS_TOKEN : & str =
414
423
"dEnGhIFs3VnqcQU5D2zRSeiarB1nwH6goIKY0J8MWZbsnWcTuu1C59lW9DgCq1y096GYdXA" ;
415
424
416
- const PERSONAL_ACCESS_TOKEN_ALTER : & str =
425
+ const PERSONAL_ACCESS_TOKEN_ALTER : & str =
417
426
"KyX1Pw1bVfYFSE0g6s3Io12I4sC-feEtkaShWstZJ0h34JHfE29q4oIOJFF0PZlfMDvaCvk" ;
418
427
419
428
#[ derive( Debug , serde:: Deserialize , serde:: Serialize ) ]
@@ -437,18 +446,18 @@ mod tests {
437
446
pub taste : Option < String > ,
438
447
#[ serde( rename = "year" ) ]
439
448
pub anum : Option < i32 > ,
440
- }
449
+ }
441
450
442
- pub trait ExtIntrospectedUser {
451
+ pub trait ExtIntrospectedUser {
443
452
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 > {
447
456
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) ?;
449
458
Ok ( custom_claims)
450
459
}
451
- }
460
+ }
452
461
453
462
#[ tokio:: test]
454
463
async fn introspect_fails_with_invalid_url ( ) {
@@ -536,13 +545,30 @@ mod tests {
536
545
//
537
546
538
547
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 ( ) ;
543
562
// move fetch_jwks after login has jwks can be purged after 30 hours of no login
544
563
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 ( ) ;
546
572
assert_eq ! ( result. taste. unwrap( ) , "funk" ) ;
547
573
assert_eq ! ( result. anum. unwrap( ) , 2025 ) ;
548
574
}
@@ -565,8 +591,8 @@ mod tests {
565
591
} ,
566
592
PERSONAL_ACCESS_TOKEN_ALTER ,
567
593
)
568
- . await
569
- . unwrap ( ) ;
594
+ . await
595
+ . unwrap ( ) ;
570
596
571
597
let custom_claims = result. custom_claims ( ) . unwrap ( ) ;
572
598
0 commit comments