Skip to main content

Sign in with Entra ID

Prism has no Entra mode, and does not need one. app.identity.mode: easyauth means "something in front of me asserts who the user is, in X-MS-CLIENT-PRINCIPAL-* request headers". On Azure Container Apps the platform does that natively, which is where the mode got its name. Anywhere else — OpenShift, plain Kubernetes — you put an OIDC proxy in front of the app and configure it to emit those headers, and Prism cannot tell the difference.

That is the whole integration. What makes it worth a page of its own is that two pieces of it are counter-intuitive enough to be copied rather than inferred, and one omission leaves the install wide open.

The mode is easyauth. Not entra, not oidc, not proxy. Since 0.7.0 an unrecognised app.identity.mode fails the render; before that it silently meant static, which installs cleanly, comes up healthy, and gives every visitor the same shared operator identity with /admin open.

The shape​

browser ──▶ Route / Ingress ──▶ OIDC proxy ──▶ prism-app
(Entra ID) (identity.mode: easyauth)

Three properties make it safe, and all three are yours to arrange:

  1. The proxy is the only way in. Prism trusts those headers from anything that can reach it — see Close the bypass, which is not optional.
  2. The proxy overwrites the identity headers rather than passing the caller's through.
  3. Prism matches super admins on the email the proxy asserts, so the claim carrying it has to be one that is actually populated for your users.

1. Register the application in Entra ID​

In Entra ID → App registrations → New registration:

FieldValue
Redirect URIPlatform Web, https://<your-prism-host>/oauth2/callback
Client secretCertificates & secrets → New client secret. Copy it now; it is shown once
Issuerhttps://login.microsoftonline.com/<tenant-id>/v2.0 — the proxy needs this

No optional claims are needed for the configuration below. oid (the stable per-user GUID) and preferred_username (the user principal name) are both in the ID token by default, and those are the two values it uses.

Which address Prism ends up matching is worth being deliberate about, and it is decided here rather than in Prism. The configuration below asserts preferred_username — the UPN — as the user's address, so app.auth.superAdmins must list UPNs. In most tenants a member's UPN is their email address and the distinction never surfaces. Where it does not hold — guest and B2B accounts, whose UPN looks like someone_example.com#EXT#@yourtenant.onmicrosoft.com — that account signs in perfectly, shows a UPN where a name belongs, and can never hold super admin, because the address Prism is matching is not one anybody would put on the list.

If your intended administrators are in that position, add email under Token configuration → Add optional claim → ID, and use the alternative mapping in Which value goes where below — it costs you the GUID as the identity key and gets you the mail attribute as the address. Populate the mail attribute too: an optional claim for an attribute nobody has set adds nothing.

To control who may sign in at all, use Entra's own assignment: in Enterprise applications → your app → Properties, set Assignment required to Yes, and assign the users or groups. Prism has no allowlist under this mode — it admits everyone the proxy admits.

2. Configure the proxy​

The worked example is oauth2-proxy v7.13. Header injection needs its --alpha-config file; the equivalent command-line flags cannot express it.

server:
BindAddress: 0.0.0.0:4180

upstreamConfig:
upstreams:
- id: prism
path: /
uri: http://prism-app.<namespace>.svc.cluster.local
passHostHeader: true
# The default is 1s, which delivers a streamed answer in one-second
# lumps. Prism streams tokens as they are produced.
flushInterval: 100ms

injectRequestHeaders:
# Prism's identity key (`sub`). Reads oauth2-proxy's Email session field,
# which `userIDClaim` below populates with the Entra object id. This looks
# wrong and is not — see the note under the table.
- name: X-MS-CLIENT-PRINCIPAL-ID
values:
- claim: email
# The address super admins are matched on.
- name: X-MS-CLIENT-PRINCIPAL-NAME
values:
- claim: preferred_username
# NOT optional. See "Close the bypass" below.
- name: X-MS-CLIENT-PRINCIPAL
values:
- claim: user

