diff --git a/src/bin/sccache-dist/main.rs b/src/bin/sccache-dist/main.rs index b11de9e14..087e06451 100644 --- a/src/bin/sccache-dist/main.rs +++ b/src/bin/sccache-dist/main.rs @@ -175,9 +175,6 @@ fn run(command: Command) -> Result { token_check::ValidJWTCheck::new(audience, issuer, &jwks_url) .context("Failed to create a checker for valid JWTs")?, ), - scheduler_config::ClientAuth::Mozilla { required_groups } => { - Box::new(token_check::MozillaCheck::new(required_groups)) - } scheduler_config::ClientAuth::ProxyToken { url, cache_secs } => { Box::new(token_check::ProxyTokenCheck::new(url, cache_secs)) } diff --git a/src/bin/sccache-dist/token_check.rs b/src/bin/sccache-dist/token_check.rs index e335ae313..e29baa6fb 100644 --- a/src/bin/sccache-dist/token_check.rs +++ b/src/bin/sccache-dist/token_check.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::result::Result as StdResult; use std::sync::Mutex; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, Instant}; // https://auth0.com/docs/jwks #[derive(Debug, Serialize, Deserialize)] @@ -73,175 +73,6 @@ impl EqCheck { } } -// https://infosec.mozilla.org/guidelines/iam/openid_connect#session-handling -const MOZ_SESSION_TIMEOUT: Duration = Duration::from_secs(60 * 15); -const MOZ_USERINFO_ENDPOINT: &str = "https://auth.mozilla.auth0.com/userinfo"; - -/// Mozilla-specific check by forwarding the token onto the auth0 userinfo endpoint -pub struct MozillaCheck { - // token, token_expiry - auth_cache: Mutex>, - client: reqwest::blocking::Client, - required_groups: Vec, -} - -impl ClientAuthCheck for MozillaCheck { - fn check(&self, token: &str) -> StdResult<(), ClientVisibleMsg> { - self.check_mozilla(token).map_err(|e| { - warn!("Mozilla token validation failed: {}", e); - ClientVisibleMsg::from_nonsensitive( - "Failed to validate Mozilla OAuth token, run sccache --dist-auth".to_owned(), - ) - }) - } -} - -impl MozillaCheck { - pub fn new(required_groups: Vec) -> Self { - Self { - auth_cache: Mutex::new(HashMap::new()), - client: new_reqwest_blocking_client(), - required_groups, - } - } - - fn check_mozilla(&self, token: &str) -> Result<()> { - // azp == client_id - // { - // "iss": "https://auth.mozilla.auth0.com/", - // "sub": "ad|Mozilla-LDAP|asayers", - // "aud": [ - // "sccache", - // "https://auth.mozilla.auth0.com/userinfo" - // ], - // "iat": 1541103283, - // "exp": 1541708083, - // "azp": "F1VVD6nRTckSVrviMRaOdLBWIk1AvHYo", - // "scope": "openid" - // } - #[derive(Deserialize)] - struct MozillaToken { - exp: u64, - sub: String, - } - let mut validation = jwt::Validation::default(); - validation.validate_exp = false; - validation.validate_nbf = false; - // We don't really do any validation here (just forwarding on) so it's ok to unsafely decode - validation.insecure_disable_signature_validation(); - let dummy_key = jwt::DecodingKey::from_secret(b"secret"); - let insecure_token = jwt::decode::(token, &dummy_key, &validation) - .context("Unable to decode jwt")?; - let user = insecure_token.claims.sub; - trace!("Validating token for user {} with mozilla", user); - if UNIX_EPOCH + Duration::from_secs(insecure_token.claims.exp) < SystemTime::now() { - bail!("JWT expired") - } - - // If the token is cached and not expired, return it - let mut auth_cache = self.auth_cache.lock().unwrap(); - if let Some(cached_at) = auth_cache.get(token) { - if cached_at.elapsed() < MOZ_SESSION_TIMEOUT { - return Ok(()); - } - } - auth_cache.remove(token); - - debug!("User {} not in cache, validating via auth0 endpoint", user); - // Retrieve the groups from the auth0 /userinfo endpoint, which Mozilla rules populate with groups - // https://github.com/mozilla-iam/auth0-deploy/blob/6889f1dde12b84af50bb4b2e2f00d5e80d5be33f/rules/CIS-Claims-fixups.js#L158-L168 - let url = reqwest::Url::parse(MOZ_USERINFO_ENDPOINT) - .expect("Failed to parse MOZ_USERINFO_ENDPOINT"); - - let res = self - .client - .get(url.clone()) - .bearer_auth(token) - .send() - .context("Failed to make request to mozilla userinfo")?; - let status = res.status(); - let res_text = res - .text() - .context("Failed to interpret response from mozilla userinfo as string")?; - if !status.is_success() { - bail!("JWT forwarded to {} returned {}: {}", url, status, res_text) - } - - // The API didn't return a HTTP error code, let's check the response - check_mozilla_profile(&user, &self.required_groups, &res_text) - .with_context(|| format!("Validation of the user profile failed for {}", user))?; - - // Validation success, cache the token - debug!("Validation for user {} succeeded, caching", user); - auth_cache.insert(token.to_owned(), Instant::now()); - Ok(()) - } -} - -fn check_mozilla_profile(user: &str, required_groups: &[String], profile: &str) -> Result<()> { - #[derive(Deserialize)] - struct UserInfo { - sub: String, - #[serde(rename = "https://sso.mozilla.com/claim/groups")] - groups: Vec, - } - let profile: UserInfo = serde_json::from_str(profile) - .with_context(|| format!("Could not parse profile: {}", profile))?; - if user != profile.sub { - bail!( - "User {} retrieved in profile is different to desired user {}", - profile.sub, - user - ) - } - for group in required_groups.iter() { - if !profile.groups.contains(group) { - bail!("User {} is not a member of required group {}", user, group) - } - } - Ok(()) -} - -#[test] -fn test_auth_verify_check_mozilla_profile() { - // A successful response - let profile = r#"{ - "sub": "ad|Mozilla-LDAP|asayers", - "https://sso.mozilla.com/claim/groups": [ - "everyone", - "hris_dept_firefox", - "hris_individual_contributor", - "hris_nonmanagers", - "hris_is_staff", - "hris_workertype_contractor" - ], - "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" - }"#; - - // If the user has been deactivated since the token was issued. Note this may be partnered with an error code - // response so may never reach validation - let profile_fail = r#"{ - "error": "unauthorized", - "error_description": "user is blocked" - }"#; - - assert!(check_mozilla_profile( - "ad|Mozilla-LDAP|asayers", - &["hris_dept_firefox".to_owned()], - profile, - ) - .is_ok()); - assert!(check_mozilla_profile("ad|Mozilla-LDAP|asayers", &[], profile).is_ok()); - assert!(check_mozilla_profile( - "ad|Mozilla-LDAP|asayers", - &["hris_the_ceo".to_owned()], - profile, - ) - .is_err()); - - assert!(check_mozilla_profile("ad|Mozilla-LDAP|asayers", &[], profile_fail).is_err()); -} - // Don't check a token is valid (it may not even be a JWT) just forward it to // an API and check for success pub struct ProxyTokenCheck { diff --git a/src/config.rs b/src/config.rs index 9c55d2ec2..6e7d4eeb3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -44,14 +44,6 @@ const APP_NAME: &str = "sccache"; const DIST_APP_NAME: &str = "sccache-dist-client"; const TEN_GIGS: u64 = 10 * 1024 * 1024 * 1024; -const MOZILLA_OAUTH_PKCE_CLIENT_ID: &str = "F1VVD6nRTckSVrviMRaOdLBWIk1AvHYo"; -// The sccache audience is an API set up in auth0 for sccache to allow 7 day expiry, -// the openid scope allows us to query the auth0 /userinfo endpoint which contains -// group information due to Mozilla rules. -const MOZILLA_OAUTH_PKCE_AUTH_URL: &str = - "https://auth.mozilla.auth0.com/authorize?audience=sccache&scope=openid%20profile"; -const MOZILLA_OAUTH_PKCE_TOKEN_URL: &str = "https://auth.mozilla.auth0.com/oauth/token"; - pub const INSECURE_DIST_CLIENT_TOKEN: &str = "dangerously_insecure_client"; // Unfortunately this means that nothing else can use the sccache cache dir as @@ -472,8 +464,6 @@ impl<'a> Deserialize<'a> for DistAuth { pub enum Helper { #[serde(rename = "token")] Token { token: String }, - #[serde(rename = "mozilla")] - Mozilla, #[serde(rename = "oauth2_code_grant_pkce")] Oauth2CodeGrantPKCE { client_id: String, @@ -488,11 +478,6 @@ impl<'a> Deserialize<'a> for DistAuth { Ok(match helper { Helper::Token { token } => DistAuth::Token { token }, - Helper::Mozilla => DistAuth::Oauth2CodeGrantPKCE { - client_id: MOZILLA_OAUTH_PKCE_CLIENT_ID.to_owned(), - auth_url: MOZILLA_OAUTH_PKCE_AUTH_URL.to_owned(), - token_url: MOZILLA_OAUTH_PKCE_TOKEN_URL.to_owned(), - }, Helper::Oauth2CodeGrantPKCE { client_id, auth_url, @@ -1096,8 +1081,6 @@ pub mod scheduler { issuer: String, jwks_url: String, }, - #[serde(rename = "mozilla")] - Mozilla { required_groups: Vec }, #[serde(rename = "proxy_token")] ProxyToken { url: String,