Skip to main content

Authentication

Authentication decides who a caller is. Datastrato Enterprise offers two mechanisms: Basic, which verifies a user name and password against local accounts the server itself holds, and OAuth, which validates a JWT issued by your identity provider. Both can run at once, and most production deployments do exactly that: OAuth for people and engines, Basic kept alongside it so the local admin account remains a way in when the identity provider is unavailable.

Every server-side setting on this page goes in my-values.yaml and takes effect when you rerun the install command from Install. Nothing here needs a configuration file on the server or a restart of your own.

simple is not supported. It performs no authentication and accepts any user name without a password, and the chart refuses to render when gravitino.authenticators lists it.

Quick start​

The example turns on OAuth with JWKS validation, keeps Basic for the local admin account, and calls the API with a machine token. It assumes a confidential client registered with your identity provider that may use the client credentials grant.

  1. Collect the provider details. You need the issuer, the JWKS endpoint, the token endpoint, and the audience the provider puts in the aud claim of the tokens it issues to your client. Most providers list the first three in their discovery document at {issuer}/.well-known/openid-configuration.

  2. Add the OAuth values. Put the details in my-values.yaml.

    my-values.yaml
    gravitino:
    authenticators: "oauth,basic"
    authenticator:
    oauth:
    tokenValidatorClass: "org.apache.gravitino.server.authentication.JwksTokenValidator"
    jwksUri: "{jwks_uri}"
    authority: "{issuer}"
    serviceAudience: "{audience}"
    principalFields: "preferred_username,email,sub"
  3. Apply the values. Rerun the install command from Install.

    Apply the values
    helm upgrade --install "$RELEASE" "./enterprise-helm-$VERSION.tgz" -n "$NAMESPACE" -f my-values.yaml \
    --set-string gravitino.license.key="$(tr -d '\n' < "$LICENSE_FILE")" \
    --timeout 15m
  4. Get a token. Request an access token with the client credentials grant.

    Get an access token
    TOKEN=$(curl -sS "{token_endpoint}" \
    -d "grant_type=client_credentials" \
    -d "client_id={client_id}" \
    -d "client_secret={client_secret}" \
    | jq -r .access_token)
  5. Call the API. List the metalakes with the token.

    Call the API with the token
    curl -sS "https://{gravitino_host}/api/metalakes" \
    -H "Accept: application/vnd.gravitino.v1+json" \
    -H "Authorization: Bearer $TOKEN"

    A 401 response means the token failed validation. Decode it and compare its aud and iss claims with serviceAudience and authority, which the server matches exactly.

For browser sign-in to the UI, add Browser sign-in for the UI. For a step-by-step provider setup, see Configure Keycloak as an OAuth provider.

Identity provider tab​

Configure → Identity provider in the UI shows the authentication setup of the server. It is read-only; every value on it comes from my-values.yaml.

The Authentication section ("Applies to every metalake on this server.") has two rows. Each row shows its current value, the configuration key behind it, and a View link that opens Configure → Settings, where the full server configuration is listed.

RowConfiguration keyValue shown
Issuergravitino.authenticator.oauth.authorityThe issuer the server checks in API tokens, or "Not enabled" when it is unset
SCIM servicegravitino.scim.httpPortThe SCIM address discovered from the ingress, "Address unknown" when none is found, or "Not enabled"

The Issuer row shows the token-validation issuer, not gravitino.browserSso.issuer. The two can differ, as the Azure AD example shows.

When SCIM is enabled, two more sections appear: SCIM provisioning, with the Endpoint, a Copy endpoint button, and Last activity, and SCIM tokens, where you add and delete the tokens your identity provider uses. Both are covered in SCIM provisioning.

Basic mode​

In Basic mode the server verifies HTTP Basic credentials against local accounts held in its metadata database.

my-values.yaml
gravitino:
authenticators: "basic"
authorization:
serviceAdmins: admin
admin:
initPassword: "{admin_password}"

The chart does two things for you when basic is listed, so they are not steps to carry out. The local-account REST endpoints are registered automatically: the chart appends org.apache.gravitino.idp.web.rest.feature to the server's REST extension packages. The initial administrator password is delivered automatically: the chart puts the first name in gravitino.authorization.serviceAdmins and gravitino.admin.initPassword in a Secret named {release}-gravitino-admin-credentials and hands the password to the server, generating a 24-character random password when you leave gravitino.admin.initPassword empty. The password must be 12 to 64 characters. It applies at the first install only; on an upgrade the chart keeps the existing Secret, so change the password through the UI or the API.

