Guides: access & security
How to set up OAuth/OIDC single sign-onHow to set up OAuth/OIDC single sign-on
On this page
Let people sign in to the admin UI with the identity provider they already have, and land in the right IAM group automatically.
Prerequisites#
- The proxy reachable at a URL the identity provider can redirect to, for example
https://s3.acme.examplein production.http://localhost:9000works for testing against providers that accept localhost callbacks. In production, setadvanced.public_urlto that URL, so that the callback URL does not depend on theHostheader of a request (see the configuration reference). - Admin access to the UI (bootstrap password or an IAM admin like
dana). - An IAM group to map people into. This guide uses
Engineering(see How to create IAM users and groups).
1. Register the app with your provider#
Every provider needs the same redirect URL. The URL has no provider-name suffix, because one callback serves every provider:
https://s3.acme.example/_/api/admin/oauth/callback
If you use Google (Cloud Console):
- APIs & Services → Credentials → Create credentials → OAuth client ID.
- Application type: Web application.
- Authorized redirect URIs: add the callback URL above.
- Save, then copy the Client ID and Client Secret.
- The issuer URL is
https://accounts.google.com.
If you use Okta:
- Applications → Create App Integration → OIDC, Web Application.
- Sign-in redirect URIs: the callback URL above.
- Assignments: pick the Okta groups that should be allowed to log in.
- Copy the Client ID and Client Secret.
- The issuer URL is the URL of your Okta authorization server. For the default custom server it is
https://<your-org>.okta.com/oauth2/default; for the org server it ishttps://<your-org>.okta.com.
If you use Azure AD / Entra:
- App registrations → New registration; Redirect URI (Web): the callback URL above.
- Certificates & secrets → New client secret. Copy the value immediately, because Azure hides it on the next page load.
- API permissions → Microsoft Graph →
openid,profile,email; addGroupMember.Read.Allif you will map on AD groups. - Copy the Application (client) ID and the secret value.
- The issuer URL is
https://login.microsoftonline.com/<tenant-id>/v2.0, where<tenant-id>is the Directory (tenant) ID on the app's overview page.
If you use any other OIDC provider: it works as long as it serves .well-known/openid-configuration. Collect the issuer URL, client ID, client secret, and scopes (at minimum openid email; add profile and groups if you map on them).
2. Add the provider in the admin UI#
The proxy has one provider type, oidc. Google, Okta, and Azure AD have no type of their own: each one is an OpenID Connect issuer, so you add it as an oidc provider with its issuer URL. The proxy reads the .well-known/openid-configuration document of the issuer and takes the authorization, token, and key endpoints from it. The form sets the type to oidc for you. In the admin API and in declarative YAML, provider_type must be oidc. The proxy refuses a provider of any other type when you save it (422 in the admin API, a refused apply in declarative mode), because it has no sign-in flow for another type.
In the default IAM mode (gui), providers live in the encrypted config database, so the admin UI (or the admin API) is the place to add one. The YAML form for declarative mode is in The same change in YAML.
In the admin UI:
-
In the sidebar, open Access → External authentication (
/_/admin/access/external-auth), and click New provider at the top right of the page. -
Fill in the form. Display name is the label of the sign-in button (
Oktagives Sign in with Okta). Provider name is the unique name in URLs and in YAML (okta). Issuer URL, Client ID and Client secret come from step 1. Scopes needs at leastopenid email; addprofile, andgroupswhen you map on groups. Leave Enabled on. -
Click Create.


