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. Notentra, notoidc, notproxy. Since 0.7.0 an unrecognisedapp.identity.modefails the render; before that it silently meantstatic, which installs cleanly, comes up healthy, and gives every visitor the same shared operator identity with/adminopen.
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:
- 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.
- The proxy overwrites the identity headers rather than passing the caller's through.
- 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:
| Field | Value |
|---|---|
| Redirect URI | Platform Web, https://<your-prism-host>/oauth2/callback |
| Client secret | Certificates & secrets → New client secret. Copy it now; it is shown once |
| Issuer | https://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. Writingclaim: oidis 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.userIDClaimpopulates oauth2-proxy'sEmailfield, not itsUserfield. That is why-PRINCIPAL-IDreadsclaim: emailand gets a GUID. The obvious mapping (claim: userfor the id,claim: emailfor 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. WithuserIDClaim: oidthe 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, above | oid | claim: email → the GUID | claim: preferred_username → the UPN |
| When the UPN is not an email | email | claim: preferred_username → the UPN | claim: 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:
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-apponly from the proxy:apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata:name: prism-app-from-proxy-onlyspec:podSelector:matchLabels:app.kubernetes.io/name: prism-apppolicyTypes: [Ingress]ingress:- from:- podSelector:matchLabels:app.kubernetes.io/name: oauth2-proxyConfirm 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 atprism-appis 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-NAMEis email-shaped — map that header to Entra'snameclaim instead ofpreferred_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.
-
Sign in. Browse to your host. You should land on the Microsoft sign-in page and come back to Prism's chat.
-
Check who Prism thinks you are —
https://<your-prism-host>/whoami:{"sub":"11111111-2222-4333-8444-555555555555",suba GUID andemailan address means the mapping is the right way round. Swap them and you have theuserIDClaimtrap. An emptyemail, or a#EXT#string where an address belongs, means-PRINCIPAL-NAMEis 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: userwhere you expectedsuper_adminmeans the asserted address is not the one inapp.auth.superAdmins; the address in this output is the one Prism matched, so compare against that rather than against what you assumed. -
Open
/adminand/selftest. Both require super admin, so this proves the tier resolved and not just the sign-in. -
Try the bypass. From another pod in the cluster, run the
curlfrom 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; onlylocalmode has an allowlist to show. - Continuity of saved work across a switch. Saved prompts, conversations
and feedback key on the identity
sub. Underlocalmode that is the lowercased email address; undereasyauthwith the default mapping it is the Entraoid, so a deployment moving from email sign-in to Entra hands everyone a clean slate. To carry it over,-PRINCIPAL-IDhas to keep asserting that same address —userIDClaim: emailwith-PRINCIPAL-IDreadingclaim: 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/timeouton 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.