At startup the server creates a local account, with that initial password, for every name in gravitino.authorization.serviceAdmins that has no account yet.

Read the generated admin password
kubectl -n "$NAMESPACE" get secret "$RELEASE-gravitino-admin-credentials" \
-o jsonpath='{.data.password}' | base64 -d

To keep the password out of the values file altogether, create that Secret yourself with username and password keys and name it in gravitino.admin.existingSecret. The chart carries a helm.sh/resource-policy: keep annotation on the Secret it creates and reuses an existing one of the same name, so the password survives upgrades and uninstalls either way.

Passwords must be 12 to 64 characters. For creating further local users and groups, see Directory.

The password is set once, at first install. Later installs leave an existing password alone, so changing gravitino.admin.initPassword afterwards does not reset it. Reset a password through the local account endpoints instead, as Directory describes.

For the client side, enable Basic mode:

Connect with Basic authentication
GravitinoClient client = GravitinoClient.builder("https://{gravitino_host}")
.withMetalake("metalake")
.withBasicAuth("admin", "{admin_password}")
.build();
Connect with Basic authentication
from gravitino.auth.basic_auth_provider import BasicAuthProvider
from gravitino.client.gravitino_client import GravitinoClient

client = GravitinoClient(
uri="https://{gravitino_host}",
metalake_name="metalake",
auth_data_provider=BasicAuthProvider("admin", "{admin_password}"),
)
Call the API with Basic authentication
curl -v -X GET \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Authorization: Basic $(echo -n 'admin:{admin_password}' | base64)" \
https://{gravitino_host}/api/version

When basic is listed, the UI sign-in page offers a user name and password form backed by local accounts. See Sign-in page.

OAuth mode​

Gravitino supports external OAuth servers with two token validation methods:

Static signing key validation uses a pre-configured signing key to validate JWT tokens.

JWKS-based validation fetches public keys from the provider's JWKS endpoint as needed, and covers OIDC providers such as Azure AD along with other JWKS-compatible providers.

The values below enable OAuth. Set gravitino.authenticator.oauth.serviceAudience to the audience your provider puts in its tokens; its default is test, which no real provider issues.

my-values.yaml
gravitino:
authenticators: "oauth,basic"
authenticator:
oauth:
serviceAudience: "{audience}"
tokenValidatorClass: "org.apache.gravitino.server.authentication.JwksTokenValidator"
jwksUri: "https://{oauth_host}/.well-known/jwks.json"
authority: "https://{oauth_host}"
principalFields: "preferred_username,email,sub"

Listing basic alongside oauth keeps the local admin account usable for recovery. A request accepted by more than one authenticator uses the first listed that claims it.

For static key validation, set defaultSignKey, serverUri, and tokenPath instead of jwksUri, with tokenValidatorClass set to org.apache.gravitino.server.authentication.StaticSignKeyValidator.

For JWKS validation, set jwksUri and tokenValidatorClass to org.apache.gravitino.server.authentication.JwksTokenValidator. With JWKS validation the static-key settings are unnecessary, since the validator fetches public keys from the provider. The validator checks the signature, requires aud to contain serviceAudience, and, when authority is set, requires iss to equal authority exactly. Leaving authority blank skips the issuer check.

A request from a client or engine carrying a token follows this path:

1. The client requests a token from the identity provider. 2. The identity provider returns a JWT. 3. The client sends the request with the JWT as a bearer token. 4. The server fetches the signing keys. 5. The identity provider returns its public keys. 6. The server checks signature, audience, and issuer. 7. The server maps the principal. 8. The server responds

The server fetches keys only as needed, so steps 4 and 5 do not repeat on every request. Static signing key validation skips them entirely.

Where the JWKS endpoint presents a certificate from a private certificate authority, import the CA so the server can reach it:

my-values.yaml
gravitino:
ssl:
trustStore:
enabled: true
existingSecret: "{ca_secret}"

For the client side, enable OAuth mode:

Connect with an OAuth token provider
DefaultOAuth2TokenProvider authDataProvider = DefaultOAuth2TokenProvider.builder()
.withUri("https://{oauth_host}")
.withCredential("{client_id}:{client_secret}")
.withPath("{token_path}")
.withScope("{scope}")
.build();