The form shows the Callback URL below Scopes, with a button that copies it. It is the URL of step 1. When advanced.public_url is set, the form shows the URL that the proxy builds from that value. Otherwise, the form shows the URL of the address that the page is open on, so open the page at the address that your users sign in from.
The form has no priority field. In the admin API and in YAML, priority sets the order of the buttons on the login page: a provider with a higher number is shown first.
Click Test connection, before or after you save. The proxy tests the values that are in the form at that moment and saves nothing. A blank client secret keeps the saved secret for the test. The proxy fetches the issuer's .well-known/openid-configuration and reports DNS, TLS, or connectivity problems before any person tries to log in. The error names the underlying cause, for example invalid peer certificate: UnknownIssuer for a certificate from a CA that the proxy does not trust. Test Connection also works in declarative IAM mode, because it changes nothing. In the admin API, POST /_/api/admin/ext-auth/providers/:id/test tests a saved provider, and an optional JSON body with the form fields replaces the saved values for that test only. POST /_/api/admin/ext-auth/providers/test tests a provider that is not saved yet. A test that fails answers 200 with success: false and the reason in error.
The proxy checks the issuer URL when you save the provider. By default the issuer must use https:// and a public address, because the proxy refuses requests to private, loopback, and cloud-metadata addresses. A provider that breaks this rule is refused with 422 and a message that names the rule. The client secret is never returned in a response: the API shows **** in its place.
An identity provider in a private network#
If your identity provider (for example Keycloak or Dex) runs on a private address, or uses a certificate from a private CA, set two keys in the provider's extra_config:
| Key | Value | Effect |
|---|---|---|
allow_local | true | The issuer, and the endpoints that its discovery document names, may use http:// and private addresses. This is the same opt-in as allow_local on backends and on event delivery. Cloud-metadata addresses stay refused. |
ca_cert_path | path to a PEM file | The certificates in the file are added to the trust roots for this provider. The proxy checks at save time that the file holds at least one certificate. |
In the admin UI, the provider form sets them in its Network fields: the Allow http:// and private addresses switch sets allow_local, and the CA certificate file field sets ca_cert_path. The form keeps the other keys of extra_config when you save. If the proxy refuses the provider with 422, the form shows the reason under the fields, and the provider stays as it was.
In the admin API, the provider body carries them as "extra_config": {"allow_local": true, "ca_cert_path": "/etc/deltaglider/idp-ca.pem"}. In declarative YAML they go under the provider entry:
# validate
access:
auth_providers:
- name: corp-keycloak
provider_type: oidc
client_id: deltaglider
client_secret: "${env:DGP_KEYCLOAK_SECRET}"
issuer_url: "https://keycloak.corp.internal/realms/acme"
scopes: "openid email profile"
extra_config:
allow_local: true
ca_cert_path: /etc/deltaglider/idp-ca.pem
The proxy does not use the operating system's certificate store for identity providers. It trusts the public web roots that are built into it, plus the file in ca_cert_path.
3. Map IdP groups to IAM groups#
Mapping rules decide which IAM groups an identity joins, based on its claims. Acme maps the Okta engineering group to the Engineering IAM group.
In the admin UI:
-
On Access → External authentication, click Add rule next to Allowed users and group assignment. A new, empty rule appears as a row. The proxy does not store it until you click Save rules, so if you leave the page first, the rule is gone.
-
Fill in the row. In Match type, select Claim value. Type
groupsin Claim field andengineeringin Match value. In Assign to group, selectEngineering. In Provider, selectOkta, or All providers. -
Click Save rules.


The match types are Email pattern (* matches any characters, for example *@acme.example), Email domain, Email exact, Email regex, and Claim value. A claim-value rule matches when the claim is a string equal to the value, or a list that contains the value, without regard to case. It does not support wildcards. Rules that read the email match only when the provider marks the email as verified.
The proxy checks every rule at each login, and the identity joins the group of every rule that matches. Rule order does not change the result.
Common claim fields: Google hd (hosted domain); Okta groups; Azure AD groups (UUIDs) or roles for app roles. For an email match, use one of the email match types.
Use the Preview box before you rely on a rule: type an email address and click Check, and the UI shows which groups that address would receive. Preview checks only the email: it cannot show the result of a claim-value rule. Group memberships are merged on each login, never replaced, so manual assignments survive SSO.
A login needs no matching rule. The proxy creates the user on the first login in any case, and a user that no rule matches has no group memberships and no permissions.
The same change in YAML#
In the default IAM mode (gui), providers and mapping rules live in the encrypted config database, so the admin UI and the admin API are the only ways to change them, and there is no YAML for steps 2 and 3. In declarative IAM mode, the file owns them, and the External authentication page is read-only (why IAM is the exception). In that mode, this access section declares the same provider and rule:
# validate
access:
iam_mode: declarative
iam_groups:
- name: Engineering
permissions:
- effect: Allow
actions: ["read", "list"]
resources: ["releases/*"]
auth_providers:
- name: okta
provider_type: oidc
display_name: Okta
client_id: 0oa1acmedeltaglider
client_secret: okta-client-secret-from-step-1
issuer_url: "https://acme.okta.com/oauth2/default"
scopes: "openid email profile groups"
group_mapping_rules:
- provider: okta
match_type: claim_value
match_field: groups
match_value: engineering
group: Engineering
Keep the client secret out of git with client_secret: "${env:OKTA_CLIENT_SECRET}". Apply the file with deltaglider_proxy config apply deltaglider_proxy.yaml --server https://s3.acme.example, as in How to manage IAM as code.
4. First login#
Open the login page in a private window. A Sign in with Okta button now appears. The credential fields move behind Sign in with credentials instead:


