Skip to content
Draft
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
8 changes: 0 additions & 8 deletions crates/core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,6 @@ pub struct RoleConfig {
#[serde(default)]
pub subject_conditions: Vec<String>,

/// 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<String>,

/// Buckets and prefixes this role can access.
#[serde(default)]
pub allowed_scopes: Vec<AccessScope>,
Expand Down
1 change: 0 additions & 1 deletion crates/static-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}],
Expand Down
1 change: 0 additions & 1 deletion crates/sts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 5 additions & 12 deletions crates/sts/src/jwks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
));
}
}

Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -411,18 +408,14 @@ 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");

let err = validate_claims(&json!({}), &claims, ISSUER, &role(), NOW)
.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]
Expand Down
2 changes: 1 addition & 1 deletion docs/auth/proxy-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
3 changes: 1 addition & 2 deletions docs/configuration/roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand All @@ -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.
Expand Down
Loading