GravitinoClient client = GravitinoClient.builder("https://{gravitino_host}")
.withMetalake("metalake")
.withOAuth(authDataProvider)
.build();

Browser sign-in for the UI​

Signing in to the UI through your identity provider is configured separately from token validation, under gravitino.browserSso. Set issuer, clientId, and clientSecret together; the chart refuses to render when only some of the three are present.

my-values.yaml
gravitino:
browserSso:
issuer: "https://{oauth_host}/realms/{realm}"
clientId: "{client_id}"
clientSecret: "{client_secret}"
scope: "openid profile email"

With all three set, the chart passes the issuer, client ID, client secret, and optional scope to the UI, and publishes gravitino.datastrato.custom.oidc.enable on the unauthenticated /configs endpoint so the UI can offer single sign-on before anyone has signed in. scope defaults to openid profile email. The optional name value is passed to the UI but is not shown anywhere. Leave the block unset when OAuth is only for API token validation.

Register the UI with your identity provider as a confidential client using the Authorization Code flow. The UI server, not the browser, exchanges the code for tokens and authenticates to the token endpoint with the client secret. In Azure AD that is the Web platform, not Single-page application.

1. The browser opens the UI. 2. The UI reads /configs from the server. 3. The server reports OIDC enabled. 4. The UI shows the sign-in page. 5. The user signs in at the identity provider. 6. The identity provider redirects with a code. 7. The browser passes the code to the UI callback. 8. The UI exchanges the code and client secret. 9. The identity provider returns an ID token and an access token. 10. The UI calls the server with the access token

The UI sends the identity provider's access token, not the ID token, to the server. The server validates it with the gravitino.authenticator.oauth settings, so the access token's aud must match serviceAudience and its iss must match authority.

The redirect URI to register is {ui_origin}/auth/callback/oidc. {ui_origin} is the NEXTAUTH_URL entry under enterpriseWeb.env when you set one. Otherwise the chart derives it from the first ingress.hosts entry, as https:// when ingress.tls is set and http:// when it is not. When TLS terminates at a load balancer in front of an ingress with no tls block, or the ingress is disabled, set NEXTAUTH_URL to the public UI address yourself:

my-values.yaml
enterpriseWeb:
env:
- name: NEXTAUTH_URL
value: "https://{ui_host}"

The callback response sets cookies carrying the provider's tokens, and their headers can exceed an ingress controller's default proxy buffers, which shows up as a 502 or 400 on the callback redirect. With the NGINX ingress controller, raise the buffers:

my-values.yaml
ingress:
annotations:
nginx.ingress.kubernetes.io/proxy-buffer-size: "128k"
nginx.ingress.kubernetes.io/proxy-buffers-number: "8"
nginx.ingress.kubernetes.io/proxy-busy-buffers-size: "256k"
warning

The chart renders gravitino.browserSso.clientSecret as a plain value in the environment of the UI Deployment (OIDC_CLIENT_SECRET), so anyone who can read that Deployment can read the client secret.

Sign-in page​

The UI sign-in page checks only whether oauth and basic are listed in gravitino.authenticators; their order does not matter.

ConfigurationSign-in page shows
basic onlyA user name and password form
oauth with browserSso setA Continue with single sign-on button
oauth and basic, with browserSso setContinue with single sign-on, with "Have a local account? Sign in with a password" beneath it
oauth and basic, no browserSsoA user name and password form

The button text is fixed. Choosing Sign in with a password switches to the form, which offers Use single sign-on instead to switch back.

Custom mode​

A custom server-side authenticator, an implementation of the Authenticator interface, cannot be selected through the chart in this release, because the chart renders no value that names one.

On the client side, a custom token provider works against any server: extend the abstract class CustomTokenProvider and pass it to the client.

Connect with a custom token provider
GravitinoClient client = GravitinoClient.builder("https://{gravitino_host}")
.withMetalake("metalake")
.withCustomTokenAuth(provider)
.build();

Principal and group mapping​

After a token is validated, the server turns the claims it names into a user and groups for authorization. Both mappings are regex-based by default and pass the claim through unchanged.

Principal mapping​

Principals are extracted from the JWT claims named by gravitino.authenticator.oauth.principalFields. Customize how those principals are mapped:

