Authenticating with an OpenID Connect Identity Provider#
Permission Service does not mint credentials of its own. Every authenticated call into the service — and every authenticated call the service makes to other Omniverse services — is backed by a bearer token issued by a separate OpenID Connect (OIDC) identity provider. This page documents the two sides of that integration as it is exposed through the Helm chart:
The top-level
openIdsection, which configures how the service validates bearer tokens presented by incoming REST and gRPC requests.The top-level
serviceIdentitysection, which configures how the service obtains a bearer token for its own outgoing calls to other services (for example, the Notification Service — see Service notifications).
For how the validated principal is used during policy evaluation, see Identity Verification and Principal. For the administrative actions that only authenticated callers can reach, see Granting administrative permissions.
Incoming request authentication (openId)#
The Helm chart exposes a top-level openId section whose values drive everything in this chapter; in particular, the Helm value openId.enabled is the master switch that turns inbound token verification on or off for the deployed service. The full list of values is documented under Helm values below.
When openId.enabled is true, every REST and gRPC request reaching the service must carry an Authorization: Bearer <token> header. Requests without a bearer token, or with a token the configured identity provider rejects, are answered with 401 Unauthorized before any Cedar policy is evaluated.
When openId.enabled is false, the service accepts every request without inspecting the header and treats the caller as an anonymous principal. This mode is only intended for local development and testing; meta-permissions are also not enforced in that mode, which means every endpoint is reachable without credentials.
Helm values#
The chart exposes authentication settings under the top-level openId key:
openId:
enabled: true
tokenVerificationType: "jwt"
openIdConfigurationUri: "https://idp.example.com/.well-known/openid-configuration"
clientRegistrations:
- name: "default"
clientId: "permission-service-web"
scope: "openid profile email"
additionalJwtAudience: []
principalIdClaim: "sub"
Value |
Purpose |
|---|---|
|
Master switch. When |
|
Selects the verification strategy — |
|
URL of the provider’s OIDC discovery document, typically ending in |
|
List of |
|
Extra allowed |
|
The JWT claim used as the principal id for incoming callers. Defaults to |
The chart combines openId.clientRegistrations[*].clientId and openId.additionalJwtAudience into a single sorted, deduplicated list and passes it to the service as the JWT_AUDIENCE environment variable. Tokens whose aud claim does not match any entry on that list are rejected.
What happens when the service starts#
When openId.enabled is true, the service performs the following steps before it begins accepting traffic:
It fetches the discovery document from
openId.openIdConfigurationUri. Startup fails if the URL is unreachable or does not return a JSON document with the expected OIDC fields.It extracts and stores the
jwks_uri,userinfo_endpoint,token_endpoint, andissuerfields. Every subsequent token verification uses the endpoints from this cached response, so the chart only needs to know the discovery URL.It initializes the in-memory auth cache for validated tokens (size
config.tokenCacheSize, TTLconfig.tokenTtl) and the JWKS cache (TTLconfig.jwksTtl).
If the discovery URL is temporarily unreachable, the pod will fail its readiness probe until the IdP becomes reachable and the pod is restarted.
How incoming requests are authenticated#
Every incoming REST or gRPC call goes through the same pipeline — see Identity Verification for how the verified principal is then used by the rest of the service. The authentication step itself is:
Extract the bearer token from the
Authorizationheader. If it is missing or malformed, return401 Unauthorized.Look the raw token up in the auth cache. On a hit, the cached principal (or cached negative result) is reused without contacting the IdP.
On a miss, validate the token according to
openId.tokenVerificationType(see below). Validated tokens are cached until the earlier of theirexpclaim andconfig.tokenTtl; validation failures are cached forconfig.tokenTtlto protect the IdP from replay loops.Build the requester principal from the validated claims: its id is the value of
openId.principalIdClaim(defaultsub), and every claim is exposed as an attribute for Cedar conditions later in the pipeline.
Token verification types#
openId.tokenVerificationType selects one of three strategies. All three reuse the endpoints discovered from openId.openIdConfigurationUri.
jwt#
The token is treated as a signed JWT. Verification is fully local after the JWKS fetch:
The service reads the
kidfrom the JWT header and looks up the matching key in the JWKS cache. On a cache miss or expiry (config.jwksTtl), it fetchesjwks_urifrom the discovery document and refreshes the cache. RSA and elliptic-curve keys are supported.It verifies the signature using the resolved key, checks
expwithconfig.jwtLeewayseconds of clock skew, and enforces that the token’saudclaim matches one of the audiences computed fromclientRegistrationsandadditionalJwtAudience.It extracts the principal id from the
principalIdClaimclaim (falling back tosub), and exposes every JWT claim as a principal attribute.expis kept so that the auth cache entry expires when the token itself does.
Use this mode when the identity provider issues JWT access tokens and all the claims you need in policies are already embedded in the token. It is the default and the cheapest — there is no per-request round-trip to the IdP after the JWKS has been cached.
opaque#
The token is treated as an opaque string. Verification is delegated to the provider:
The service sends
GET <userinfo_endpoint>withAuthorization: Bearer <token>. A200 OKresponse means the token is valid; the JSON body of that response is used as the claim set for the principal.401is cached as a negative result;403is returned as403 Forbiddento the caller.Because opaque tokens do not carry an
exp, the resulting principal is cached forconfig.tokenTtl. Setconfig.tokenTtlno higher than your provider’s actual token lifetime, otherwise a revoked token will keep working until the cache entry expires.
Use this mode when the identity provider hands out opaque reference tokens (common for providers that enforce token introspection or want to keep claims server-side).
jwtuserinfo#
Runs the jwt verification first, then additionally calls userinfo:
If the JWT signature, audience, and expiration are valid, the service fetches
userinfowith the same bearer token.The JSON returned by
userinfois merged on top of the JWT claims, so the principal exposes both sets of attributes for Cedar conditions. If the JWT does not carryprincipalIdClaim(norsub), the id is taken fromuserinfoinstead.
Use this mode when the JWT is enough to prove identity but some attributes you want to reference in policies (for example, group memberships, tenant id, or custom profile fields) live only on the userinfo endpoint. The cost is one extra HTTP round-trip per token — mitigated by config.tokenCacheSize and config.tokenTtl.
Configuring Microsoft Entra ID#
Microsoft Entra ID (formerly Azure Active Directory) issues JWT access tokens signed with rotating RSA keys that are published through a standard OIDC discovery document. It is a straightforward fit for tokenVerificationType: "jwt".
Prerequisites on the Entra side. In the Azure portal:
Register an App registration for the API (the Permission Service). Under Expose an API, add at least one scope (for example,
access_as_user) so that client apps have something to request. Note the registration’s Application (client) ID (<api-client-id>) — this is the GUID that Entra puts into theaudclaim of v2.0 access tokens issued for this API.Register a separate App registration for each client that will call Permission Service (web app, CLI, or service). Grant it at least one delegated or application permission on the API registration from step 1.
Note the directory’s tenant id (
<tenant-id>) and, for each client, its client id (<web-client-id>,<cli-client-id>, and so on).
The OIDC discovery URL for a tenant is:
https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration
Permission Service values. A minimal Entra-backed configuration looks like this:
openId:
enabled: true
tokenVerificationType: "jwt"
openIdConfigurationUri: "https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration"
clientRegistrations:
- name: "default"
clientId: "<web-client-id>"
scope: "openid profile email <api-client-id>/.default"
- name: "cli"
clientId: "<cli-client-id>"
scope: "<api-client-id>/.default"
additionalJwtAudience:
- "<api-client-id>"
principalIdClaim: "oid"
Notes specific to Entra:
Entra v2.0 access tokens issued for a custom API set
audto the client id (GUID) of the API app registration — not to any of the calling client ids. Put that GUID (the<api-client-id>from step 1 of the prerequisites) inadditionalJwtAudienceso it passes audience validation. The individualclientIdvalues underclientRegistrationsstay useful for documentation and for deployments where the IdP issues tokens whoseaudequals the calling client id.Clients request tokens for the API using the scope
<api-client-id>/.default(theapi://Application ID URI form is not used here — use the bare client-id GUID). Any additional standard scopes (openid,profile,email) can be added alongside it as shown above.Entra exposes the stable per-user directory identifier as the
oidclaim;subis pairwise (different per application) and not suitable as a cross-service user id. SetprincipalIdClaim: "oid"so Cedar policies can reference a stable principal across applications. For service principals (client-credentials grants), Entra populatesoidwith the service principal’s object id.Group membership claims in Entra are off by default. Enable them in the Token configuration blade of the API app registration to get a
groupsclaim, then reference it in policies throughprincipal.groups(see Groups and membership).
With those values applied and the pod restarted, Permission Service fetches the Entra discovery document at startup, caches the JWKS, and begins validating Bearer tokens issued by that tenant.
Outbound service identity (serviceIdentity)#
Permission Service is a client of other Omniverse services. Most notably, Service notifications are delivered through gRPC to the Event Aggregation (Notification) Service, and publishing there is itself an authorized action (Action::"event-aggregation-service:publish-event"). Anything Permission Service calls over the network needs its own bearer token; the serviceIdentity section of the chart configures how that token is obtained.
serviceIdentity offers two mutually exclusive modes:
Client-credentials flow — Permission Service exchanges a client id and a client secret for an access token at the provider’s token endpoint, and refreshes the token as it expires.
Access-token file — A sidecar container writes a pre-provisioned access token into a shared in-memory volume, and Permission Service reads it from there. No secret material lives inside the Permission Service container.
Exactly one of serviceIdentity.clientCredentials.enabled and serviceIdentity.accessTokenFile.enabled may be true. When both are false, outgoing calls are made without a bearer token — acceptable only for downstream services that do not require authentication (typically only in tests).
Client credentials flow#
serviceIdentity:
clientCredentials:
enabled: true
openIdConfigurationUri: "https://idp.example.com/.well-known/openid-configuration"
clientId: "permission-service"
clientScope: "notifications.publish"
clientSecretRef:
name: permission-service-client-credentials
key: clientSecret
extraHeaders: {}
extraBodyParams: {}
Value |
Purpose |
|---|---|
|
Turns the client-credentials flow on. Default |
|
Discovery URL of the IdP used for the token exchange. When empty, the chart falls back to |
|
Client id registered at the IdP for Permission Service. |
|
Value of the |
|
Reference to a pre-existing Kubernetes |
|
Optional JSON object of additional HTTP headers to attach to the token exchange request (for example |
|
Optional JSON object of additional body form parameters (for example |
At startup, the service fetches the discovery document from openIdConfigurationUri (or falls back to openId.openIdConfigurationUri) and extracts the token_endpoint. Before every outgoing RPC, a gRPC interceptor injects the current cached access token into the Authorization header; when the cached token has expired or the downstream service returns Unauthenticated, the interceptor triggers a fresh token exchange against the token endpoint — posting grant_type=client_credentials together with the client id, the secret read from the mounted file, the configured scope, and any extraHeaders/extraBodyParams — and caches the new token for its reported expires_in.
Use this mode when your IdP supports the standard OAuth 2.0 client-credentials grant and you are happy storing a long-lived client secret in a Kubernetes Secret.
Access-token file#
serviceIdentity:
accessTokenFile:
enabled: true
pollInterval: "15s"
provisionerSidecar:
container:
name: token-provisioner
image: registry.example.com/token-provisioner:1.0.0
args: ["--output", "/etc/secrets/access-token/token"]
Value |
Purpose |
|---|---|
|
Turns the access-token-file mode on. Default |
|
How often the main container re-reads the shared token file (humantime format, for example |
|
Full container spec for a sidecar that writes the access token to the shared volume. The chart takes this spec verbatim and appends a |
This mode lets you deploy the Permission Service alongside any token-provisioning container you already use — a workload-identity helper, a Vault agent, a Kubernetes service-account token projector, or a cloud-specific metadata client. The sidecar runs in the same Pod and shares an in-memory (emptyDir with medium: Memory) volume called token-exchange, mounted at /etc/secrets/access-token/ in both containers. The sidecar writes (and, over time, rewrites) a single file named token containing JSON with access_token and expires_on fields. expires_on must be an RFC3339 timestamp. The access token must be accepted by the downstream services Permission Service talks to — for example, a token with event-aggregation-service:publish-event granted when notifications are enabled.
Permission Service reads /etc/secrets/access-token/token at startup, watches it at pollInterval, and re-reads it eagerly whenever a downstream service returns Unauthenticated. The provisioner sidecar is fully under your control — from its image to its args to its volume mounts — which makes this the preferred mode in environments where:
Short-lived tokens are minted from a platform-specific identity (workload identity federation, IRSA, AKS workload identity, GKE workload identity, and similar) and the Permission Service itself should not hold a client secret.
The token exchange requires a flow (JWT assertion, mTLS, workload-attested OIDC) that is more complex than the standard OAuth 2.0 client-credentials grant supported by the built-in client.
Several services in the same deployment need to share a single sidecar pattern.
See Service notifications → Authenticating to the Notification Service for the exact policy the presented principal needs on the Notification Service side, and for how Permission Service falls back to an anonymous channel when neither mode is enabled.
See also#
Service notifications — the primary consumer of the
serviceIdentitycredentials.How authorization requests are evaluated — pipeline that runs after a token has been validated.
Permission Service Configuration — full Helm chart reference, including
openIdin its wider context.Writing Cedar policies for Permission Service — how the verified principal and its claims become
principalin Cedar.Managing service metadata through the REST API — per-service
idClaimoverride that complementsopenId.principalIdClaim.