Authentication
Authentication is optional. When configured and enabled, S3 Lens uses OpenID Connect (OIDC) for identity, server-side sessions for requests, and typed policy documents for authorization.
Web and Electron share the Rust server’s OIDC code exchange, token validation, session store, and authorization policies. Both begin at /v1/auth/authorize and return through /v1/auth/callback. Web sessions use an HttpOnly cookie; Electron obtains a bearer session through an authorization code and PKCE.
When upgrading from the earlier /auth routes, change the identity provider’s registered redirect URI to /v1/auth/callback under the same public origin. Update any explicit oidc.redirect_uri and reverse-proxy auth routes as well. Existing sessions use the same store and credential format; no database migration is needed.
OIDC configuration
Section titled “OIDC configuration”auth: enabled: true public_origin: https://s3lens.example.com oidc: issuer: https://identity.example.com client_id: s3lens client_secret: ${S3LENS_OIDC_CLIENT_SECRET} scopes: [profile, groups] groups_claim: groups # Optional; these are the defaults derived from public_origin. redirect_uri: https://s3lens.example.com/v1/auth/callback rp_initiated_logout: true post_logout_redirect_uri: https://s3lens.example.com/Register the callback URI and the post-logout redirect URI with the identity provider:
https://s3lens.example.com/v1/auth/callbackhttps://s3lens.example.com/public_origin must be HTTPS, except that plain HTTP is accepted on a numeric loopback host for local development. It must not contain a path, query, fragment, or credentials.
oidc.redirect_uri is the callback the provider is asked to return to; it defaults to public_origin plus /v1/auth/callback. Set it only when the provider must see a different registered value, for example behind a proxy that rewrites paths; the server always serves the callback at /v1/auth/callback. oidc.post_logout_redirect_uri is where the provider returns a browser after sign-out and defaults to public_origin plus /. Both follow the public_origin rules but may carry a path. oidc.rp_initiated_logout: false makes sign-out revoke the S3 Lens session only, without visiting the provider; see Browser sessions for what that changes.
oidc.scopes lists the additional scopes requested from the identity provider. It defaults to profile and groups; openid is always implicit and does not need to be listed.
Configuring oidc.client_secret makes S3 Lens a confidential client and sends the secret through HTTP Basic authentication. Omitting it configures a public client and sends no secret. If the provider requires the secret in the token request body, set oidc.client_auth: post; basic is also accepted as an explicit value. A client_auth override without client_secret is rejected.
The provider’s discovery metadata must support the selected secret method. If discovery omits the supported-methods field, the OIDC default is Basic authentication. Other methods, including private_key_jwt, are unsupported.
OIDC HTTP requests have a five-second connection timeout and a fifteen-second total timeout per request. Discovery, signing-key retrieval, and code exchange do not follow HTTP redirects.
Session storage
Section titled “Session storage”auth.session selects where authenticated sessions live. Omitting it uses process-local memory with an eight-hour TTL (28800 seconds) and capacity for 4096 active sessions.
auth: session: type: database ttl_seconds: 28800 capacity: 4096Use type: memory for process-local sessions, type: database to keep sessions in the shared state database, or type: redis for an external Redis-protocol server — any RESP-compatible server works, including Valkey (pnpm run dev -- --with valkey starts one for local testing). Redis requires url, does not use a local capacity setting, and accepts environment substitution such as ${S3LENS_REDIS_URL:-redis://127.0.0.1:6379/0}. All backends default to the same eight-hour TTL; memory and database default to capacity 4096.
Policies and bindings
Section titled “Policies and bindings”Policies are native YAML documents in the policies sequence. A binding activates named policies when any issuer-scoped subject or verified group matches.
auth: enabled: true public_origin: https://s3lens.example.com oidc: issuer: https://identity.example.com client_id: s3lens client_secret: ${S3LENS_OIDC_CLIENT_SECRET} scopes: [profile, groups] groups_claim: groups policies: - version: 1 name: media-reader rules: - effect: allow actions: [providers:list] resources: - type: provider provider: prod - effect: allow actions: [buckets:list] resources: - type: bucket provider: prod bucket: media - effect: allow actions: [objects:list, objects:get] resources: - type: bucket provider: prod bucket: media bindings: - groups: [media-readers] policies: [media-reader] - subjects: - issuer: https://identity.example.com subject: 248289761001 policies: [media-reader]Authorization is deny by default. A matching deny in any granted policy overrides every allow; otherwise one matching allow permits the operation. No match denies the operation.
Resource selectors can target all resources, one Provider, one Bucket, an Object prefix, or one exact Object. Identifiers and Object keys are compared exactly; * has no wildcard meaning.
Browser sessions
Section titled “Browser sessions”Browser login begins at /v1/auth/authorize, which also sets a short-lived HTTP-only s3lens_login cookie that the callback must present. It ties the callback to the browser that started the login. A successful callback creates an HTTP-only s3lens_session cookie with SameSite=Lax; HTTPS origins also receive the Secure attribute. /v1/auth/session returns the current Principal, and POST /v1/auth/logout revokes the session.
The server captures the verified issuer, subject, and group claims at sign-in. These claims remain a snapshot for the lifetime of that S3 Lens session. The server does not refresh group membership or check whether the identity-provider account is still enabled on each request. Group changes and account disablement therefore do not immediately end an existing session. Session expiry or revocation ends access; signing in again obtains a new claim snapshot.
Sign-out follows OpenID Connect RP-Initiated Logout 1.0. POST /v1/auth/logout first revokes the S3 Lens session, then answers 200 with the provider’s end-session URL as endSessionUrl. The URL includes the session’s ID Token as id_token_hint, the client_id, and post_logout_redirect_uri. Register the post-logout URI with the provider. The browser navigates to this URL to ask the provider to end its own session; provider behavior determines whether confirmation is required. 204 No Content means sign-out ends at S3 Lens because provider logout is disabled, unavailable, or has an unsafe endpoint URL. Accepted endpoints use HTTPS, or HTTP on a numeric loopback host.
The session store holds credential digests and ID Tokens used for provider logout. ID Tokens carry user claims, so protect the state database and Redis server accordingly. The browser receives the ID Token inside the end-session URL, where it can also enter browser history or provider access logs. Do not log end-session URLs.
An ID Token can expire before its S3 Lens session. If the provider rejects the logout hint, its browser session can remain active even though the S3 Lens session has already been revoked. A later sign-in can then reuse that provider session. S3 Lens session expiry alone never signs the user out of the provider.
OIDC callbacks and their state values are single-use. Pending login state lives in server memory, so restarting the server invalidates unfinished logins. An already-completed callback returns 401 Unauthorized when replayed. Start a fresh sign-in instead of retrying a callback.
For browser credentials and WebSockets, the request Origin must match auth.public_origin. Transfer tickets are also restricted to the configured origin when authentication is enabled.
Desktop sessions
Section titled “Desktop sessions”Electron opens the system browser at /v1/auth/authorize with response_type=code, the built-in public client_id=s3lens-desktop, a random state, and an S256 code_challenge. Its registered redirect is /callback on http://127.0.0.1:<port> or http://[::1]:<port>, with an explicit nonzero port. No client secret belongs in Electron. This follows the external-browser and loopback patterns in OAuth 2.0 for Native Apps.
The browser completes the same upstream OIDC flow and must present the same login-binding cookie as web login. The server then returns a short-lived authorization code and the original state to Electron’s loopback listener. Electron verifies state and posts grant_type=authorization_code, client_id, redirect_uri, code, and code_verifier as form data to /v1/auth/token. The code expires after 60 seconds and can be used once; the client, exact redirect, and S256 proof must match before a session is issued.
The token response contains access_token, token_type: "Bearer", and expires_in. This access token is an opaque S3 Lens session credential. Electron keeps it in the main process, using the platform’s encrypted credential storage when available. The renderer receives the verified Principal through a sender-bound IPC method and uses the main process’s authenticated socket. Both platforms use /v1/auth/session and /v1/auth/logout; browser and desktop sessions are independently revocable.
This is a built-in first-party authorization-code flow, not a general-purpose OIDC issuer or third-party OAuth authorization server. S3 Lens continues to rely on the configured identity provider for user authentication. API Keys retain their separate scoped application-credential lifecycle.
Using the web UI
Section titled “Using the web UI”The browser checks /v1/auth/session before it creates its RPC connection or loads a route. Signed-out users see a sign-in screen; after the OIDC flow completes, S3 Lens returns them to the same relative path. The session menu shows the current user’s name or subject, their issuer, and a Sign out action. When provider logout is enabled and available, sign-out also navigates to the provider’s end-session URL.
If a session expires or is revoked, the active RPC connection closes and the web UI returns to the sign-in screen. Signing in again returns to the current path and reuses the identity provider’s session when it has one. An authorization denial is shown as an access-denied state; retrying does not grant additional permissions.
A session-store or network failure shows a session-check error, not a signed-out state. A failed or lost logout response leaves sign-out unconfirmed. In either case, the UI hides account data and offers Check session before continuing.
Test authentication locally
Section titled “Test authentication locally”The development runner starts the included Keycloak fixture next to Garage, waits for the realm to import, and applies examples/keycloak/s3lens.auth.yaml as an authentication overlay on top of the fixture Providers:
pnpm run dev -- --authOpen the printed web address and select Sign in. Add more stores with --with seaweedfs,rustfs; operations succeed only for a Provider whose fixture is running. The overlay also enables the API Key registry in the development database.
To switch users, select Sign out in the signed-in popover: S3 Lens revokes its session and sends the browser through Keycloak’s logout endpoint, which ends the Keycloak session and returns to S3 Lens, so the next Sign in shows Keycloak’s login form. Reloading the page or letting the session expire does not end the Keycloak session; Keycloak signs the same user in again without a prompt.
The realm has one user per authorization shape; every password is password. Each is bound to a policy in the overlay by group, except developer, which is bound by subject id so both binding forms are exercised:
| User | Groups | Can |
|---|---|---|
developer |
s3lens-developers |
Everything (full-access, bound by subject) |
admin |
s3lens-admins |
Everything (full-access, bound by group) |
reader |
s3lens-readers |
Browse and download everywhere; every write is forbidden |
uploader |
s3lens-uploaders |
Reader, plus put/delete/tag under uploads/ in Garage’s s3lens bucket only |
auditor |
s3lens-auditors |
Reader, plus versions, multipart and policy inspection; denied any Object under private/ |
contractor |
s3lens-readers, s3lens-uploaders |
The union of both groups’ grants |
nobody |
none | Signs in, matches no binding: every listing is empty and every action is forbidden |
disabled |
s3lens-readers, disabled |
Cannot sign in; Keycloak rejects the credentials |
auditor shows deny overriding allow: read-only grants objects:get everywhere, and the auditor policy denies it under private/, so the Object appears in listings but cannot be fetched. The realm console at http://127.0.0.1:5556/admin/ (admin / admin) is the place to add users or move them between groups; the group membership mapper emits plain group names, so bindings use s3lens-readers, not /s3lens-readers. Garage does not implement ListObjectVersions, so auditor’s version listing needs another fixture such as SeaweedFS.
The realm registers a portless loopback callback and post-logout redirect URI, so worktrees and named instances on other ports work unchanged. Ctrl-C stops the development stack; the fixtures keep running until pnpm run dev -- down. Keycloak imports the realm only when its container is created, so after editing the realm file run pnpm run dev -- reset keycloak.
API Keys for applications
Section titled “API Keys for applications”Enable the durable API Key registry inside auth to let interactive sessions create expiring API Keys through the API Key RPC methods:
auth: # public_origin, oidc, policies, and bindings omitted here api_keys: capacity: 256API Keys are stored in the shared state database selected by state.path. S3 Lens persists only a SHA-256 verifier, never the issued secret. The complete s3l_ak_… credential is shown once at creation.
Send the credential as a bearer token to the JSON-RPC endpoint:
curl --fail \ --header 'authorization: Bearer s3l_ak_REPLACE_WITH_THE_ISSUED_SECRET' \ --header 'content-type: application/json' \ --data '{"jsonrpc":"2.0","id":1,"method":"provider/list","params":{}}' \ https://s3lens.example.com/rpcThe command line does the same with s3lens api provider list --server https://s3lens.example.com --credential s3l_ak_…, or with S3LENS_SERVER and S3LENS_CREDENTIAL set.
Every request must pass two independent checks: the creator’s configured S3 Lens grants and the key’s allow-only scope. A key therefore cannot gain rights its creator did not have, cannot create or manage other keys, and stops authenticating immediately after revocation or expiration. An interactive session can replace the key’s scope without changing its credential. Rotating a key replaces its secret while keeping its public ID, description, scope, and expiration; the previous credential stops working as soon as rotation succeeds. Policy and binding changes take effect after the server is restarted. The creator’s issuer, subject, and verified group claims are captured when the key is created; S3 Lens does not contact the identity provider on each key request, so revoke keys when those claims or group memberships change.
The initialize result advertises capabilities.apiKeys. API Key management methods are available only when that value is true; otherwise clients should hide the feature.
These credentials are scoped S3 Lens API Keys. They are not OAuth access tokens issued by the identity provider, and sharing the Bearer header format does not make them interchangeable. S3 Lens does not accept an identity provider’s access token as an API credential. API Keys also differ from S3 access keys and do not implement AWS Signature Version 4.
Deployment checklist
Section titled “Deployment checklist”- Terminate TLS for every non-loopback deployment.
- Configure one exact public origin and OIDC callback URI.
- Use a dedicated OIDC client and keep its secret outside committed YAML.
- Bind only the policies a subject or group needs.
- Keep the shared state database private, durable, and backed up according to your credential-recovery policy.
- Protect the session store like credential material; it holds each session’s ID Token and the claims inside it.
- Keep the server and reverse proxy logs free of cookies, bearer credentials, transfer-ticket URLs, and end-session URLs.
- Restart the server after changing policies or bindings.