my-values.yaml
gravitino:
authenticator:
oauth:
# Default regex mapper, passes the claim through unchanged
principalMapper: "regex"
principalMapperRegexPattern: "^(.*)$"

To take the user name out of an email address, for example user@example.com to user, keep the regex mapper and change the pattern to ([^@]+)@.*. To use a custom implementation, set principalMapper to its fully qualified class name.

principalFields accepts a comma-separated list and each field is tried in order, so preferred_username,email,sub uses the first of the three the token carries.

Custom principal mapper​

For advanced use cases, implement the PrincipalMapper interface:

MyCustomPrincipalMapper.java
package com.example;

import org.apache.gravitino.auth.PrincipalMapper;
import java.security.Principal;

public class MyCustomPrincipalMapper implements PrincipalMapper {
@Override
public Principal map(String principal) {
return () -> "mapped_" + principal;
}
}

Point the server at the class, which must be on the server classpath:

my-values.yaml
gravitino:
authenticator:
oauth:
principalMapper: "com.example.MyCustomPrincipalMapper"

Group mapping​

Groups are extracted from the JWT claims named by gravitino.authenticator.oauth.groupsFields. Customize how those groups are mapped:

my-values.yaml
gravitino:
authenticator:
oauth:
# Default regex mapper, passes the claim through unchanged
groupMapper: "regex"
groupMapperRegexPattern: "^(.*)$"

To extract a group from a more complex string, for example /group to group, keep the regex mapper and change the pattern to ^/(.*). To use a custom implementation, set groupMapper to its fully qualified class name, such as com.example.MyCustomGroupMapper.

groupMapperRegexPattern is the values key; the server property it renders is gravitino.authenticator.oauth.groupMapper.regex.pattern.

Custom group mapper​

For advanced use cases, implement the GroupMapper interface:

MyCustomGroupMapper.java
package com.example;

import org.apache.gravitino.UserGroup;
import org.apache.gravitino.auth.GroupMapper;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class MyCustomGroupMapper implements GroupMapper {
@Override
public List<UserGroup> map(List<Object> groups) {
if (groups == null) {
return Collections.emptyList();
}
return groups.stream()
.map(g -> new UserGroup(Optional.empty(), "mapped_" + g.toString()))
.collect(Collectors.toList());
}
}

Point the server at the class, which must be on the server classpath:

my-values.yaml
gravitino:
authenticator:
oauth:
groupMapper: "com.example.MyCustomGroupMapper"

When SCIM provisioning is enabled, the chart sets principalMapper and clears groupsFields itself, and what you set for those two keys is ignored. See SCIM provisioning.

Server configuration​

Values under gravitino.authenticator.oauth are read only when gravitino.authenticators contains oauth. Paths in the table are relative to gravitino. in my-values.yaml.