providers:
- id: entra
provider: oidc
clientID: <application-client-id>
clientSecret: <client-secret>
oidcConfig:
issuerURL: https://login.microsoftonline.com/<tenant-id>/v2.0
emailClaim: email
# The stable per-user GUID, and what ends up in -PRINCIPAL-ID.
userIDClaim: oid

with --cookie-secret, --email-domain=* and --redirect-url=https://<your-prism-host>/oauth2/callback alongside it.

Three things in that file are traps, and each one was found by running it rather than by reading the documentation:

  • claim: does not take an arbitrary ID-token claim. oauth2-proxy resolves only its own session fields there — user, email, groups, preferred_username, id_token, access_token. Writing claim: oid is not an error and does not warn: the header is simply absent, and Prism then refuses every request with "TRUST_EASYAUTH=1 but no Easy Auth identity headers on the request" — a message that points at the app, three layers from the mistake.
  • userIDClaim populates oauth2-proxy's Email field, not its User field. That is why -PRINCIPAL-ID reads claim: email and gets a GUID. The obvious mapping (claim: user for the id, claim: email for the address) delivers the two values swapped, and Prism will happily run that way — showing GUIDs where names belong and matching super admins against nothing.
  • --email-domain=* is load-bearing here. With userIDClaim: oid the proxy's Email field holds a domainless GUID, so any domain restriction rejects every user in your tenant. Do the restricting in Entra (assignment required, above), which is where it belongs.

And one that is not a trap so much as a thing worth knowing: claim: user is not the UPN. Against real Entra it resolves to the token's sub — an opaque, per-application identifier that means nothing to a human and matches nothing on your admin list. It is used above only to overwrite the claims blob, where any value will do. preferred_username is the claim that carries the UPN.

Which value goes where​

Prism takes the user's email from -PRINCIPAL-NAME, and its identity key (sub) from -PRINCIPAL-ID. oauth2-proxy will only surface two distinct values to inject — whatever userIDClaim names, and preferred_username — so there are two workable mappings and you pick by which address you want matched:

userIDClaim-PRINCIPAL-ID (sub)-PRINCIPAL-NAME (email)
Default, aboveoidclaim: email → the GUIDclaim: preferred_username → the UPN
When the UPN is not an emailemailclaim: preferred_username → the UPNclaim: email → the mail attribute

The default gives a key that survives a user being renamed, and matches on the UPN. The alternative matches on the real mail address — needed for guest accounts — at the cost of keying identity on the UPN, so renaming a user there starts them a fresh history. Neither can do both; there is no third slot.

3. Point Prism at it​

app:
identity:
mode: easyauth
auth:
superAdmins:
- ops-[email protected] # at least one, or nobody can open /admin
ingress:
enabled: false # the Route points at the PROXY, not the app

superAdmins matters as much here as under email sign-in: left empty, the app fails closed and nobody can reach /admin or /selftest. Unlike local mode the chart cannot refuse to render for you — under easyauth an empty list is a legitimate, if useless, install — so it warns and installs. Details in Identity and roles.

4. Close the bypass​

Under easyauth Prism believes those headers from anything that can reach it. There is no signature, no IP allowlist and no second check — that is what the mode is. On Azure Container Apps the platform guarantees the headers and strips forged copies; here, you do. A request straight to the Service, skipping the proxy, looks like this:

$ curl -H "X-MS-CLIENT-PRINCIPAL-ID: anything" \
-H "X-MS-CLIENT-PRINCIPAL-NAME: [email protected]" \
http://prism-app/whoami
{"sub":"anything","email":"[email protected]","role":"super_admin"}

