* feat(oidc): make the Console session duration configurable The OIDC callback hard-coded the Console credential lifetime at 3600 seconds, so a Console session always ended after about an hour. RUSTFS_CONSOLE_AUTH_TIMEOUT, the documented knob next to this behaviour, is the Console HTTP request timeout and cannot extend a session. Resolve the lifetime from the process-level RUSTFS_CONSOLE_OIDC_SESSION_DURATION instead and forward it to complete_authorization_code(). Unset, unparseable, zero, and negative values keep DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS (3600), so existing deployments keep the previous behaviour and a misconfigured variable can never issue an already expired credential. Values above CONSOLE_OIDC_SESSION_DURATION_MAX_SECS (7 days) are clamped to that maximum with a warning, which also caps how far an operator can stretch a credential by accident. Keep the Console logout token for that same lifetime. OidcStateStore pinned the logout cache to a hard-coded hour, so a session configured to last longer than that dropped its logout token while the credential was still valid: RP-initiated logout could no longer resolve, and the browser was sent back to the Console login page with the IdP session still active. The logout cache now derives its TTL from the same console_session_duration_secs(), which applies the same clamp, and the callback reads its lifetime from that one function instead of a local copy, so the credential lifetime and the logout-token retention cannot drift apart. Add tests covering the default hour, an extended session, the maximum, and out-of-range values. The variable is deliberately not part of ENV_IDENTITY_OPENID_KEYS: that list is suffix-scanned per provider, and this setting applies to every provider. It has no identity_openid KVS equivalent and needs a restart, like the other process-level settings in the same document. Document the setting in the login flow, the process-level table, and the troubleshooting table, and clarify that RUSTFS_CONSOLE_AUTH_TIMEOUT is a request timeout rather than a session lifetime. * test(ecstore): retain offline marker fixtures across reconnect Keep simulated drive handles attached and reject I/O until explicit rejoin. Attempt reconnect in the current-marker case so the former CI race is deterministic while retaining physical cleanup and historical-data checks. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(oidc): align session diagnostics and lifetime regression tests Use structured IAM diagnostics for clamping and move configuration tests to their owning crate. Cover duration boundaries, logout retention and signed credential expiration through the federation binding. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: Chris <anzhengchao@gmail.com> Co-authored-by: Hauser <housemecn@gmail.com> Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com>
16 KiB
OIDC Console integration
Use this when: connecting RustFS Console login to an OpenID Connect provider (Keycloak, Authing, or any standards-compliant IdP), or debugging an OIDC redirect, token, or policy-mapping failure.
Source of truth: crates/config/src/constants/oidc.rs (provider keys and RUSTFS_IDENTITY_OPENID_*), crates/iam/src/oidc/config.rs (per-provider env suffixes), crates/iam/src/oidc/provider.rs (discovery and JWKS), crates/iam/src/oidc/runtime.rs (PKCE and token validation), rustfs/src/admin/handlers/oidc.rs (authorize/callback handlers), crates/config/src/constants/app.rs (ENV_RUSTFS_BROWSER_REDIRECT_URL), crates/utils/src/egress.rs (ENV_OUTBOUND_ALLOW_ORIGINS), crates/policy/src/policy/policy.rs (built-in policies).
The RustFS side is vendor-neutral and is described once; what RustFS requires from any provider is tabulated in oidc-provider-requirements.md. The Keycloak and Authing sections contain only IdP-side steps and vendor caveats. Examples use provider id default and public origin https://rustfs.example.com.
Integration model
RustFS requires a standards-compliant OpenID Connect provider: discovery at <issuer>/.well-known/openid-configuration, authorization and token endpoints, a JWKS, and an authorization-code flow that returns an id_token. RustFS never calls a vendor's authorization API; access is decided by RustFS IAM policies after claim mapping. Protocol requirements for IdP vendors are collected in oidc-provider-requirements.md.
Login flow:
- The browser opens
https://rustfs.example.com/rustfs/admin/v3/oidc/authorize/<provider_id>. - RustFS creates
state,nonce, and a PKCE S256 challenge and redirects to the IdP. - The IdP redirects back to
/rustfs/admin/v3/oidc/callback/<provider_id>?code=...&state=.... - RustFS exchanges the code at the token endpoint, sending
client_idandclient_secretin the request body (client_secret_post) together with the PKCE verifier. - RustFS validates the ID token signature (JWKS), issuer, audience, expiry, and nonce.
- RustFS maps claim values to policy names and issues STS credentials to the Console with the lifetime set by
RUSTFS_CONSOLE_OIDC_SESSION_DURATION(default 3600 seconds, one hour).
In-flight state and PKCE verifiers are node-local: the authorize and callback requests must reach the same RustFS node.
Configuration keys
Every provider key can be set as RUSTFS_IDENTITY_OPENID_<KEY> in the process environment or as identity_openid <key>=<value> through mc admin config set. Names are constants in crates/config/src/constants/oidc.rs.
| Provider key | Environment variable | Purpose |
|---|---|---|
enable |
RUSTFS_IDENTITY_OPENID_ENABLE |
on loads the provider. |
config_url |
RUSTFS_IDENTITY_OPENID_CONFIG_URL |
Issuer URL used for discovery. A trailing /.well-known/openid-configuration is stripped; any other .well-known path is rejected. |
issuer |
RUSTFS_IDENTITY_OPENID_ISSUER |
Expected iss when it differs from config_url (internal discovery URL, public token issuer). |
client_id, client_secret |
RUSTFS_IDENTITY_OPENID_CLIENT_ID, RUSTFS_IDENTITY_OPENID_CLIENT_SECRET |
Confidential client credentials. |
scopes |
RUSTFS_IDENTITY_OPENID_SCOPES |
Comma-separated; openid is required. |
other_audiences |
RUSTFS_IDENTITY_OPENID_OTHER_AUDIENCES |
Additional accepted aud values. |
redirect_uri |
RUSTFS_IDENTITY_OPENID_REDIRECT_URI |
Callback URL sent to the IdP; must equal the URL registered there. |
redirect_uri_dynamic |
RUSTFS_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC |
on derives the callback from request headers. Keep off behind proxies. |
claim_name, claim_prefix |
RUSTFS_IDENTITY_OPENID_CLAIM_NAME, RUSTFS_IDENTITY_OPENID_CLAIM_PREFIX |
Policy claim name and a fixed string prepended to each value. claim_prefix is not a mapping table. |
groups_claim, roles_claim |
RUSTFS_IDENTITY_OPENID_GROUPS_CLAIM, RUSTFS_IDENTITY_OPENID_ROLES_CLAIM |
Flat top-level array claims whose values are RustFS policy names. |
email_claim, username_claim |
RUSTFS_IDENTITY_OPENID_EMAIL_CLAIM, RUSTFS_IDENTITY_OPENID_USERNAME_CLAIM |
Identity claims shown in the Console. |
role_policy |
RUSTFS_IDENTITY_OPENID_ROLE_POLICY |
One fixed policy for every login from this provider. Connectivity testing only. |
display_name |
RUSTFS_IDENTITY_OPENID_DISPLAY_NAME |
Login button label. |
hide_from_ui |
RUSTFS_IDENTITY_OPENID_HIDE_FROM_UI |
Hides the provider from /oidc/providers. Required (on) for STS workload issuers whose discovery document omits authorization_endpoint; see workload provider requirements. Complete hidden providers still support direct Console login. |
Process-level settings (environment only, never suffixed per provider):
| Variable | Purpose |
|---|---|
RUSTFS_BROWSER_REDIRECT_URL |
Public browser origin used for callback generation, Console success redirects, and logout fallback. |
RUSTFS_OUTBOUND_ALLOW_ORIGINS |
Exact scheme://host[:port] origins RustFS may contact for discovery, JWKS, and token requests when the IdP resolves to a private, loopback, or container-network address. See outbound-connection-policy.md. |
RUSTFS_CONSOLE_OIDC_SESSION_DURATION |
Credential lifetime in seconds for Console credentials issued by this callback, for every provider. Default 3600. Values that are missing, not a positive integer, or zero keep the default; values above 604800 (7 days) are clamped to 604800 with a warning. The same value also sets how long the Console logout token stays valid, so RP-initiated logout keeps working for the whole session. Note that RUSTFS_CONSOLE_AUTH_TIMEOUT does not control this: it is the Console HTTP request timeout. |
Named providers: to use provider id <id>, suffix every provider env var with _<id> (for example RUSTFS_IDENTITY_OPENID_CLIENT_ID_keycloak) and register the callback /rustfs/admin/v3/oidc/callback/<id>. Suffix scanning is parse_single_provider in crates/iam/src/oidc/config.rs.
Restart RustFS after changing any of these settings.
Environment example
export RUSTFS_BROWSER_REDIRECT_URL="https://rustfs.example.com"
export RUSTFS_IDENTITY_OPENID_ENABLE=on
export RUSTFS_IDENTITY_OPENID_CONFIG_URL="<ISSUER>"
export RUSTFS_IDENTITY_OPENID_CLIENT_ID="<CLIENT_ID>"
export RUSTFS_IDENTITY_OPENID_CLIENT_SECRET="<CLIENT_SECRET>"
export RUSTFS_IDENTITY_OPENID_SCOPES="openid,profile,email"
export RUSTFS_IDENTITY_OPENID_REDIRECT_URI="https://rustfs.example.com/rustfs/admin/v3/oidc/callback/default"
export RUSTFS_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC=off
export RUSTFS_IDENTITY_OPENID_DISPLAY_NAME="<IdP name>"
export RUSTFS_IDENTITY_OPENID_GROUPS_CLAIM="groups"
export RUSTFS_IDENTITY_OPENID_ROLES_CLAIM="roles"
export RUSTFS_IDENTITY_OPENID_EMAIL_CLAIM="email"
export RUSTFS_IDENTITY_OPENID_USERNAME_CLAIM="preferred_username"
The same keys through admin config:
mc admin config set rustfs identity_openid \
enable=on config_url="<ISSUER>" client_id="<CLIENT_ID>" client_secret="<CLIENT_SECRET>" \
scopes="openid,profile,email" \
redirect_uri="https://rustfs.example.com/rustfs/admin/v3/oidc/callback/default" \
redirect_uri_dynamic=off display_name="<IdP name>" \
groups_claim="groups" roles_claim="roles" email_claim="email" username_claim="preferred_username"
mc admin service restart rustfs
RUSTFS_BROWSER_REDIRECT_URL is not an identity_openid key; it must still be set in the process environment.
Redirect URL priority
- Provider
redirect_uri, when set, is the callback URL sent to the IdP. RUSTFS_BROWSER_REDIRECT_URL, when set, is the public origin for callback generation when no providerredirect_uriexists, and for Console success and logout fallback redirects.- Request headers (
Host,X-Forwarded-Proto) are used only whenredirect_uri_dynamic=onand no browser redirect URL is configured.
Behind a reverse proxy or load balancer, set RUSTFS_BROWSER_REDIRECT_URL and keep session affinity for the authorize and callback requests.
Policy mapping
Claim values are used verbatim as policy names (after claim_prefix, if any). Names must satisfy is_safe_claim_policy_name in crates/iam/src/sys.rs: ASCII letters, digits, _, -, :, . only, so a value containing / (for example Keycloak's full group path /consoleAdmin) never matches. Built-in policies:
| Policy | Grants |
|---|---|
consoleAdmin |
Full Console, admin, KMS, and S3 access. |
readwrite |
S3 read/write. |
readonly |
S3 read-only. |
writeonly |
S3 write-only. |
diagnostics |
Diagnostic admin access. |
For first-contact testing only, RUSTFS_IDENTITY_OPENID_ROLE_POLICY=consoleAdmin grants every login full access; remove it before production.
Validation
- Discovery:
curl -fsS "<ISSUER>/.well-known/openid-configuration" | jq '{issuer, authorization_endpoint, token_endpoint, jwks_uri, code_challenge_methods_supported, token_endpoint_auth_methods_supported, scopes_supported}'
issuer must equal RUSTFS_IDENTITY_OPENID_ISSUER when set, otherwise the issuer derived from RUSTFS_IDENTITY_OPENID_CONFIG_URL; code_challenge_methods_supported must include S256; token_endpoint_auth_methods_supported must include client_secret_post; scopes_supported must include every configured scope.
-
Provider visibility:
curl -fsS https://rustfs.example.com/rustfs/admin/v3/oidc/providers | jqlists the provider unlesshide_from_ui=on. -
Browser login: open
https://rustfs.example.com/rustfs/admin/v3/oidc/authorize/default. Expect a redirect to the IdP, sign-in, a redirect to/rustfs/admin/v3/oidc/callback/default?code=...&state=..., and then the Console with the mapped permissions. -
ID token claims (decode the token after a test login):
issmatches the issuer,audincludes the client id,emailandpreferred_usernameare present when configured,groupsorrolesis a flat array of policy names.
Troubleshooting
| Symptom | Common cause | Fix |
|---|---|---|
/oidc/providers does not list the provider |
provider failed to load, or RustFS was not restarted | Check env/admin config and restart RustFS. |
Provider or login button missing; startup logs OIDC provider discovery blocked by outbound policy |
IdP origin is private/internal and not allowlisted | Add the exact origin to RUSTFS_OUTBOUND_ALLOW_ORIGINS on every node and restart. |
IdP reports a redirect mismatch (invalid redirect_uri) |
registered callback differs from RustFS redirect_uri |
Use the exact /rustfs/admin/v3/oidc/callback/<provider_id> URL on both sides. |
Callback reports missing code or state |
proxy dropped the query string | Preserve the full callback URL and query string. |
| Token exchange fails | wrong secret, or the IdP rejects request-body client authentication | Confirm the client is confidential and accepts client_secret_post. |
No id_token in the token response |
openid scope missing or a non-OIDC OAuth flow |
Add openid; use the authorization-code flow. |
| ID token verification fails | issuer, audience, algorithm, or JWKS mismatch | Compare discovery metadata with CONFIG_URL/ISSUER/CLIENT_ID; prefer RS256. |
| Login succeeds, access denied | no claim value matches a policy name | Emit groups or roles as a flat array equal to policy names; check for / prefixes. |
| Console redirects to an internal host | RUSTFS_BROWSER_REDIRECT_URL unset or proxy headers wrong |
Set RUSTFS_BROWSER_REDIRECT_URL to the public origin. |
| Invalid or expired OIDC state | callback reached a different node | Configure load-balancer session affinity for authorize and callback. |
| Console session expires after about an hour | callback issues credentials with the default lifetime | Set RUSTFS_CONSOLE_OIDC_SESSION_DURATION to the desired lifetime in seconds (max 604800, 7 days) and restart RustFS. RUSTFS_CONSOLE_AUTH_TIMEOUT is a request timeout and does not extend the session. |
Production checklist
- RustFS and the IdP use HTTPS.
- The IdP registers the exact callback URL (no wildcard) and
RUSTFS_IDENTITY_OPENID_REDIRECT_URImatches it. RUSTFS_BROWSER_REDIRECT_URLis the public browser origin.- PKCE S256 is allowed or required at the IdP.
- ID tokens carry
groupsorrolesvalues equal to RustFS policy names. role_policyis not used as a permanent shortcut.- The load balancer preserves query strings and pins authorize/callback to one node.
- Internal IdP origins are listed exactly in
RUSTFS_OUTBOUND_ALLOW_ORIGINSon every node.
Keycloak
| Value | Example |
|---|---|
| Realm | rustfs |
Issuer (config_url) |
https://keycloak.example.com/realms/rustfs |
| Discovery URL | https://keycloak.example.com/realms/rustfs/.well-known/openid-configuration |
| Client id | rustfs-console |
| Scopes | openid,profile,email |
| Groups claim | groups (flat array) |
Client setup in the Keycloak Admin Console:
- Create or select the realm and confirm discovery returns
issuerequal tohttps://keycloak.example.com/realms/rustfs. Clients→ create:Client type=OpenID Connect,Client ID=rustfs-console.- Enable
Client authenticationandStandard flow; disableImplicit flow,Direct access grants, andService accounts roles. Valid redirect URIs=https://rustfs.example.com/rustfs/admin/v3/oidc/callback/default;Web origins=https://rustfs.example.com.Proof Key for Code Exchange Code Challenge Method=S256.- Save and copy the secret from
Credentials. Do not apply a client policy that disablesclient_secret_post.
Group mapper (a Group Membership mapper in the client's dedicated scope):
| Mapper field | Value |
|---|---|
| Name | rustfs-groups |
| Token Claim Name | groups |
| Full group path | Off (a leading / breaks policy matching) |
| Add to ID token / access token / userinfo | On |
| Multivalued | On |
Create Keycloak groups named after RustFS policies (consoleAdmin, readonly, ...) and add users to them.
Roles instead of groups: assign realm or client roles named after policies, add a User Realm Role or User Client Role mapper that emits a flat top-level roles claim, and set RUSTFS_IDENTITY_OPENID_ROLES_CLAIM=roles. RustFS does not read Keycloak's nested realm_access.roles claim.
Internal discovery URL with a public issuer (for example in-cluster Keycloak on Kubernetes):
export RUSTFS_IDENTITY_OPENID_CONFIG_URL="http://keycloak.keycloak.svc.cluster.local:8080/realms/rustfs"
export RUSTFS_IDENTITY_OPENID_ISSUER="https://keycloak.example.com/realms/rustfs"
export RUSTFS_OUTBOUND_ALLOW_ORIGINS="http://keycloak.keycloak.svc.cluster.local:8080"
Discovery and issuer-relative JWKS requests use the CONFIG_URL base; iss validation uses ISSUER. The allowlist entry is the origin only (no realm or discovery path) and is read at startup on every node. Prefer HTTPS with a trusted CA for the internal URL: discovery and JWKS define the token-signing trust root, so plain HTTP is acceptable only where DNS and traffic cannot be tampered with.
Authing
| Value | Example | Note |
|---|---|---|
| Application domain | https://example.authing.cn |
From the Authing application page. |
Issuer (config_url) |
https://example.authing.cn/oidc |
Tenants differ (/oidc, /oauth/oidc): copy the issuer from the console and confirm discovery returns the same issuer. |
| App ID / App Secret | <AUTHING_APP_ID> / <AUTHING_APP_SECRET> |
RustFS client_id / client_secret. |
| Scopes | openid,profile,email,roles |
roles is needed when Authing emits role claims. |
| Roles claim | roles |
Set RUSTFS_IDENTITY_OPENID_ROLES_CLAIM=roles. |
Application settings in the Authing console:
| Setting | Value |
|---|---|
| Protocol | OpenID Connect |
| Grant type / response type | Authorization Code / code |
| Token endpoint authentication | client_secret_post |
| PKCE | allow or require S256 |
| ID token signing algorithm | RS256 |
| Redirect URL | https://rustfs.example.com/rustfs/admin/v3/oidc/callback/default (exact) |
Assign Authing roles whose names equal RustFS policy names; a test user with role consoleAdmin should produce "roles": ["consoleAdmin"] in the ID token. claim_prefix only prepends a fixed string, so keep role values equal to policy names unless policies with that prefix already exist.