Values keyDescriptionDefault valueRequired
authenticatorsComma-separated authenticators to enable, from basic and oauth. Order decides which authenticator handles a request more than one accepts. Do not use simplebasicNo
authorization.serviceAdminsComma-separated service administrator user names. With basic enabled, a local account with the initial password is created at startup for each name that has noneadminNo
admin.initPasswordInitial administrator password, 12 to 64 characters. A random 24-character password is generated when empty. Applies at the first install only(none)No
admin.existingSecretName of a Secret holding username and password, used instead of admin.initPassword(none)No
authenticator.oauth.serviceAudienceAudience the server expects in the aud claimtestYes when oauth is enabled
authenticator.oauth.tokenValidatorClassValidator implementation. org.apache.gravitino.server.authentication.JwksTokenValidator for JWKS, org.apache.gravitino.server.authentication.StaticSignKeyValidator for a static keyStaticSignKeyValidatorNo
authenticator.oauth.jwksUriJWKS endpoint the server fetches public keys from(none)Yes with JwksTokenValidator
authenticator.oauth.defaultSignKeySigning key used to validate JWTs(none)Yes with StaticSignKeyValidator
authenticator.oauth.serverUriURI of the OAuth server(none)Yes with StaticSignKeyValidator
authenticator.oauth.tokenPathToken endpoint path on the OAuth server(none)Yes with StaticSignKeyValidator
authenticator.oauth.providerProvider type, default or oidcdefaultNo
authenticator.oauth.clientIdOAuth client ID(none)No
authenticator.oauth.authorityIssuer the token's iss claim must equal exactly. When blank, the issuer is not checked. Shown as Issuer on Configure → Identity provider(none)No
authenticator.oauth.scopeSpace-separated scopes(none)No
authenticator.oauth.principalFieldsJWT claims to use as the principal identity, tried in orderpreferred_usernameNo
authenticator.oauth.principalMapperregex, or a fully qualified class implementing org.apache.gravitino.auth.PrincipalMapperregexNo
authenticator.oauth.principalMapperRegexPatternPattern whose first capture group becomes the principal. Renders principalMapper.regex.pattern^(.*)$No
authenticator.oauth.groupsFieldsJWT claims to use as group membership, tried in ordergroupsNo
authenticator.oauth.groupMapperregex, or a fully qualified class implementing org.apache.gravitino.auth.GroupMapperregexNo
authenticator.oauth.groupMapperRegexPatternPattern whose first capture group becomes the group. Renders groupMapper.regex.pattern^(.*)$No
browserSso.issuerIssuer URL the UI signs in against, used for OIDC discovery(none)Yes for browser sign-in
browserSso.clientIdClient ID of the confidential client registered for the UI(none)Yes for browser sign-in
browserSso.clientSecretClient secret for that client. Rendered as a plain value in the UI Deployment's environment(none)Yes for browser sign-in
browserSso.nameProvider name passed to the UI. Not shown on the sign-in page(none)No
browserSso.scopeScopes requested at sign-inopenid profile emailNo
ssl.trustStore.enabledImport custom certificate authorities so the server can reach an HTTPS JWKS endpointfalseNo
ssl.trustStore.existingSecretSecret holding the CA certificates to import(none)Yes when ssl.trustStore.enabled is set

Example: Azure AD as OIDC provider with JWKS validation​

Azure AD with JWKS-based token validation and browser sign-in.

Prerequisites:

  • An Azure AD app registration with a client secret
  • Platform configuration: Web, with the redirect URI {ui_origin}/auth/callback/oidc (see Browser sign-in for the UI)
my-values.yaml
gravitino:
authenticators: "oauth,basic"
authenticator:
oauth:
provider: "oidc"
clientId: "{client_id}"
authority: "https://sts.windows.net/{tenant_id}/"
jwksUri: "https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys"
tokenValidatorClass: "org.apache.gravitino.server.authentication.JwksTokenValidator"
serviceAudience: "{client_id}"
principalFields: "preferred_username,email,upn,sub"
browserSso:
issuer: "https://login.microsoftonline.com/{tenant_id}/v2.0"
clientId: "{client_id}"
clientSecret: "{client_secret}"
scope: "openid profile email {client_id}/.default"

Usage:

  • UI: the sign-in page shows Continue with single sign-on, which takes the user to Azure AD
  • API access: send Azure AD access tokens in the Authorization: Bearer {token} header

Scope: the UI forwards the access token to the server. With only openid profile email, Azure AD issues an access token for Microsoft Graph, whose audience does not match serviceAudience, and every UI call is rejected. Requesting {client_id}/.default makes Azure AD issue the access token for your own app registration.

Issuer: the iss claim of an Azure AD access token follows the access-token version set on the app registration (accessTokenAcceptedVersion in its manifest), not the authority the UI signed in against. Version 1 tokens, the default, carry https://sts.windows.net/{tenant_id}/; version 2 tokens carry https://login.microsoftonline.com/{tenant_id}/v2.0. Set authority to the one your tokens carry, because the server compares it exactly. Azure AD signs both versions with the same keys, so the v2.0 jwksUri serves either. browserSso.issuer is used only for the UI's OIDC discovery and stays on the v2.0 endpoint.

Service audience: serviceAudience must match the aud claim of the access token. Decode one issued to the UI to confirm the value before you rely on it.

Example: static key OAuth provider​

For compatibility with existing OAuth servers that do not support JWKS:

my-values.yaml
gravitino:
authenticators: "oauth,basic"
authenticator:
oauth:
provider: "default"
clientId: "{client_id}"
scope: "{scope}"
serviceAudience: "{audience}"
tokenValidatorClass: "org.apache.gravitino.server.authentication.StaticSignKeyValidator"
serverUri: "https://{oauth_host}"
tokenPath: "/oauth2/token"
defaultSignKey: "{static_signing_key}"