Full super admin, from two headers and no credential. Three things close it, and the chart ships none of them:

  • A NetworkPolicy admitting traffic to prism-app only from the proxy:

    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
    name: prism-app-from-proxy-only
    spec:
    podSelector:
    matchLabels:
    app.kubernetes.io/name: prism-app
    policyTypes: [Ingress]
    ingress:
    - from:
    - podSelector:
    matchLabels:
    app.kubernetes.io/name: oauth2-proxy

    Confirm your cluster actually enforces NetworkPolicy — on a cluster with no network plugin that implements it, this object is decoration.

  • No second door. app.ingress.enabled: false, and the Route or Ingress resolves to the proxy. An Ingress left pointing at prism-app is the bypass above, published.

  • Overwriting X-MS-CLIENT-PRINCIPAL, as the config in step 2 does. The proxy overrides only the headers it is told to, so a caller's own base64 claims blob otherwise reaches Prism untouched. Prism ignores it as long as -PRINCIPAL-NAME is email-shaped — map that header to Entra's name claim instead of preferred_username, a very natural mistake, and the blob is what Prism reads for the email it matches super admins against.

5. Prove it​

In this order, because each step tells you something the next one assumes.

  1. Sign in. Browse to your host. You should land on the Microsoft sign-in page and come back to Prism's chat.

  2. Check who Prism thinks you are — https://<your-prism-host>/whoami:

    {"sub":"11111111-2222-4333-8444-555555555555",
    "email":"[email protected]","role":"super_admin"}

    sub a GUID and email an address means the mapping is the right way round. Swap them and you have the userIDClaim trap. An empty email, or a #EXT# string where an address belongs, means -PRINCIPAL-NAME is carrying a UPN that is not email-shaped — the guest-account case, and the second mapping in Which value goes where is the answer. role: user where you expected super_admin means the asserted address is not the one in app.auth.superAdmins; the address in this output is the one Prism matched, so compare against that rather than against what you assumed.

  3. Open /admin and /selftest. Both require super admin, so this proves the tier resolved and not just the sign-in.

  4. Try the bypass. From another pod in the cluster, run the curl from step 4 against the app's Service. It must fail to connect. If it answers, your NetworkPolicy is absent or unenforced, and the sign-in page is decoration.

What this mode does not give you​

  • A sign-out button. The session belongs to the proxy, so Prism renders no sign-out affordance — offering one that cannot end the session would be worse than none. Point users at the proxy's own endpoint (oauth2-proxy: /oauth2/sign_out), and note that signing out of Prism is not signing out of Entra.
  • An in-app allowlist. Who may sign in is an Entra question. /admin → App users is not served under this mode; only local mode has an allowlist to show.
  • Continuity of saved work across a switch. Saved prompts, conversations and feedback key on the identity sub. Under local mode that is the lowercased email address; under easyauth with the default mapping it is the Entra oid, so a deployment moving from email sign-in to Entra hands everyone a clean slate. To carry it over, -PRINCIPAL-ID has to keep asserting that same address — userIDClaim: email with -PRINCIPAL-ID reading claim: email. That buys continuity at the price of a key that moves when a mailbox does.

What this page has been proved against​

The header contract, the claim mapping, the header-overwrite behaviour and the streaming pass-through were verified end to end against oauth2-proxy v7.13.0 — first against a stand-in OIDC issuer, then against a real Entra ID tenant, signing a real account in through a real authorization-code flow into a real Prism with easyauth on. sub came back as the account's actual directory object id and the super-admin tier resolved off the asserted UPN. The values in step 2 are the configuration that passed, not a reconstruction.

Two layers around it were not exercised and are the likely places for a first install to stick:

  • Your Route's own idle timeout. Prism holds an SSE connection open for the length of an answer. On OpenShift the default HAProxy timeout is 30 seconds, which is shorter than a long answer takes; set haproxy.router.openshift.io/timeout on the Route (10m, as Installing Prism step 1e asks for). An answer that starts streaming and stops part-way, reproducibly, at the same number of seconds, is this and not the model.
  • Guest and B2B accounts. The tenant this was proved against signed in a member account, whose UPN is their email address. The #EXT# case above is reasoned from how Entra issues those UPNs, not observed — if your administrators are guests, do step 5 before you rely on it.

If sign-in fails, the self test reports the identity mode the app is actually running in, which is the fastest way to tell a proxy problem from a Prism one.