diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 3831c68..a593420 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -178,14 +178,6 @@ pub struct RoleConfig { #[serde(default)] pub subject_conditions: Vec, - /// Issuers whose tokens may omit `exp`, because the host tracks their - /// validity itself — its own long-lived API keys with server-side - /// revocation, say. Tokens from every other issuer must carry `exp`: a - /// third-party token with no expiry is an indefinitely replayable - /// credential its issuer never meant to issue. - #[serde(default)] - pub allow_missing_exp_from: Vec, - /// Buckets and prefixes this role can access. #[serde(default)] pub allowed_scopes: Vec, diff --git a/crates/static-config/src/lib.rs b/crates/static-config/src/lib.rs index 7786617..5fa56b9 100644 --- a/crates/static-config/src/lib.rs +++ b/crates/static-config/src/lib.rs @@ -298,7 +298,6 @@ mod tests { trusted_oidc_issuers: vec!["https://issuer.example.com".into()], required_audiences: vec!["my-audience".into()], subject_conditions: vec!["*".into()], - allow_missing_exp_from: vec![], allowed_scopes: vec![], max_session_duration_secs: 3600, }], diff --git a/crates/sts/README.md b/crates/sts/README.md index 6cde12f..8cceb09 100644 --- a/crates/sts/README.md +++ b/crates/sts/README.md @@ -30,5 +30,4 @@ Roles define who can assume them: - **`trusted_oidc_issuers`** — accepted OIDC providers (e.g., `https://token.actions.githubusercontent.com`) - **`required_audiences`** — accepted `aud` claim values (string or list); a token passes if its `aud` matches any. Empty/omitted accepts no token. Legacy `required_audience` (single string) still accepted. - **`subject_conditions`** — glob patterns for the `sub` claim (e.g., `repo:myorg/*`). Empty accepts no subject; `"*"` accepts any. -- **`allow_missing_exp_from`** — issuers whose tokens may omit `exp` because the host tracks their validity; every other issuer's tokens must carry it - **`allowed_scopes`** — buckets, prefixes, and actions the minted credentials grant diff --git a/crates/sts/src/jwks.rs b/crates/sts/src/jwks.rs index 816af0f..06e3c3b 100644 --- a/crates/sts/src/jwks.rs +++ b/crates/sts/src/jwks.rs @@ -241,13 +241,11 @@ fn validate_claims( return Err(ProxyError::InvalidOidcToken("token has expired".into())); } } - // Only an issuer the host vouches for may leave expiry to the host. + // A token with no expiry is replayable for good. None => { - if !role.allow_missing_exp_from.iter().any(|i| i == issuer) { - return Err(ProxyError::InvalidOidcToken( - "token has no exp claim".into(), - )); - } + return Err(ProxyError::InvalidOidcToken( + "token has no exp claim".into(), + )); } } @@ -370,7 +368,6 @@ mod tests { trusted_oidc_issuers: vec![ISSUER.into()], required_audiences: vec!["aud".into()], subject_conditions: vec!["*".into()], - allow_missing_exp_from: vec![], allowed_scopes: vec![], max_session_duration_secs: 3600, } @@ -411,7 +408,7 @@ mod tests { } #[test] - fn exp_is_required_unless_the_issuer_is_exempt() { + fn exp_is_required() { let mut claims = claims(); claims.as_object_mut().unwrap().remove("exp"); @@ -419,10 +416,6 @@ mod tests { .unwrap_err() .to_string(); assert!(err.contains("no exp claim"), "{}", err); - - let mut exempt = role(); - exempt.allow_missing_exp_from = vec![ISSUER.into()]; - validate_claims(&json!({}), &claims, ISSUER, &exempt, NOW).unwrap(); } #[test] diff --git a/docs/auth/proxy-auth.md b/docs/auth/proxy-auth.md index f06022f..7651267 100644 --- a/docs/auth/proxy-auth.md +++ b/docs/auth/proxy-auth.md @@ -82,7 +82,7 @@ When a client calls `AssumeRoleWithWebIdentity`: - **Issuer**: must be in the role's `trusted_oidc_issuers` - **Token type**: if the header carries `typ`, it must be `JWT` - **Audience**: the token's `aud` claim must match at least one of the role's `required_audiences`; a role with none accepts no token - - **Expiry**: the token must carry `exp`, unless its issuer is in the role's `allow_missing_exp_from` + - **Expiry**: the token must carry `exp` - **Subject**: the token's `sub` claim must match at least one of the role's `subject_conditions` (supports `*` glob wildcards); a role with none accepts no subject 5. The proxy mints temporary credentials scoped to the role's `allowed_scopes` 6. If `SESSION_TOKEN_KEY` is configured, the credentials are AES-256-GCM encrypted into the session token (see [Sealed Session Tokens](./sealed-tokens)) diff --git a/docs/configuration/roles.md b/docs/configuration/roles.md index 77909cb..926d7f9 100644 --- a/docs/configuration/roles.md +++ b/docs/configuration/roles.md @@ -36,7 +36,6 @@ actions = ["get_object", "head_object"] | `trusted_oidc_issuers` | string[] | Validated as required | OIDC provider URLs whose tokens are accepted. Deserializes fine when absent, but config validation rejects a role with no issuers (it could never accept a token). | | `required_audiences` | string \| string[] | Validated as required | Accepted `aud` claim values. A token passes if its `aud` matches any entry. Empty or omitted accepts no token — the audience is what keeps a token minted for another service from being exchanged here — and config validation rejects the role. Accepts a single string or a list. The legacy `required_audience` key (single string) is still accepted for backward compatibility — set one key or the other, not both. | | `subject_conditions` | string[] | Validated as required | Glob patterns matched against the `sub` claim. Empty or omitted accepts no subject, and config validation rejects the role; to accept every subject, say so with `"*"`. | -| `allow_missing_exp_from` | string[] | No | Issuers whose tokens may omit `exp` because the host tracks their validity itself — its own long-lived API keys with server-side revocation, say. Tokens from every other issuer must carry `exp`. | | `max_session_duration_secs` | integer | Yes | Maximum session lifetime granted by this role | | `allowed_scopes` | AccessScope[] | Yes | Buckets, prefixes, and actions granted | @@ -49,7 +48,7 @@ When a client calls `AssumeRoleWithWebIdentity`, the proxy evaluates the JWT aga 3. **Signature** — Verified against the issuer's JWKS (fetched and cached) 4. **Token type** — If the JWT header carries `typ`, it must be `JWT`; access tokens (`at+jwt`) and other typed tokens are not identity tokens 5. **Audience** — The JWT's `aud` claim must match at least one of `required_audiences`; a role with none accepts no token -6. **Expiry** — The JWT must carry `exp` (validated with 60 seconds of clock skew), unless its issuer is listed in `allow_missing_exp_from` +6. **Expiry** — The JWT must carry `exp` (validated with 60 seconds of clock skew) 7. **Subject** — The JWT's `sub` claim must match at least one of `subject_conditions`; a role with none accepts no subject If any check fails, the STS request returns an error.