OIDC Authentication

OpenID Connect (OIDC) can be used for external authentication of users logging in to the Deephaven Web interface.

OIDC is an open standard, built on top of OAuth 2.0, whereby third-party Identity Providers such as Google, Okta, Keycloak, Auth0, and others can validate the identity of a user requesting to log on to a Deephaven system. The main benefits of OIDC authentication are single sign-on to multiple services, including Deephaven, and the possibility to centralize multi-factor authentication at the Identity Provider.

There are two systems involved in authentication using OIDC: the Identity Provider (IdP) and the application it authenticates users for, which the standard calls the Relying Party. The Identity Provider is responsible for authenticating a user and providing evidence of that user's identity, in the form of a signed token called an id_token. In the context of this document, the Relying Party is the Deephaven installation — specifically, a module hosted inside the Deephaven authentication server.

Note

The authenticated user is the user who authenticated to the system to initiate the operation. The effective user is the user for whom Deephaven enforces permission checks. A simple example is the "Operate as" login, where the user who logs in (via password, OIDC, etc.) is the Authenticated User, and the user they are operating as is the Effective User.

The diagram below shows how the pieces interact during a login:

Both systems need to be configured: the Identity Provider so that it recognizes the Deephaven installation as a registered application and will send responses back to it, and Deephaven so that it knows which Identity Provider to work with and can verify the tokens it receives.

Once both systems are properly configured, users attempting to log in to Deephaven can choose to authenticate with the external Identity Provider. If they do so, they are redirected to a login process managed by the Identity Provider. Upon completion of that process, the Identity Provider redirects the browser back to the Deephaven authentication server with a short-lived authorization code, which Deephaven exchanges for the user's id_token. If the login was successful, the authentication server grants the user the rights associated with the user name found in that token.

For a user to access Deephaven with OIDC authentication, the Identity Provider administrator must create a login in the Identity Provider system and grant the user access to the Deephaven application there. In addition, a Deephaven administrator must either create a Deephaven login matching the user's name as the Identity Provider reports it, or configure group synchronization, which creates Deephaven users automatically on first login.

Note

OIDC login is available in the Deephaven Web interface only. There is no OIDC login for the Legacy Deephaven Classic (Java/Swing) UI.

OIDC and SAML are independent and may be enabled at the same time. When both are configured, the Web login screen shows a button for each provider (plus Basic login, if enabled).

The OIDC module is provider-agnostic. It reads the provider's endpoints from a standard discovery document published by the provider, and uses the standard Authorization Code flow with PKCE, so any OIDC-compliant Identity Provider works by changing only configuration values.

How it works

The OIDC module runs an embedded Jetty web server inside the Deephaven authentication server (by default on port 9033, under the path /dh-oidc). A login proceeds as follows:

  1. From the Deephaven login screen, the browser is sent to the module's login URL, which builds a login request and forwards the browser to the Identity Provider.
  2. The user authenticates at the Identity Provider, which redirects the browser back to the module's callback URL (/dh-oidc/cb) with a short-lived authorization code.
  3. The module exchanges that code for tokens, verifies the id_token's signature and contents, and reads a configurable field from it — a claim — to determine the Deephaven user name. The default claim is preferred_username.
  4. The user name is returned to the Deephaven login, which grants the user the rights associated with that name.

Important

Port 9033 must be reachable — it is distinct from the Web UI's port, and login fails without it. Behind a proxy, the proxy must reach port 9033 on each authentication server; browsers only need the proxy's port. Without a proxy, browsers must reach the authentication server on port 9033 directly, both to start the login and to deliver the Identity Provider's callback.

Note

The claim used for the user name must resolve to a valid Deephaven user name. For example, Okta's preferred_username is typically the user's email address, so either align your Deephaven user names with the Identity Provider login, set authentication.oidcauth.usernameClaim to a claim that does match, or enable group synchronization so that Deephaven users are created automatically.

Configure Deephaven to use OIDC authentication

To configure the authentication server to present the option of OIDC authentication, two sets of properties need to be added to the iris-environment.prop properties: one for front-end configuration and one for server-level configuration.

Configure front-end

The front-end must be configured to offer the OIDC login option. These properties are defined within the following stanza in the iris-environment.prop file:

Because there is no OIDC login panel for the Classic (Java/Swing) UI, no authentication.client.customlogin.class entry is used.

Note

authentication.client.configuration.list is a single property. If you also enable SAML, combine both providers' keys into one comma-separated value rather than letting one setting overwrite the other.

Enable OIDC-based auth requests

The second set enables the authentication server to accept and handle OIDC-based authentication requests. These properties should be added to the authentication server stanza:

Example configuration

