@@ -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