Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/bin/sccache-dist/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,6 @@ fn run(command: Command) -> Result<i32> {
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))
}
Expand Down
171 changes: 1 addition & 170 deletions src/bin/sccache-dist/token_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<HashMap<String, Instant>>,
client: reqwest::blocking::Client,
required_groups: Vec<String>,
}

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<String>) -> 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::<MozillaToken>(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<String>,
}
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 {
Expand Down
17 changes: 0 additions & 17 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -1096,8 +1081,6 @@ pub mod scheduler {
issuer: String,
jwks_url: String,
},
#[serde(rename = "mozilla")]
Mozilla { required_groups: Vec<String> },
#[serde(rename = "proxy_token")]
ProxyToken {
url: String,
Expand Down
Loading