The following are minimal, working configurations for three common Identity Providers. Each replaces only the Identity Provider section of the stanza above; the front-end properties are the same in every case.

Minimal Okta-IdP example

An Okta Integrator Free Plan organization ships with a pre-configured custom authorization server named default. Use it rather than the bare organization server, because only the custom server can emit the group claims that group synchronization needs.

In the Okta Admin Console, create the application with Applications → Create App Integration → OIDC – OpenID Connect → Web Application, set its Sign-in redirect URI to the redirectUri below, and assign the application to the users and groups permitted to log in. Unassigned users cannot complete the login.

The last two properties below enable group synchronization; omit them if you do not want it.

Minimal Keycloak-IdP example

In Keycloak, create a realm and a client with Client authentication on and Standard flow enabled, and set its valid redirect URI to the redirectUri below.

The last two properties below enable group synchronization; omit them if you do not want it.

Minimal Google-IdP example

Google's issuer is fixed at https://accounts.google.com. Create the client in the Google Cloud Console under APIs & Services → Credentials → Create credentials → OAuth client ID → Web application, and set its authorized redirect URI to the redirectUri below.

Google does not include group membership in the id_token, so group synchronization needs the google source described under Google Workspace groups. This is configured by the last two properties below; omit them if you do not want group synchronization.

OIDC over Envoy

The examples above assume the browser reaches the auth-server Jetty directly on port 9033. When Deephaven is fronted by the Envoy proxy, use OIDC authentication through the proxy by leveraging Extra Envoy XDS routes. In addition to changing the target port from 9033 to the appropriate Envoy port in authentication.oidcauth.redirectUri and authentication.client.oidcauth.login.url, the Identity Provider must be configured with the Envoy-based redirect URI. The following should be added so that appropriate routing is done for the OIDC URLs:

For Kubernetes, this wiring is generated for you from Helm values — see OIDC in a Kubernetes installation.

Choosing the keystore

OidcAuth.tls.keystore and OidcAuth.tls.passphrase.file name the certificate the OIDC module's embedded Jetty presents. They default to the authentication server's own keystore, which every authentication server has and whose certificate carries that machine's hostname, so they do not normally need to be set.

Set them only to give the module a different certificate than the authentication server's own — for example one from a public CA, for a deployment where browsers reach the Jetty directly and must trust it without your installation's CA:

Group synchronization

Group synchronization lets your identity provider decide what each person can do in Deephaven, rather than an administrator setting that up by hand for every user.

In Deephaven, permissions are attached to groups rather than to individual people, and the groups you define control which data each person can see. Most identity providers can also report which groups a user belongs to. Group synchronization connects the two: at each login, Deephaven asks the provider which groups the user is in and brings their Deephaven groups into line.

The practical effect is that access is granted and revoked in one place. Adding someone to your "QA Team" group at the provider gives them the matching Deephaven access at their next login, and removing them there takes it away again.

What happens at each login. After the user's name has been determined:

  1. Deephaven fetches the list of groups the provider reports for that user.
  2. Any names listed in sync.ignoregroups.idp are discarded.
  3. The remaining names are translated through the sync.mapgroup.* entries you configured. A name with no mapping passes through unchanged.
  4. If the user does not yet exist in Deephaven, they are created. Every Deephaven user belongs to two groups automatically: allusers, and a group named after the user.
  5. The resulting groups are added to the user, and any Deephaven groups they hold that are no longer in that list are removed.

Step 4 is why group synchronization is usually worth enabling for OIDC: without it, every user must be created in Deephaven ahead of time or their login fails.

Important

Step 5 works in both directions. Deephaven treats the identity provider as the source of truth, so a group you grant by hand in the ACL editor is taken away at that user's next login unless you list it in authentication.oidcauth.sync.ignoregroups.dh. If you intend to manage some groups inside Deephaven, put them on that list.

A worked example. Suppose the provider reports that Dana belongs to QATeam and Everyone, and you configure:

At Dana's next login Deephaven discards Everyone, translates QATeam into the two Deephaven groups, and leaves Dana in dana, allusers, iris-superusers, and iris-schemamanagers — creating the Deephaven account first if it did not exist. If Dana later leaves QATeam at the provider, those two groups are removed at the following login.

authentication.oidcauth.groups.source determines where the group list comes from:

SourceWhere groups come fromRequires
claimThe id_token Deephaven already received during login.groupsClaim. No credentials and no extra network call.
userinfoThe provider's userinfo_endpoint, as advertised in its discovery document, queried separately.groupsClaim. One extra call per lookup; no credentials.
googleGoogle's Cloud Identity Groups API, queried per user.Provider-specific setup — see Google Workspace groups.