The screenshot shows the button only. It does not show a completed sign-in, because the example issuer is not a real Okta tenant.
Have dana click it. She authenticates at the provider, consents, and is redirected back. On success:
- A row appears under Login activity on the External authentication page, linking the provider's subject ID to a DeltaGlider user.
- The matching mapping rules apply, so
danais now a member ofEngineering. - She gets a session cookie and lands in the file browser. Only a user with admin permissions (direct or through a group) gets an admin session. Every other user gets a browser-only session, which opens the file browser with the user's own S3 permissions and refuses the admin API with
403. The bulk copy, move, delete, and ZIP actions of the file browser work in that session, under the same permissions.
Every successful OAuth login shows as external_login in Observability → Audit log. A failed login shows an error page in the browser and a line in the proxy log.
If the login fails#
These three failures come from the provider side:
invalid_redirect_uriat the provider. The registered URI does not matchhttps://s3.acme.example/_/api/admin/oauth/callbackbyte for byte. Check trailing slashes andhttpversushttps. If a reverse proxy fronts DeltaGlider, also confirm it forwards the sameHostheader the user sees. Whenadvanced.public_url(orDGP_PUBLIC_URL) is set, the proxy builds the callback URL from that value only, so the URL is always<public_url>/_/api/admin/oauth/callback. Without it, the proxy builds the callback URL from theHostheader. It usesX-Forwarded-HostandX-Forwarded-Protoonly whenDGP_TRUST_PROXY_HEADERS=trueand the request comes from a network inDGP_TRUSTED_PROXY_CIDRS, because otherwise any client could choose the host that receives the authorization code.- Azure
groupsclaim missing. Azure AD omits groups by default; in the app registration go to Token configuration → Add groups claim, then retry the flow. - "Code exchange failed" on the error page, or a failed Test connection. The proxy could not reach the provider. Read the error first: it names the cause (DNS, a refused connection, a refused private address, or a TLS error such as
UnknownIssuer). Acurlfrom the proxy container is not a reliable check.curltrusts the operating system's certificate store and connects to private addresses, but the proxy does neither unless you setca_cert_pathandallow_local(see An identity provider in a private network). Socurlcan succeed while the proxy fails. Ifcurlfails too, fix DNS, the network, or the certificate first, because the problem is not in OAuth.
For Google, the proxy asks for the account chooser (prompt=select_account), so a browser that is signed in to a personal and a work account asks which one to use. Without it, Google signs in with the browser's current account. To send another prompt value, or none, set prompt in the provider's extra_config (an empty string sends none). Other providers get no prompt unless you set one, because not every provider accepts select_account.
Three refusals come from the proxy itself. First, the proxy refuses the first login of an account that no mapping rule matches. Without this check, any account at the provider (for a Google provider, any Google account) could sign in and get a user row. The error page says "Access Denied", and the proxy creates no user. To create a user for every account that signs in, and to grant its permissions by hand later, set provision_unmapped: true in the provider's extra_config. Second, the proxy refuses a login whose email the provider does not mark as verified (an ID token without the email_verified claim counts as unverified), and the error page says so. To accept such logins, set require_email_verified: false in the provider's extra_config; email-based mapping rules still do not match such an identity. Third, after too many failed sign-ins from one address, the proxy locks that address out for a while. The authorize and callback pages then answer 429 with a Retry-After header, and the error page names the wait, for example "Try again in 10 min.".
If login succeeds but the user has no permissions, the matching rule targets a group that grants nothing, or an administrator removed the user from the group, or the user signed in before the refusal of unmatched accounts existed. Check with the Preview box, and verify the auto-created user row in Access → Users. If you rotate the client secret at the provider, update it in the provider form. The change takes effect when you save, with no restart.
Verify#
- Log in via the provider as a user in the mapped IdP group. Expect a session and membership of
Engineering. On Access → Users, the row of the user carries an SSO tag, and the form of the user listsEngineeringunder Groups & Inherited Access. - Log in as a user outside the mapped group. Expect a login that lands with no group memberships (or no login at all, if the provider restricts assignments).
- Confirm both attempts in the audit log.
Related#
- Authentication reference: auth modes, claim handling, error responses.
- How to create IAM users and groups: the groups your mapping rules target.
- How to manage IAM as code: providers and mapping rules can live in YAML too.
- About authentication and access control: how OAuth layers on bootstrap and IAM.
- Two ways to configure DeltaGlider: how the admin UI and the YAML file relate.