Microsoft Entra ID
muvee supports Microsoft Entra ID (formerly Azure AD) via OpenID Connect on top of the OAuth 2.0 authorization code flow. It is the only provider wired into both planes from a single set of credentials:
| Plane | Who signs in | Toggle |
|---|---|---|
| Platform | muvee control-panel users (muvee_session) | platform_entra_login_enabled |
| Project | end users of a project subdomain, through ForwardAuth (muvee_fwd_session) | entra_enabled |
Unlike Google / Feishu / WeCom / DingTalk, Entra is not configured with environment variables:
everything lives in Admin → Settings (stored in system_settings), so no redeploy is needed.
ENTRA_* env vars are only a fallback for values left blank in the UI.
See Entra ID — admin setup walkthrough, which walks an administrator through the Azure app registration and the muvee settings card with screenshots. The page you are reading is the reference behind it.
Setup
1. Register the application in Azure
-
In the Azure portal, go to Microsoft Entra ID → App registrations → New registration
-
Pick the Supported account types that match your intent — this determines the tenant value below
-
Under Redirect URI, choose the Web platform and add the URIs you need (replace
example.comwith yourBASE_DOMAIN):https://example.com/auth/entra/callback # platform login
https://example.com/_oauth/entra # project subdomains (ForwardAuth)Both can live on the same app registration — add only the ones for the planes you enable.
Multi-domainIf you serve muvee under multiple base domains (
BASE_DOMAINS), add both URIs for each domain — muvee sends the user back to whichever base domain they started on. See Configuration → Multi-domain. -
Go to Certificates & secrets → New client secret and copy the secret Value (not its ID). Entra secrets expire — note the expiry date, because sign-in breaks on that day.
2. Configure muvee
In Admin → Settings → Social Login Providers, fill in the Microsoft Entra ID card:
| Field | Where it comes from |
|---|---|
| Directory (tenant) ID | Azure app registration Overview |
| Application (client) ID | Azure app registration Overview |
| Client Secret | the secret Value from step 4 |
Then tick the planes you want and press Save:
- Enable on project subdomains — muvee-server reloads muvee-authservice's provider set immediately, so the button shows up on project login pages without a restart.
- Enable on the muvee platform login page — registered in-process, also without a restart.
The card shows both redirect URIs read-only, with a copy button, so you can paste them straight into Azure.
3. Pick the projects that offer it
Enabling Entra downstream makes it available; each project's Auth tab decides what its login page shows. Leave Sign-in providers empty to offer every configured provider, or tick a subset (now including Microsoft) to restrict it.
Tenant values and what they imply
entra_tenant_id accepts three shapes, and the choice changes how strictly muvee validates tokens:
| Value | Who can sign in | ID-token check | ALLOWED_DOMAINS |
|---|---|---|---|
| Directory GUID (recommended) | that one directory | issuer and tid must equal the configured tenant | skipped (org-scoped) |
Verified domain (contoso.onmicrosoft.com) | that one directory | issuer must match the tid it resolves to | skipped (org-scoped) |
organizations / common / consumers | any work/school directory (plus personal accounts for the latter two) | issuer must match the token's tid | enforced |
In every case the audience must equal your client ID, and the ID token signature is verified against
the tenant's JWKS at login.microsoftonline.com.
Single-tenant configurations are treated as org-scoped, the same way Feishu / WeCom / DingTalk
are: membership in the directory is itself the authorisation, so the platform's ALLOWED_DOMAINS
email-domain whitelist is skipped. With a multi-tenant alias anyone in the world can complete the
flow, so ALLOWED_DOMAINS (and access_mode) remain your only gate — set them.
Claims muvee reads
| Claim | Used for |
|---|---|
email, falling back to preferred_username (the UPN) | the platform / project identity |
name | display name (the composed one — given_name / family_name are optional claims muvee has no separate fields for) |
oid + tid | stable subject key |
tid, iss, aud | validation (see above) |
name and oid need the openid profile scopes and email needs the email scope; muvee requests
all of them. The email claim is present by default only for guest accounts — for members of the
tenant it is the email scope that produces it, and if the directory has no mail attribute for the
user, muvee falls back to the UPN in preferred_username.
Profile photos
Entra v2.0 ID tokens carry no picture claim, and Graph profile photos sit behind a bearer token
rather than a public URL. So when Fetch profile photos is on (the default), muvee adds the
delegated Graph scope User.Read to the login request and, right after the token exchange, reads
GET /me/photos/96x96/$value.
Those bytes then get written to disk and the user record stores an ordinary URL
(https://<base-domain>/api/public/avatars/avatar-<hash>.jpg), so an Entra avatar behaves exactly
like Google's picture or Lark's avatar_url everywhere downstream. The file lives in the branding
asset volume (BRANDING_DIR, default /data/branding) under avatars/, is named by content hash —
so repeat logins reuse one file — and is served publicly and cached immutably, because the <img>
tags loading it sit on project subdomains and carry no credentials.
An earlier build stored the photo as a data: URI on the user record. That value also travelled into
the forward-auth session JWT, and a 2-8 KB photo becomes a 4-15 KB cookie once base64 and the JWT
envelope have each inflated it by 4/3 — past the 4096-byte limit every browser enforces. Browsers drop
an oversized cookie silently, so the login appeared to succeed while no session stuck and the project
subdomain bounced the user back to the login page indefinitely.
Existing users still holding an inline data: URI are migrated lazily — the next sign-in replaces it.
A session is never signed with an avatar over 2048 bytes regardless, which is the backstop for a
deployment with no asset storage configured.
It is best-effort: a user with no photo (Graph answers 404), a slow Graph, or a missing consent all
degrade to an empty avatar and never fail the login. Turning the option off drops User.Read from the
authorize request entirely, which is what you want in a tenant that withholds consent for it.
Guest accounts that expose neither email nor a UPN-shaped preferred_username cannot be admitted
to the platform plane — there is no address to match against ADMIN_EMAILS, the invite white-list,
or ALLOWED_DOMAINS.
Settings reference
| Setting | Values | Description |
|---|---|---|
entra_enabled | true / false | Offer Entra on project subdomains |
platform_entra_login_enabled | true / false | Offer Entra on the muvee platform login page |
entra_tenant_id | GUID / domain / common / organizations / consumers | Azure directory |
entra_client_id | — | Application (client) ID |
entra_client_secret | — | Client secret value |
entra_avatar_enabled | true / false (default true) | Read the profile photo from Microsoft Graph; adds the User.Read scope |
Env fallbacks, used per-field only when the setting is blank: ENTRA_TENANT_ID, ENTRA_CLIENT_ID,
ENTRA_CLIENT_SECRET, ENTRA_FETCH_AVATAR, plus ENTRA_REDIRECT_URL (overrides the derived platform
callback) and PLATFORM_ENTRA_LOGIN (fallback for the platform toggle).
Secrets are stored unencrypted in system_settings, the same threat model as the other provider
credentials — the admin-only settings API is the protection boundary.
Troubleshooting
AADSTS50011: redirect URI mismatch — the URI in Azure must match byte for byte, including
https:// and the trailing path. Copy it from the settings card rather than typing it.
The button doesn't appear after saving — the credentials are incomplete (a blank tenant, client
ID or secret registers nothing on purpose) or the tenant is malformed; check the muvee-server log for
entra provider. For project pages also confirm the project's Sign-in providers list isn't
restricted to other providers.
id_token was issued by tenant …, not the configured … — the app registration is multi-tenant
while entra_tenant_id pins a single GUID. Either point the tenant at the directory actually issuing
tokens, or switch it to organizations.
Sign-in worked and then stopped — check whether the client secret expired in Azure.
Nobody gets an avatar — the tenant most likely never granted User.Read; the server logs
entra: fetch avatar. Untick Fetch profile photos to stop asking for the scope. A single user
without a photo is just an empty avatar and needs no action.