claim and userinfo differ only in where they look. claim reads the groups out of the id_token Deephaven already holds from the login exchange, so it costs nothing extra. userinfo makes a separate request to the provider's UserInfo endpoint — the userinfo_endpoint value in the provider's .well-known/openid-configuration discovery document — and reads the groups from that response instead. Use userinfo when your provider reports groups there but not in the token, which some do to keep tokens small.

Either way, the groups arrive in a named field, and authentication.oidcauth.groupsClaim is how you tell Deephaven which field to read. Providers commonly name it groups, so groupsClaim = groups is the usual setting, but the name is whatever your provider emits.

Most providers do not send groups at all unless configured to, and how you turn that on differs by provider — consult your identity provider's documentation for how to include group membership in the id_token or the UserInfo response, and what it names the field. Google is a special case: it reports groups in neither place, so it needs the google source instead.

Use claim unless your provider cannot supply groups that way — it is the simplest option and behaves identically everywhere. Group synchronization is off until groups.source is set; there is no default source.

An installation can also supply its own source in a jar on the authentication server's classpath and select it by name here, for an identity provider Deephaven does not know about or to adjust what a built-in source returns.

The three properties in detail, all optional:

  • Groups that are in the authentication.oidcauth.sync.ignoregroups.idp list are ignored when adding new groups to the Deephaven user. For example, you may want to exclude an "Everyone" group that is included by default with your provider (as Deephaven already includes an "allusers" group).
  • Groups that are in the authentication.oidcauth.sync.ignoregroups.dh list are not deleted by Deephaven. This allows you to have some groups that are managed solely within Deephaven.
  • Each identity provider group can be mapped to one or more Deephaven groups with properties of the form authentication.oidcauth.sync.mapgroup.<idp group name>=<dh group name>. This permits you to map identity provider groups to special Deephaven groups like "iris-acleditors" or "iris-superusers".

A provider group with no mapgroup entry passes through under its own name, creating a Deephaven group with that name.

Important

Group names containing spaces need escaping. sync.mapgroup.<idp group name> puts the group name in the property key, where an unescaped space ends the key. Escape it with a backslash:

sync.ignoregroups.idp is a comma-separated value, so spaces there need no escaping.

Note

Consult the documentation of your Identity provider to configure sending group information.

Google Workspace groups (the google source)

Google Workspace does not report group membership in the id_token, and does not report it from the UserInfo endpoint either, so neither claim nor userinfo can work with it. The google source instead reads the user's groups from the Cloud Identity Groups API, using the access token the user's own login already produced — there is no service account, no domain-wide delegation, and no Google administrator involved in the setup.

Note

The Admin SDK Directory API is not used: it requires administrator privilege even for a user to read their own groups.

To enable it:

  1. Enable the Cloud Identity API in the Google Cloud project that owns your OIDC client. Google charges the API call against that project, and calls fail without it.

  2. Ask for permission to read groups when the user logs in. This can only be requested at login, so it must be part of scopes rather than added afterwards.

  3. Select the source, and make sure the Deephaven user name is the user's email address, which is how Google identifies group members.

Each user is asked to approve the additional permission at their first login after this change.

Naming groups. Groups may be named either by display name (QATeam, All MyCompany) or by group email address (qateam@example.com) in sync.mapgroup.* and sync.ignoregroups.idp; whichever form you use is matched. Display names are more readable, so they are the normal choice, and they are what an unmapped group passes through as. They are neither unique nor stable, though: two groups may share one, and renaming a group in Google silently stops it matching. Use the email address for groups where that matters.

Nested groups. By default only direct memberships are synchronized. To also count membership a user inherits by belonging to a group that is itself a member of another group:

Each group named in sync.mapgroup.* that is not already a direct membership is then checked individually, so the added cost is bounded by how many groups you map rather than by the size of your directory. This is off by default because installations that do not nest groups should not pay for those checks. groups.google.customer is needed only to resolve display names to groups; find it with gcloud organizations list, which reports it as DIRECTORY_CUSTOMER_ID. If every group in sync.mapgroup.* is named by email address, it can be omitted.

When membership cannot be determined

A failed lookup is not the same as a user having no groups, and the two are handled differently:

  • The provider reported no groups. Synchronization runs with an empty list, so any Deephaven groups the user has lost are removed. This is the normal path for a user who was removed from every group.
  • Membership could not be determined — the user declined the permission prompt, the provider returned an error, or the configured claim was missing. Synchronization is skipped and the user's existing Deephaven groups are left untouched, because acting on an unknown result would remove every group they have. The reason is written to the authentication server log.

In both cases the login itself succeeds. A group-synchronization problem never blocks a user from logging in.

