11use std:: collections:: HashMap ;
2- use std:: fmt:: Debug ;
32
43use crate :: axum:: introspection:: IntrospectionState ;
54use axum:: http:: StatusCode ;
@@ -13,10 +12,9 @@ use axum_extra::headers::authorization::Bearer;
1312use axum_extra:: headers:: Authorization ;
1413use axum_extra:: TypedHeader ;
1514use custom_error:: custom_error;
16- use openidconnect:: TokenIntrospectionResponse ;
1715use serde_json:: json;
1816
19- use crate :: oidc:: introspection:: { introspect , IntrospectionError , ZitadelIntrospectionResponse } ;
17+ use crate :: oidc:: introspection:: { claims :: ZitadelClaims , introspect , IntrospectionError } ;
2018
2119custom_error ! {
2220 /// Error type for guard related errors.
@@ -54,61 +52,31 @@ impl IntoResponse for IntrospectionGuardError {
5452 }
5553}
5654
57- /// Struct for the extracted user. The extracted user will always be valid, when fetched in a
58- /// request function arguments. If not the api will return with an appropriate error.
55+ /// Type alias for the extracted user.
56+ ///
57+ /// The extracted user will always be valid when fetched in request function arguments.
58+ /// If not, the API will return with an appropriate error.
5959///
60- /// It can be used as a basis for further customized authorization checks with a custom extractor
61- /// or an extension trait.
60+ /// # Example
6261///
6362/// ```
6463/// use axum::http::StatusCode;
6564/// use axum::response::IntoResponse;
6665/// use zitadel::axum::introspection::IntrospectedUser;
6766///
68- /// enum Role {
69- /// Admin,
70- /// Client
71- /// }
72- ///
7367/// async fn my_handler(user: IntrospectedUser) -> impl IntoResponse {
74- /// if !user.has_role(Role::Admin, "MY-ORG-ID ") {
68+ /// if !user.has_role("admin ") {
7569/// return StatusCode::FORBIDDEN.into_response();
7670/// }
77- /// "Hello Admin".into_response()
78- /// }
79- ///
80- /// trait MyAuthorizationChecks {
81- /// fn has_role(&self, role: Role, org_id: &str) -> bool;
82- /// }
83- ///
84- /// impl MyAuthorizationChecks for IntrospectedUser {
85- /// fn has_role(&self, role: Role, org_id: &str) -> bool {
86- /// let role = match role {
87- /// Role::Admin => "Admin",
88- /// Role::Client => "Client",
89- /// };
90- /// self.project_roles.as_ref()
91- /// .and_then(|roles| roles.get(role))
92- /// .map(|org_ids| org_ids.contains_key(org_id))
93- /// .unwrap_or(false)
71+ ///
72+ /// if user.has_role_in_project("project123", "editor") {
73+ /// return "Hello Editor".into_response();
9474/// }
75+ ///
76+ /// "Hello Admin".into_response()
9577/// }
9678/// ```
97- #[ derive( Debug ) ]
98- pub struct IntrospectedUser {
99- /// UserID of the introspected user (OIDC Field "sub").
100- pub user_id : String ,
101- pub username : Option < String > ,
102- pub name : Option < String > ,
103- pub given_name : Option < String > ,
104- pub family_name : Option < String > ,
105- pub preferred_username : Option < String > ,
106- pub email : Option < String > ,
107- pub email_verified : Option < bool > ,
108- pub locale : Option < String > ,
109- pub project_roles : Option < HashMap < String , HashMap < String , String > > > ,
110- pub metadata : Option < HashMap < String , String > > ,
111- }
79+ pub type IntrospectedUser = ZitadelClaims ;
11280
11381impl < S > FromRequestParts < S > for IntrospectedUser
11482where
@@ -166,37 +134,17 @@ where
166134 )
167135 . await ;
168136
169- let user: Result < IntrospectedUser , IntrospectionGuardError > = match res {
170- Ok ( res) => match res. active ( ) {
171- true if res. sub ( ) . is_some ( ) => Ok ( res. into ( ) ) ,
172- false => Err ( IntrospectionGuardError :: Inactive ) ,
173- _ => Err ( IntrospectionGuardError :: NoUserId ) ,
174- } ,
175- Err ( source) => return Err ( IntrospectionGuardError :: Introspection { source } ) ,
176- } ;
177-
178- user
179- }
180- }
137+ let claims = res. map_err ( |source| IntrospectionGuardError :: Introspection { source } ) ?;
181138
182- impl From < ZitadelIntrospectionResponse > for IntrospectedUser {
183- fn from ( response : ZitadelIntrospectionResponse ) -> Self {
184- Self {
185- user_id : response. sub ( ) . unwrap ( ) . to_string ( ) ,
186- username : response. username ( ) . map ( |s| s. to_string ( ) ) ,
187- name : response. extra_fields ( ) . name . clone ( ) ,
188- given_name : response. extra_fields ( ) . given_name . clone ( ) ,
189- family_name : response. extra_fields ( ) . family_name . clone ( ) ,
190- preferred_username : response. extra_fields ( ) . preferred_username . clone ( ) ,
191- email : response. extra_fields ( ) . email . clone ( ) ,
192- email_verified : response. extra_fields ( ) . email_verified ,
193- locale : response. extra_fields ( ) . locale . clone ( ) ,
194- project_roles : response. extra_fields ( ) . project_roles . clone ( ) ,
195- metadata : response. extra_fields ( ) . metadata . clone ( ) ,
139+ if claims. sub . is_empty ( ) {
140+ return Err ( IntrospectionGuardError :: NoUserId ) ;
196141 }
142+
143+ Ok ( claims)
197144 }
198145}
199146
147+
200148#[ cfg( test) ]
201149mod tests {
202150 #![ allow( clippy:: all) ]
@@ -229,7 +177,7 @@ mod tests {
229177 async fn authed ( user : IntrospectedUser ) -> impl IntoResponse {
230178 format ! (
231179 "Hello authorized user: {:?} with id {}" ,
232- user. username, user. user_id
180+ user. username, user. sub
233181 )
234182 }
235183
@@ -362,7 +310,6 @@ mod tests {
362310 use super :: * ;
363311 use crate :: oidc:: introspection:: cache:: in_memory:: InMemoryIntrospectionCache ;
364312 use crate :: oidc:: introspection:: cache:: IntrospectionCache ;
365- use crate :: oidc:: introspection:: ZitadelIntrospectionExtraTokenFields ;
366313 use chrono:: { TimeDelta , Utc } ;
367314 use http_body_util:: BodyExt ;
368315 use std:: ops:: Add ;
@@ -393,12 +340,17 @@ mod tests {
393340 let cache = Arc :: new ( InMemoryIntrospectionCache :: default ( ) ) ;
394341 let app = app_witch_cache ( cache. clone ( ) ) . await ;
395342
396- let mut res = ZitadelIntrospectionResponse :: new (
397- true ,
398- ZitadelIntrospectionExtraTokenFields :: default ( ) ,
399- ) ;
400- res. set_sub ( Some ( "cached_sub" . to_string ( ) ) ) ;
401- res. set_exp ( Some ( Utc :: now ( ) . add ( TimeDelta :: days ( 1 ) ) ) ) ;
343+ use crate :: oidc:: introspection:: claims:: ZitadelClaims ;
344+ let res = ZitadelClaims {
345+ sub : "cached_sub" . to_string ( ) ,
346+ iss : "https://test.zitadel.cloud" . to_string ( ) ,
347+ aud : vec ! [ "test" . to_string( ) ] ,
348+ username : Some ( "cached_user" . to_string ( ) ) ,
349+ exp : Utc :: now ( ) . add ( TimeDelta :: days ( 1 ) ) . timestamp ( ) ,
350+ iat : Utc :: now ( ) . timestamp ( ) ,
351+ active : true ,
352+ ..Default :: default ( )
353+ } ;
402354 cache. set ( PERSONAL_ACCESS_TOKEN , res) . await ;
403355
404356 let response = app
@@ -454,7 +406,7 @@ mod tests {
454406
455407 let cached_response = cache. get ( PERSONAL_ACCESS_TOKEN ) . await . unwrap ( ) ;
456408
457- assert ! ( text. contains( cached_response. sub ( ) . unwrap( ) ) ) ;
409+ assert ! ( text. contains( & cached_response. username . unwrap( ) ) ) ;
458410 }
459411 }
460412}
0 commit comments