@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
66use  std:: collections:: HashMap ; 
77use  std:: result:: Result  as  StdResult ; 
88use  std:: sync:: Mutex ; 
9- use  std:: time:: { Duration ,  Instant ,   SystemTime ,   UNIX_EPOCH } ; 
9+ use  std:: time:: { Duration ,  Instant } ; 
1010
1111// https://auth0.com/docs/jwks 
1212#[ derive( Debug ,  Serialize ,  Deserialize ) ]  
@@ -73,175 +73,6 @@ impl EqCheck {
7373    } 
7474} 
7575
76- // https://infosec.mozilla.org/guidelines/iam/openid_connect#session-handling 
77- const  MOZ_SESSION_TIMEOUT :  Duration  = Duration :: from_secs ( 60  *  15 ) ; 
78- const  MOZ_USERINFO_ENDPOINT :  & str  = "https://auth.mozilla.auth0.com/userinfo" ; 
79- 
80- /// Mozilla-specific check by forwarding the token onto the auth0 userinfo endpoint 
81- pub  struct  MozillaCheck  { 
82-     // token, token_expiry 
83-     auth_cache :  Mutex < HashMap < String ,  Instant > > , 
84-     client :  reqwest:: blocking:: Client , 
85-     required_groups :  Vec < String > , 
86- } 
87- 
88- impl  ClientAuthCheck  for  MozillaCheck  { 
89-     fn  check ( & self ,  token :  & str )  -> StdResult < ( ) ,  ClientVisibleMsg >  { 
90-         self . check_mozilla ( token) . map_err ( |e| { 
91-             warn ! ( "Mozilla token validation failed: {}" ,  e) ; 
92-             ClientVisibleMsg :: from_nonsensitive ( 
93-                 "Failed to validate Mozilla OAuth token, run sccache --dist-auth" . to_owned ( ) , 
94-             ) 
95-         } ) 
96-     } 
97- } 
98- 
99- impl  MozillaCheck  { 
100-     pub  fn  new ( required_groups :  Vec < String > )  -> Self  { 
101-         Self  { 
102-             auth_cache :  Mutex :: new ( HashMap :: new ( ) ) , 
103-             client :  new_reqwest_blocking_client ( ) , 
104-             required_groups, 
105-         } 
106-     } 
107- 
108-     fn  check_mozilla ( & self ,  token :  & str )  -> Result < ( ) >  { 
109-         // azp == client_id 
110-         // { 
111-         //   "iss": "https://auth.mozilla.auth0.com/", 
112-         //   "sub": "ad|Mozilla-LDAP|asayers", 
113-         //   "aud": [ 
114-         //     "sccache", 
115-         //     "https://auth.mozilla.auth0.com/userinfo" 
116-         //   ], 
117-         //   "iat": 1541103283, 
118-         //   "exp": 1541708083, 
119-         //   "azp": "F1VVD6nRTckSVrviMRaOdLBWIk1AvHYo", 
120-         //   "scope": "openid" 
121-         // } 
122-         #[ derive( Deserialize ) ]  
123-         struct  MozillaToken  { 
124-             exp :  u64 , 
125-             sub :  String , 
126-         } 
127-         let  mut  validation = jwt:: Validation :: default ( ) ; 
128-         validation. validate_exp  = false ; 
129-         validation. validate_nbf  = false ; 
130-         // We don't really do any validation here (just forwarding on) so it's ok to unsafely decode 
131-         validation. insecure_disable_signature_validation ( ) ; 
132-         let  dummy_key = jwt:: DecodingKey :: from_secret ( b"secret" ) ; 
133-         let  insecure_token = jwt:: decode :: < MozillaToken > ( token,  & dummy_key,  & validation) 
134-             . context ( "Unable to decode jwt" ) ?; 
135-         let  user = insecure_token. claims . sub ; 
136-         trace ! ( "Validating token for user {} with mozilla" ,  user) ; 
137-         if  UNIX_EPOCH  + Duration :: from_secs ( insecure_token. claims . exp )  < SystemTime :: now ( )  { 
138-             bail ! ( "JWT expired" ) 
139-         } 
140- 
141-         // If the token is cached and not expired, return it 
142-         let  mut  auth_cache = self . auth_cache . lock ( ) . unwrap ( ) ; 
143-         if  let  Some ( cached_at)  = auth_cache. get ( token)  { 
144-             if  cached_at. elapsed ( )  < MOZ_SESSION_TIMEOUT  { 
145-                 return  Ok ( ( ) ) ; 
146-             } 
147-         } 
148-         auth_cache. remove ( token) ; 
149- 
150-         debug ! ( "User {} not in cache, validating via auth0 endpoint" ,  user) ; 
151-         // Retrieve the groups from the auth0 /userinfo endpoint, which Mozilla rules populate with groups 
152-         // https://github.com/mozilla-iam/auth0-deploy/blob/6889f1dde12b84af50bb4b2e2f00d5e80d5be33f/rules/CIS-Claims-fixups.js#L158-L168 
153-         let  url = reqwest:: Url :: parse ( MOZ_USERINFO_ENDPOINT ) 
154-             . expect ( "Failed to parse MOZ_USERINFO_ENDPOINT" ) ; 
155- 
156-         let  res = self 
157-             . client 
158-             . get ( url. clone ( ) ) 
159-             . bearer_auth ( token) 
160-             . send ( ) 
161-             . context ( "Failed to make request to mozilla userinfo" ) ?; 
162-         let  status = res. status ( ) ; 
163-         let  res_text = res
164-             . text ( ) 
165-             . context ( "Failed to interpret response from mozilla userinfo as string" ) ?; 
166-         if  !status. is_success ( )  { 
167-             bail ! ( "JWT forwarded to {} returned {}: {}" ,  url,  status,  res_text) 
168-         } 
169- 
170-         // The API didn't return a HTTP error code, let's check the response 
171-         check_mozilla_profile ( & user,  & self . required_groups ,  & res_text) 
172-             . with_context ( || format ! ( "Validation of the user profile failed for {}" ,  user) ) ?; 
173- 
174-         // Validation success, cache the token 
175-         debug ! ( "Validation for user {} succeeded, caching" ,  user) ; 
176-         auth_cache. insert ( token. to_owned ( ) ,  Instant :: now ( ) ) ; 
177-         Ok ( ( ) ) 
178-     } 
179- } 
180- 
181- fn  check_mozilla_profile ( user :  & str ,  required_groups :  & [ String ] ,  profile :  & str )  -> Result < ( ) >  { 
182-     #[ derive( Deserialize ) ]  
183-     struct  UserInfo  { 
184-         sub :  String , 
185-         #[ serde( rename = "https://sso.mozilla.com/claim/groups" ) ]  
186-         groups :  Vec < String > , 
187-     } 
188-     let  profile:  UserInfo  = serde_json:: from_str ( profile) 
189-         . with_context ( || format ! ( "Could not parse profile: {}" ,  profile) ) ?; 
190-     if  user != profile. sub  { 
191-         bail ! ( 
192-             "User {} retrieved in profile is different to desired user {}" , 
193-             profile. sub, 
194-             user
195-         ) 
196-     } 
197-     for  group in  required_groups. iter ( )  { 
198-         if  !profile. groups . contains ( group)  { 
199-             bail ! ( "User {} is not a member of required group {}" ,  user,  group) 
200-         } 
201-     } 
202-     Ok ( ( ) ) 
203- } 
204- 
205- #[ test]  
206- fn  test_auth_verify_check_mozilla_profile ( )  { 
207-     // A successful response 
208-     let  profile = r#"{ 
209-         "sub": "ad|Mozilla-LDAP|asayers", 
210-         "https://sso.mozilla.com/claim/groups": [ 
211-             "everyone", 
212-             "hris_dept_firefox", 
213-             "hris_individual_contributor", 
214-             "hris_nonmanagers", 
215-             "hris_is_staff", 
216-             "hris_workertype_contractor" 
217-         ], 
218-         "https://sso.mozilla.com/claim/README_FIRST": "Please refer to https://github.com/mozilla-iam/person-api in order to query Mozilla IAM CIS user profile data" 
219-     }"# ; 
220- 
221-     // If the user has been deactivated since the token was issued. Note this may be partnered with an error code 
222-     // response so may never reach validation 
223-     let  profile_fail = r#"{ 
224-         "error": "unauthorized", 
225-         "error_description": "user is blocked" 
226-     }"# ; 
227- 
228-     assert ! ( check_mozilla_profile( 
229-         "ad|Mozilla-LDAP|asayers" , 
230-         & [ "hris_dept_firefox" . to_owned( ) ] , 
231-         profile, 
232-     ) 
233-     . is_ok( ) ) ; 
234-     assert ! ( check_mozilla_profile( "ad|Mozilla-LDAP|asayers" ,  & [ ] ,  profile) . is_ok( ) ) ; 
235-     assert ! ( check_mozilla_profile( 
236-         "ad|Mozilla-LDAP|asayers" , 
237-         & [ "hris_the_ceo" . to_owned( ) ] , 
238-         profile, 
239-     ) 
240-     . is_err( ) ) ; 
241- 
242-     assert ! ( check_mozilla_profile( "ad|Mozilla-LDAP|asayers" ,  & [ ] ,  profile_fail) . is_err( ) ) ; 
243- } 
244- 
24576// Don't check a token is valid (it may not even be a JWT) just forward it to 
24677// an API and check for success 
24778pub  struct  ProxyTokenCheck  { 
0 commit comments