Results are cached per user for authentication.oidcauth.groups.cache.ttlseconds (default 300). The cache is per authentication-server process, so with multiple replicas each one performs its own first lookup for a given user. Only successful results are cached, so a transient failure is retried on the next login rather than being held for the whole cache lifetime.

Disabling password authentication

After enabling OIDC authentication, password authentication may be disabled by setting the following property in iris-environment.prop and restarting the authentication server:

When set to false, the authentication server does not accept any password authentication. You must use alternative methods like OIDC, SAML, or Deephaven challenge response authentication with private keys.

The password authentication option on the web front-end may be removed by adding the following property into the existing client configuration list and reloading the web API service:

Additional OIDC settings

The previous sections provide minimal configurations for some common public Identity Providers. More settings are available, which we detail here:

PropertyDefaultDescription
authentication.oidcauth.issuer(required)IdP issuer URL; discovery reads <issuer>/.well-known/openid-configuration.
authentication.oidcauth.clientId(required)The OAuth client ID registered at the IdP.
authentication.oidcauth.clientSecret / .file—Client secret (inline, or read from a file — prefer the file). Omit for a public client.
authentication.oidcauth.redirectUri(required for login)The /dh-oidc/cb callback URL, registered at the IdP.
authentication.oidcauth.scopesopenid profile emailSpace-separated scopes; must include openid.
authentication.oidcauth.usernameClaimpreferred_usernameid_token claim mapped to the Deephaven user (falls back to sub).
authentication.oidcauth.groupsClaim—Name of the field in the provider's response holding the user's groups, commonly groups. Required by the claim and userinfo sources. See Group synchronization.
authentication.oidcauth.groups.source—Which group source to use: claim, userinfo, google, or the name of one supplied on the classpath. Unset means group synchronization is off.
authentication.oidcauth.groups.cache.ttlseconds300How long a user's fetched group list is reused. 0 disables caching. Only successful lookups are cached.
authentication.oidcauth.groups.google.transitivefalseAlso count membership inherited through nested groups, by checking each mapped group individually.
authentication.oidcauth.groups.google.customer—Cloud Identity customer (customers/C0xxxxxxx), required by groups.google.transitive when groups are named by display name rather than email.
authentication.oidcauth.sync.aclwriteuseririsThe Deephaven user the module writes ACL changes as during group synchronization.
authentication.oidcauth.clientAuthMethodclient_secret_basicToken-endpoint client auth; also client_secret_post.
authentication.oidcauth.jwsAlgorithmRS256Expected id_token signing algorithm. Asymmetric only (RSA/EC/ED); HMAC and none are rejected.
authentication.oidcauth.requireHttpstrueRequire https on provider URLs (discovery/token/JWKS).
authentication.oidcauth.allowedHosts—Optional comma-separated host allow-list for provider URLs.
authentication.oidcauth.clockskewseconds60Permitted clock skew when validating exp/iat.
authentication.oidcauth.discovery.url—Explicit discovery-document URL, overriding the issuer-derived default.
authentication.oidcauth.endpoint.authorization / .token / .jwks—Manual endpoint overrides used when discovery is disabled/unavailable (all three required together).
authentication.oidcauth.jetty.port9033Port for the module's embedded Jetty server.
authentication.oidcauth.jetty.sniHostCheckfalseWhether Jetty rejects a TLS SNI host that does not match its certificate. Leave off behind a proxy: the proxy forwards the browser's external hostname, which no internally-issued certificate covers, so enabling it fails every login with 400 Invalid SNI.
authentication.oidcauth.jetty.context/dh-oidcContext path for the module's servlets.
authentication.oidcauth.jetty.redirect.list—Allow-list of Web UI URLs the login flow may redirect back to.
authentication.oidcauth.http.connectTimeoutMillis / .readTimeoutMillis5000Timeouts for provider HTTP calls.
authentication.oidcauth.contacts.support.given_name / .email_address—Support contact shown on the login-failure page.
authentication.federatedauth.cookie.ttlmillis300000Lifetime of the encrypted login-state cookie — how long the user has to complete the IdP login before the flow expires. Shared with SAML.
OidcAuth.tls.keystore/etc/sysconfig/deephaven/auth/keystore.authserver.p12Keystore holding the certificate the module's embedded Jetty presents. See Choosing the keystore.
OidcAuth.tls.passphrase.file/etc/sysconfig/deephaven/auth/.auth_passphraseFile holding the passphrase for that keystore.

Note that the last two property names are different from the others: they begin with OidcAuth., not authentication.oidcauth.. A property written as authentication.oidcauth.tls.keystore is ignored without any warning.

Other references