Example: JWKS validation with default provider​

For JWKS-based token validation without browser sign-in:

my-values.yaml
gravitino:
authenticators: "oauth,basic"
authenticator:
oauth:
provider: "default"
serviceAudience: "{audience}"
tokenValidatorClass: "org.apache.gravitino.server.authentication.JwksTokenValidator"
jwksUri: "https://{oauth_host}/.well-known/jwks.json"

Configure Keycloak as an OAuth provider​

These steps assume a running Keycloak reachable at https://{keycloak_host} and use a realm named gravitinorealm. A typical deployment registers two confidential clients:

ClientUsed byKeycloak settings
gravitino-clientEngines and machine flows (CLI, connectors, service accounts)Client authentication on, Standard flow and Service accounts roles
gravitino-uiBrowser sign-in to the UIClient authentication on, Standard flow
  1. Create a realm. In the Keycloak admin console, click Manage realms, then Create realm, and name it gravitinorealm.

    create-realm.png

  2. Add users. In gravitinorealm, click Users, then Create new user, and fill in Username, First name, Last name, and Email. On the Credentials tab, set a password and turn Temporary off so the user does not have to change it at first sign-in.

  3. Register the machine client. Click Clients, then Create client. Set Client type to OpenID Connect and Client ID to gravitino-client, click Next, enable Client authentication, Standard flow, and Service accounts roles, and click Save. Copy the Client Secret from the Credentials tab.

    create-client.png

  4. Register the UI client. Create a second client with Client ID gravitino-ui, enable Client authentication and Standard flow, and set Valid redirect URIs to {ui_origin}/auth/callback/oidc. Click Save and copy its Client Secret.

  5. Point the server and the UI at the realm. Put the realm details in my-values.yaml, then apply the values with the install command from Install.

    my-values.yaml
    gravitino:
    authenticators: "oauth,basic"
    authenticator:
    oauth:
    provider: "oidc"
    authority: "https://{keycloak_host}/realms/gravitinorealm"
    jwksUri: "https://{keycloak_host}/realms/gravitinorealm/protocol/openid-connect/certs"
    tokenValidatorClass: "org.apache.gravitino.server.authentication.JwksTokenValidator"
    serviceAudience: "account"
    principalFields: "preferred_username,email,sub"
    browserSso:
    issuer: "https://{keycloak_host}/realms/gravitinorealm"
    clientId: "gravitino-ui"
    clientSecret: "{ui_client_secret}"
    scope: "openid profile email"

    One server configuration validates tokens from both clients, so no separate validator per client is needed.

  6. Call the server with a machine token. Request a token for the service account, then use it.

    Get an access token with client credentials
    curl \
    -d "client_id=gravitino-client" \
    -d "client_secret={machine_client_secret}" \
    -d "grant_type=client_credentials" \
    "https://{keycloak_host}/realms/gravitinorealm/protocol/openid-connect/token"
    Call the server with the access token
    curl -v -X GET \
    -H "Accept: application/vnd.gravitino.v1+json" \
    -H "Authorization: Bearer {access_token}" \
    https://{gravitino_host}/api/version

    The Iceberg REST service takes the same token on its own ingress host, set by ingress.iceberg.host, and does not need the Accept header:

    Call the Iceberg REST service with the token
    curl -v -X GET \
    -H "Authorization: Bearer {access_token}" \
    https://{iceberg_host}/iceberg/v1/config

Align the token issuer for split-hostname deployments​

The iss (issuer) claim in a Keycloak token is built from the hostname the token was obtained through. When the browser and the Gravitino server reach Keycloak through different hostnames (for example, the browser uses a public URL while the server uses an internal cluster hostname), the iss claim in the browser-issued token will not match the authority the server expects, and validation fails with an error such as JWT iss claim value rejected.

To keep the issuer consistent regardless of which hostname is used to reach Keycloak, set a unified Frontend URL on the realm:

  • Open the gravitinorealm realm, go to Realm settings > General.
  • Set Frontend URL to the canonical, externally reachable Keycloak base URL (for example, https://keycloak.example.com).
  • Save, then use that same base URL in gravitino.authenticator.oauth.authority, the matching jwksUri, and gravitino.browserSso.issuer.

With the Frontend URL set, Keycloak stamps the same iss claim on every token regardless of the hostname the request arrived on, so browser-issued and server-validated tokens agree.