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.
-
Collect the provider details. You need the issuer, the JWKS endpoint, the token endpoint, and the audience the provider puts in the
audclaim of the tokens it issues to your client. Most providers list the first three in their discovery document at{issuer}/.well-known/openid-configuration. -
Add the OAuth values. Put the details in
my-values.yaml.my-values.yamlgravitino:
authenticators: "oauth,basic"
authenticator:
oauth:
tokenValidatorClass: "org.apache.gravitino.server.authentication.JwksTokenValidator"
jwksUri: "{jwks_uri}"
authority: "{issuer}"
serviceAudience: "{audience}"
principalFields: "preferred_username,email,sub" -
Apply the values. Rerun the install command from Install.
Apply the valueshelm 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 -
Get a token. Request an access token with the client credentials grant.
Get an access tokenTOKEN=$(curl -sS "{token_endpoint}" \
-d "grant_type=client_credentials" \
-d "client_id={client_id}" \
-d "client_secret={client_secret}" \
| jq -r .access_token) -
Call the API. List the metalakes with the token.
Call the API with the tokencurl -sS "https://{gravitino_host}/api/metalakes" \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Authorization: Bearer $TOKEN"A
401response means the token failed validation. Decode it and compare itsaudandissclaims withserviceAudienceandauthority, 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.
| Row | Configuration key | Value shown |
|---|---|---|
| Issuer | gravitino.authenticator.oauth.authority | The issuer the server checks in API tokens, or "Not enabled" when it is unset |
| SCIM service | gravitino.scim.httpPort | The 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.
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.
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:
GravitinoClient client = GravitinoClient.builder("https://{gravitino_host}")
.withMetalake("metalake")
.withBasicAuth("admin", "{admin_password}")
.build();
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}"),
)
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.
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:
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:
gravitino:
ssl:
trustStore:
enabled: true
existingSecret: "{ca_secret}"
For the client side, enable OAuth mode:
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.
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.
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:
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:
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"
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.
| Configuration | Sign-in page shows |
|---|---|
basic only | A user name and password form |
oauth with browserSso set | A Continue with single sign-on button |
oauth and basic, with browserSso set | Continue with single sign-on, with "Have a local account? Sign in with a password" beneath it |
oauth and basic, no browserSso | A 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.
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:
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:
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:
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:
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:
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:
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 key | Description | Default value | Required |
|---|---|---|---|
authenticators | Comma-separated authenticators to enable, from basic and oauth. Order decides which authenticator handles a request more than one accepts. Do not use simple | basic | No |
authorization.serviceAdmins | Comma-separated service administrator user names. With basic enabled, a local account with the initial password is created at startup for each name that has none | admin | No |
admin.initPassword | Initial administrator password, 12 to 64 characters. A random 24-character password is generated when empty. Applies at the first install only | (none) | No |
admin.existingSecret | Name of a Secret holding username and password, used instead of admin.initPassword | (none) | No |
authenticator.oauth.serviceAudience | Audience the server expects in the aud claim | test | Yes when oauth is enabled |
authenticator.oauth.tokenValidatorClass | Validator implementation. org.apache.gravitino.server.authentication.JwksTokenValidator for JWKS, org.apache.gravitino.server.authentication.StaticSignKeyValidator for a static key | StaticSignKeyValidator | No |
authenticator.oauth.jwksUri | JWKS endpoint the server fetches public keys from | (none) | Yes with JwksTokenValidator |
authenticator.oauth.defaultSignKey | Signing key used to validate JWTs | (none) | Yes with StaticSignKeyValidator |
authenticator.oauth.serverUri | URI of the OAuth server | (none) | Yes with StaticSignKeyValidator |
authenticator.oauth.tokenPath | Token endpoint path on the OAuth server | (none) | Yes with StaticSignKeyValidator |
authenticator.oauth.provider | Provider type, default or oidc | default | No |
authenticator.oauth.clientId | OAuth client ID | (none) | No |
authenticator.oauth.authority | Issuer 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.scope | Space-separated scopes | (none) | No |
authenticator.oauth.principalFields | JWT claims to use as the principal identity, tried in order | preferred_username | No |
authenticator.oauth.principalMapper | regex, or a fully qualified class implementing org.apache.gravitino.auth.PrincipalMapper | regex | No |
authenticator.oauth.principalMapperRegexPattern | Pattern whose first capture group becomes the principal. Renders principalMapper.regex.pattern | ^(.*)$ | No |
authenticator.oauth.groupsFields | JWT claims to use as group membership, tried in order | groups | No |
authenticator.oauth.groupMapper | regex, or a fully qualified class implementing org.apache.gravitino.auth.GroupMapper | regex | No |
authenticator.oauth.groupMapperRegexPattern | Pattern whose first capture group becomes the group. Renders groupMapper.regex.pattern | ^(.*)$ | No |
browserSso.issuer | Issuer URL the UI signs in against, used for OIDC discovery | (none) | Yes for browser sign-in |
browserSso.clientId | Client ID of the confidential client registered for the UI | (none) | Yes for browser sign-in |
browserSso.clientSecret | Client secret for that client. Rendered as a plain value in the UI Deployment's environment | (none) | Yes for browser sign-in |
browserSso.name | Provider name passed to the UI. Not shown on the sign-in page | (none) | No |
browserSso.scope | Scopes requested at sign-in | openid profile email | No |
ssl.trustStore.enabled | Import custom certificate authorities so the server can reach an HTTPS JWKS endpoint | false | No |
ssl.trustStore.existingSecret | Secret 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)
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:
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:
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:
| Client | Used by | Keycloak settings |
|---|---|---|
gravitino-client | Engines and machine flows (CLI, connectors, service accounts) | Client authentication on, Standard flow and Service accounts roles |
gravitino-ui | Browser sign-in to the UI | Client authentication on, Standard flow |
-
Create a realm. In the Keycloak admin console, click Manage realms, then Create realm, and name it
gravitinorealm.
-
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. -
Register the machine client. Click Clients, then Create client. Set Client type to
OpenID Connectand Client ID togravitino-client, click Next, enable Client authentication, Standard flow, and Service accounts roles, and click Save. Copy the Client Secret from the Credentials tab.
-
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. -
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.yamlgravitino:
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.
-
Call the server with a machine token. Request a token for the service account, then use it.
Get an access token with client credentialscurl \
-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 tokencurl -v -X GET \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Authorization: Bearer {access_token}" \
https://{gravitino_host}/api/versionThe Iceberg REST service takes the same token on its own ingress host, set by
ingress.iceberg.host, and does not need theAcceptheader:Call the Iceberg REST service with the tokencurl -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
gravitinorealmrealm, 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 matchingjwksUri, andgravitino.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.