Enterprise Setup & SSO Guide
Deploying TheiaCast for an organisation: hostnames and TLS, Single Sign-On with Microsoft Entra ID or any OpenID Connect provider, identity-provider app registrations, user and role management, device rollout, and troubleshooting.
How this guide fits with the others
This page is the end-to-end reference for standing TheiaCast up in a company: a real hostname, HTTPS, Single Sign-On, and a fleet of devices. It goes deep on the parts that are not covered elsewhere — identity-provider app registrations and SSO troubleshooting in particular.
- Quick Start — the fastest path to a running server on a workbench.
- Installation Guide — full server and client install steps for every platform.
- User Guide — day-to-day operation: content, playlists, scheduling, broadcasts, remote control.
- Administrator Guide — settings, branding, licensing, backups, database maintenance.
Contents
- 1. Architecture & network topology
- 2. Enterprise prerequisites
- 3. Server install (summary)
- 4. Hostname, HTTPS & reverse proxy
- 5. Authentication model (accounts, roles, MFA)
- 6. Single Sign-On — how it works
- 7. Microsoft Entra ID SSO (app registration + config)
- 8. Generic OpenID Connect SSO (Okta, Keycloak, …)
- 9. SAML 2.0 SSO (ADFS, legacy IdPs)
- 10. Managing users once SSO is live
- 11. Device / client rollout
- 12. Troubleshooting
- 13. Appendix: settings keys, redirect URIs, claims
1. Architecture & network topology
A TheiaCast deployment has three moving parts:
- Server — three containers: an nginx frontend (serves the admin web app and proxies the API), the ASP.NET Core backend (REST API + WebSocket hub), and PostgreSQL.
- Admin browsers — staff open the web app over HTTPS. This is also where the SSO redirect lands.
- Display devices — Raspberry Pi, another Linux mini-PC (Intel NUC, etc.), Windows, or Android/Fire TV. Each runs a full (non-headless) Chromium client in kiosk mode that makes an outbound connection to the backend and holds a WebSocket open.
| Flow | Source → Destination | Port / Protocol |
|---|---|---|
| Admin web app | Staff browser → frontend | 443 / HTTPS (80 redirects to 443) |
| API + WebSocket (admin) | Staff browser → frontend → backend | 443 / HTTPS + WSS (proxied to backend 8080) |
| Device control + telemetry | Display device → backend | 5001 / HTTP + WS (or 443 / WSS if devices go through the proxy) |
| SSO redirect | Staff browser ↔ identity provider | 443 / HTTPS, returns to https://<your-host>/login |
| SSO token validation | backend → identity provider discovery / JWKS | 443 / HTTPS outbound from the server |
| Teams Relay RTMP ingest | Microsoft Teams datacenter → mediamtx container | 1935 / RTMP, the only port this service publishes — HLS output and its Control API stay internal to the compose network, reached by devices through the frontend’s /hls/ proxy |
Firewall note: devices only need outbound reach to the server — there is no inbound connection to a device. If your identity provider is cloud-hosted (Entra ID, Okta…), the server itself also needs outbound HTTPS so it can fetch the provider’s signing keys to validate SSO tokens.
2. Enterprise prerequisites
Before you start, line these up:
| Item | Why |
|---|---|
| A DNS name for the server e.g. signage.example.com | SSO redirect URIs must be registered against a fixed origin. A bare IP works for a lab but not for SSO in production. |
| A TLS certificate for that name | Identity providers refuse non-HTTPS redirect URIs (except http://localhost). Browsers block the SSO popup/redirect over plain HTTP. |
| Admin access to your identity provider | You need to create an app registration / client and (often) grant admin consent. |
| Server outbound HTTPS | The backend pulls the provider’s /.well-known/openid-configuration and JWKS to verify tokens. |
| SMTP relay (optional) | For device-offline email alerts and scheduled report emails. See the Administrator Guide. |
| Licence key (optional) | Only needed for more than 3 devices or custom branding. Everything else — SSO, MFA, floor map, the canvas designer, reporting — is on the free tier. |
3. Server install (summary)
The full step-by-step is in the Installation Guide. In short: install Docker, drop in a docker-compose.yml and .env, then docker compose up -d.
Minimum docker-compose.yml
services:
postgres:
image: postgres:15-alpine
container_name: theiacast-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: theiacast
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
backend:
image: ghcr.io/jimmyeao/theiacast-backend:latest
container_name: theiacast-backend
environment:
- ConnectionStrings__Default=Host=postgres;Port=5432;Database=theiacast;Username=postgres;Password=${POSTGRES_PASSWORD}
- ASPNETCORE_ENVIRONMENT=Production
- Jwt__Secret=${JWT_SECRET}
ports:
- "5001:8080"
depends_on: [postgres]
volumes:
- theiacast_media:/app/wwwroot
- theiacast_dataprotection_keys:/app/dataprotection-keys
- ${SSL_CERT_PATH:-./ssl-certs}:/app/ssl-certs
restart: unless-stopped
frontend:
image: ghcr.io/jimmyeao/theiacast-frontend:latest
container_name: theiacast-frontend
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
volumes:
- ${SSL_CERT_PATH:-./ssl-certs}:/etc/nginx/ssl:ro
depends_on: [backend]
restart: unless-stopped
volumes:
postgres_data:
theiacast_media:
theiacast_dataprotection_keys:Database-name gotcha: the Postgres volume is created the first time the stack starts and is bound to whatever POSTGRES_DB was then. Changing POSTGRES_DB later does not rename an existing database — your data stays in the original one on the same volume. Pick the name once.
Teams Relay needs a fourth service (mediamtx, RTMP ingest on 1935) and three extra backend environment variables — not shown in this trimmed-down example. See the Installation Guide’s full docker-compose.yml and .env, and the Teams Relay guide.
.env
POSTGRES_PASSWORD=change-me-to-a-long-random-string JWT_SECRET=another-long-random-string-at-least-32-chars # Optional custom ports / cert path # HTTP_PORT=80 # HTTPS_PORT=443 # SSL_CERT_PATH=./ssl-certs
Bring it up and check:
docker compose up -d docker compose ps docker compose logs -f backend
SSO does not need any extra environment variables. The redirect URL is derived in the browser from the origin you load the app from, and all provider settings are entered later in the admin UI and stored in the database. The only requirement is that everyone reaches the app at one canonical HTTPS URL — see the next section.
4. Hostname, HTTPS & reverse proxy
Decide the one URL staff will always use, for example https://signage.example.com. Everything downstream — SSO redirect URIs, bookmarks, device server URLs — keys off it. If people sometimes reach the app by IP and sometimes by name, SSO will fail for the mismatched one because the redirect URI will not be an exact match.
Option A — terminate TLS in the bundled frontend
Mount your certificate and key where the frontend container expects them and restart it:
# Files expected inside the container at /etc/nginx/ssl/ # cert.pem (full chain) # key.pem (private key) mkdir -p ~/theiacast/ssl-certs cp fullchain.pem ~/theiacast/ssl-certs/cert.pem cp privkey.pem ~/theiacast/ssl-certs/key.pem chmod 600 ~/theiacast/ssl-certs/key.pem docker compose restart frontend docker compose logs frontend | grep TheiaCast: # expect: certificate found, serving HTTPS on 443
Pin the certificate path. The compose mount defaults to the relative ./ssl-certs, which resolves against wherever docker compose is invoked. If you start the stack from a systemd unit or a different working directory it will silently look in the wrong place and fall back to plain HTTP. Set an absolute path in .env: SSL_CERT_PATH=/home/USER/theiacast/ssl-certs.
Managed browsers. A self-signed or internal-CA certificate on a bare IP still throws a warning, and Edge/Chrome under SSLErrorOverrideAllowed=false give no click-through at all. Push cert.pem (or your internal root) to the admin machines’ Trusted Root Certification Authorities store via GPO or Intune, or use a publicly trusted certificate for the chosen hostname.
Option B — put your own reverse proxy in front
Common in companies that already run nginx / Traefik / Caddy / an F5. Point it at the frontend container and make sure WebSocket upgrades are passed through — the admin UI keeps a live socket open.
# nginx example
server {
listen 443 ssl;
server_name signage.example.com;
ssl_certificate /etc/letsencrypt/live/signage.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/signage.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:80; # the frontend container
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocket
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3600s; # keep long-lived sockets alive
}
}
server {
listen 80;
server_name signage.example.com;
return 301 https://$host$request_uri;
}Do not serve the app over plain HTTP in production and expect SSO to work. The identity provider will reject an http:// redirect URI for any host other than localhost, and the browser blocks cross-origin auth on insecure origins.
5. Authentication model
TheiaCast has three independent ways a person can sign in, and you can enable any combination:
- Local login — username + password held by TheiaCast, optionally with TOTP MFA. On by default.
- Microsoft Entra ID — native integration built for Entra / Microsoft 365 tenants.
- Generic OpenID Connect — any standards-compliant OIDC provider (Okta, Keycloak, and others).
- SAML 2.0 — SP-initiated Web-SSO for SAML-only identity providers (ADFS, legacy on-prem IdPs).
Roles
Every user has one role. The three built-in roles are:
| Role | Can do |
|---|---|
| Viewer | Read-only: dashboards, device status, logs, reports. |
| Operator | Everything a Viewer can, plus manage content, playlists, broadcasts, and drive devices. |
| Admin | Full control, including users, roles, authentication settings, licensing and backups. |
Custom roles can also be created (Admin → Users & Roles). Any existing role name — built-in or custom — can be chosen as the default that SSO assigns to new accounts.
MFA
TheiaCast’s own MFA is authenticator-app TOTP, enrolled per account under the user menu. It is independent of any identity provider. Accounts created through SSO start without TheiaCast MFA — the expectation is that MFA is enforced at the identity provider, which is where those users actually authenticate.
API keys
Automation uses API keys (Admin → API Keys), not user logins. Keys are unaffected by SSO. See the API documentation.
6. Single Sign-On — how it works
TheiaCast supports three SSO methods: Microsoft Entra ID, generic OIDC, and SAML 2.0. The flow below covers Entra/OIDC (authorization code + PKCE, an ID token exchanged in the browser). SAML is a different, browser-level POST flow with no code exchange — see SAML 2.0 SSO for its own step-by-step. The user-resolution logic below (match by identity, then email, then auto-provision) and the anti-lockout rule apply identically to all three.
The flow (Entra ID / generic OIDC)
- A user clicks Sign in with Microsoft or Sign in with <your provider> on the TheiaCast login page.
- The browser is redirected to the identity provider using the authorization code flow with PKCE. By default TheiaCast is a public client with no client secret; for a confidential provider (e.g. Google Workspace) a secret can be stored server-side — see Confidential providers.
- After the user authenticates (and passes any MFA / conditional-access policy), the provider redirects back to
https://<your-host>/loginwith a one-time code. - That code is exchanged for an ID token — in the browser for a public client, or via the TheiaCast backend when a server-side client secret is configured — and the ID token is validated by the backend.
- The backend fetches the provider’s signing keys from its discovery document and validates the ID token: signature, issuer, audience (= your client ID), and expiry.
- On success the backend resolves the user (below) and issues its own TheiaCast session token. From then on the TheiaCast session is independent of the provider session.
Requested scopes are always openid profile email. The redirect URI is always the app origin plus /login — there is nothing to configure for it in TheiaCast, but it must be registeredexactly at the provider.
How a token becomes a TheiaCast user
On each SSO login the backend does, in order:
- Match by external identity. If a TheiaCast user is already linked to this provider + this subject (
oidfor Entra,subfor OIDC,NameIDfor SAML), sign them in. - Otherwise link by email. If a user already exists with the same email address, that account is linked to the external identity and used from then on. This is how you connect your existing local admin account to SSO — just make sure the email matches.
- Otherwise auto-provision. A new user is created with the configured default role and a username taken from the token’s name (or the email prefix), with a provider suffix if that name is already taken.
Optional group / role-claim mapping. By default the role is set once, at provisioning time, from the default role you configure — to give an SSO user a higher role, change it afterwards in Admin → Users. Configure Settings → Authentication → Group / role claim mapping to drive the role from the provider’s group/role claims instead — applies on every login, so moving a user between provider groups changes their TheiaCast role at next sign-in. Works with Entra, generic OIDC, and SAML. See Mapping groups to roles under Entra ID below.
The anti-lockout rule
At least one authentication method must always be enabled. You can only turn off local login once Microsoft Entra ID, generic OIDC, or SAML is enabled and fully configured (client ID plus tenant/issuer for Entra/OIDC; IdP entity ID, SSO URL and certificate for SAML). TheiaCast rejects the change otherwise, so you cannot accidentally lock everyone out.
Recommended rollout: keep local login on, get SSO working end-to-end for a real user, promote that user to Admin, then — if you want SSO-only — disable local login.
7. Microsoft Entra ID SSO
Use this path for Microsoft 365 / Entra ID tenants. It is a single-tenant integration: TheiaCast validates tokens against your tenant specifically.
Part A — Create the app registration
Step 1 — New registration
- Go to the Microsoft Entra admin center → Identity → Applications → App registrations → New registration.
- Name:
TheiaCast(whatever your users will recognise on the consent screen). - Supported account types: Accounts in this organizational directory only (single tenant).
- Redirect URI: platform Single-page application (SPA), value
https://signage.example.com/login(your real host). - Click Register.
Step 2 — Copy the two IDs
From the app’s Overview page, note the Application (client) ID and the Directory (tenant) ID. You will paste both into TheiaCast.
Step 3 — Confirm the platform & redirect URI
- Open Authentication.
- Under Platform configurations there must be a Single-page application entry with
https://signage.example.com/login. - Add extra SPA redirect URIs here if staff also use another hostname (e.g. an internal name). Each must be exact, including scheme and the
/loginpath. No trailing slash. - Leave the implicit grant checkboxes (Access tokens, ID tokens) unticked — the SPA uses the code flow with PKCE, not implicit.
- Do not add a client secret. It is a public client.
- Save.
Step 4 — API permissions
- Open API permissions.
- Ensure Microsoft Graph delegated permissions
openid,profile,emailare present (add them via Add a permission → Microsoft Graph → Delegated if not). - If your tenant requires admin consent, click Grant admin consent for <tenant>.
Step 5 — (Optional) make sure an email claim is present
TheiaCast links SSO logins to existing accounts by email. Entra always includes preferred_username (usually the UPN). If your UPNs are not email addresses, open Token configuration → Add optional claim → ID → email so the real address is carried, and make sure mailboxes have a primary SMTP address.
Part B — Configure TheiaCast
- Sign in to TheiaCast as an Admin → Settings → Authentication.
- Tick Enable Microsoft Entra ID login.
- Application (Client) ID — paste from Step 2.
- Directory (Tenant) ID — paste from Step 2.
- Default role for new Microsoft users — leave at
Viewerunless you have a reason not to. - Save. Keep local login enabled for now.
Restricting who can sign in
To restrict sign-in to approved people rather than everyone in the tenant, use two controls, either or both:
- Entra-side: in the Entra portal, Enterprise applications → your app → Properties → Assignment required = Yes, then assign users/groups under Users and groups. Unassigned users are blocked by Entra itself before any token reaches TheiaCast. (Assigning groups requires Entra ID P1+; individual users are free.)
- TheiaCast-side: Settings → Authentication → “Only allow SSO sign-in for existing accounts”. With this on, a valid SSO identity still gets in if their group claims match a group/role mapping below — group membership counts as pre-approval and the account is auto-provisioned with the mapped role. Identities matching neither an existing account nor a mapping are rejected. Applies to Entra, generic OIDC, and SAML alike, and covers providers with no assignment feature of their own.
Mapping groups to roles
Entra security-group membership can drive the TheiaCast role at every sign-in:
- In the app registration (not the enterprise app): Token configuration → Add groups claim (a separate button next to “Add optional claim” — groups is not in the optional claims list). Choose Groups assigned to the application (recommended: the token then only carries relevant groups) or Security groups, and tick ID under token types. Equivalent manifest setting:
"groupMembershipClaims": "ApplicationGroup". The ID token will now carry agroupsclaim containing the groups’ Object IDs (GUIDs — visible on each group’s Overview page in Entra). - In TheiaCast, Settings → Authentication → Group / role claim mapping: add a row per group, pasting its Object ID and choosing the TheiaCast role, e.g.
sg_users_app_Theiacast_admins→ Admin. Order matters — the first matching row wins, so put the most privileged group first. - Users with no matching group get the provider’s configured Default Role. Mapping applies on every login, so moving a user between Entra groups changes their role at next sign-in.
App-role values work too: if the registration defines app roles (manifest appRoles), the token’s roles claim values can be entered in the same table.
Group membership as the access list. When “Only allow SSO sign-in for existing accounts” is on and a role map is configured, group membership becomes authoritative — a matched group auto-provisions the account (no manual user creation needed), and an identity matching no mapping is rejected and has its refresh tokens revoked, even if they signed in before. Removing someone from the Entra group locks them out at their next sign-in. Caveats: already-issued access tokens live until they expire, and a local password (if the account has one and local login is enabled) still works — disable local sign-in too for strict enforcement.
Claim overage. If a user is a member of a very large number of groups, Entra emits a claim overage pointer instead of the groups claim — the mapping then sees no groups and falls back to the default role. Keep membership under ~150 groups, or prefer Groups assigned to the application, which avoids overage entirely.
Part C — Test
- Open the TheiaCast login page in a private window at
https://signage.example.com. - Click Sign in with Microsoft, complete the Microsoft prompt, and confirm you land back in the dashboard.
- As an existing Admin, open Users and confirm the new (or newly linked) account is present with the expected role.
- Check Logs / the audit trail for a
login.msalevent.
Part D — (Optional) enforce SSO only
Once at least one Entra user is an Admin, return to Settings → Authentication, untick Enable local login, and Save. If you have not fully configured SSO the save is rejected — that is the anti-lockout guard doing its job. Keep one local Admin password in your password manager as a break-glass account even if you disable local login in the UI.
8. Generic OpenID Connect SSO
This path works with any provider that implements OpenID Connect discovery. The default flow is a public client using PKCE with no client secret. Providers that require a client secret at the token endpoint — Google Workspace is the common one — are also supported by storing that secret server-side; see Confidential providers below.
What TheiaCast requires of the provider
- A discovery document at
<issuer>/.well-known/openid-configuration, reachable from the server. - Support for the authorization code flow with PKCE. A public / SPA client type (no secret) is simplest; a confidential client with a secret also works — see Confidential providers.
- An ID token whose
issclaim exactly equals the Issuer URL you enter in TheiaCast, after any single trailing slash is removed. Ordinal, case-sensitive match. - An
audclaim equal to the client ID you enter. - A stable
subclaim.emailandnameare used for account linking and the display name if present. - The redirect URI
https://<your-host>/loginregistered exactly.
The issuer-match rule catches people out.
TheiaCast trims one trailing / from the Issuer URL you enter and then requires the token’s iss to match that value character-for-character. If your provider issues tokens with iss ending in / (some do), there is no value you can enter that will match, and validation fails. See Known limitations below.
Provider compatibility
| Provider | Client type to create | Issuer URL to enter | Works? |
|---|---|---|---|
| Okta | OIDC → Single-Page Application | https://<org>.okta.com or https://<org>.okta.com/oauth2/<id> | Yes |
| Keycloak | Public client, standard flow, PKCE S256 | https://<host>/realms/<realm> | Yes |
| Microsoft Entra ID | SPA app registration | https://login.microsoftonline.com/<tenant>/v2.0 | Yes — but prefer the native path |
| PingFederate / PingOne, Authentik, other OIDC | Public / SPA client with PKCE | Whatever the discovery doc reports as issuer | Yes, if iss has no trailing slash (a client secret is fine — store it server-side) |
| Auth0 | SPA | — | No — iss has a trailing slash |
| Google Workspace | Web application (OAuth client ID) | https://accounts.google.com | Yes — with a server-side Client Secret |
Walkthrough — Okta
Create the app
- Okta Admin Console → Applications → Applications → Create App Integration.
- Sign-in method OIDC - OpenID Connect; application type Single-Page Application. Next.
- Grant type: Authorization Code (PKCE is implied for SPA; do not add Client Credentials).
- Sign-in redirect URIs:
https://signage.example.com/login. - Sign-out redirect URIs:
https://signage.example.com/login(or your home page). - Assignments: assign the groups / people who should be able to sign in. Save.
Collect the values
- Client ID — from the app’s General tab.
- Issuer — decide between the Okta org authorization server (
https://<org>.okta.com) and a custom authorization server (https://<org>.okta.com/oauth2/default). Open<issuer>/.well-known/openid-configurationin a browser and use whichever value appears as"issuer"there, verbatim.
Walkthrough — Keycloak
- In the target realm: Clients → Create client. Type OpenID Connect, Client ID e.g.
theiacast. Next. - Client authentication: Off (this makes it a public client). Standard flow: On. Direct access grants: off. Next.
- Valid redirect URIs:
https://signage.example.com/login. Web origins:https://signage.example.com. Save. - Advanced → Proof Key for Code Exchange Code Challenge Method: S256.
- Issuer URL for TheiaCast:
https://<keycloak-host>/realms/<realm>(confirm against the discovery document). Client ID:theiacast.
Configure TheiaCast
- Admin → Settings → Authentication → tick Enable generic OIDC / SSO login.
- Provider name — the label on the login button, e.g.
OktaorCompany SSO. - Issuer URL — exactly as it appears in the discovery document’s
issuerfield. - Client ID — from the provider.
- Client Secret — leave blank for a public / SPA client. Fill it in only for a confidential provider such as Google Workspace — see below.
- Default role —
Viewerrecommended. - Save, then test from a private window exactly as in the Entra section.
Confidential providers (Google Workspace)
Some providers — notably Google Workspace — only issue a “Web application” OAuth client that requires a client secret at the token endpoint, even with PKCE, and offer no public SPA client type. For these, put the secret in the Client Secret field on Settings → Authentication. When it is set, the browser hands the authorization code to the TheiaCast backend, which performs the token exchange with the stored secret and returns only the ID token — the secret is never exposed to the browser.
Walkthrough — Google Workspace
- Google Cloud console → APIs & Services → Credentials → Create credentials → OAuth client ID.
- Application type Web application.
- Authorized redirect URI:
https://signage.example.com/login(your real host, exact, no trailing slash). - Create, then copy the Client ID and Client secret.
- In TheiaCast: Issuer URL
https://accounts.google.com, Client ID and Client Secret as above, then Save and test from a private window.
The server needs outbound HTTPS to oauth2.googleapis.com for the token exchange, on top of the discovery / JWKS fetch it already makes.
Known limitations
- Auth0 issues ID tokens with
iss=https://<tenant>.auth0.com/— note the trailing slash. TheiaCast’s issuer check strips one trailing slash from your configured value and then compares exactly, so an Auth0 token’sisscan never match. Auth0 as a direct provider is not supported today. A client secret does not help here — the mismatch is in the issuer, not the token exchange. - Any provider whose
issclaim ends in a trailing slash has the same problem as Auth0, regardless of client type. - Workaround for a trailing-slash issuer: front that identity with a broker that presents a compliant
iss— Keycloak or Okta federating upstream — and point TheiaCast at the broker. For Microsoft shops, use the native Entra path.
9. SAML 2.0 SSO (ADFS, legacy IdPs)
SAML covers the providers OIDC can’t: ADFS, older on-prem directory integrations, and legacy enterprise IdP setups that only speak SAML. TheiaCast is a Service Provider — it cannot act as an identity provider for other applications.
The flow is browser-level — there is no code exchange:
- The user clicks Sign in with {provider}; the browser hits
GET /api/auth/saml/login. - TheiaCast builds a SAML
AuthnRequest(HTTP-Redirect binding) and 302s the browser to the IdP’s SSO URL. - The IdP authenticates the user and POSTs the signed assertion back to the ACS endpoint
https://<your-host>/api/auth/saml/acs(HTTP-POST binding). - The backend validates the signature against the configured IdP certificate (never a certificate carried inside the response itself), checks issuer, audience and timestamps, and requires
InResponseToto match a request it issued — IdP-initiated (unsolicited) sign-in is not supported, and assertion replay is rejected. - On success the browser lands back on
/loginwith a normal TheiaCast session.
Walkthrough — AD FS
AD FS is the most common reason to reach for SAML at all — it doesn’t speak OIDC. The trust is configured on the AD FS side as a relying party trust, with claim rules controlling what the assertion carries.
- AD FS Management console → Relying Party Trusts → Add Relying Party Trust → Claims aware.
- Select Data Source: Import data about the relying party published online or on a local network, and give it TheiaCast’s own SP metadata URL:
https://<your-host>/api/auth/saml/metadata. (If AD FS can’t reach TheiaCast directly, download that URL and use Import from a file instead.) - Access control policy: Permit everyone or a specific group.
- Finish — this opens Edit Claim Issuance Policy. Add two rules: Send LDAP Attributes as Claims (
E-Mail-Addresses→ E-Mail Address), then Transform an Incoming Claim (E-Mail Address → Name ID, format Email). The second rule is what populatesNameID— without it AD FS’s default assertion has no usable subject identifier. - AD FS signs responses and assertions by default with its own token-signing certificate — nothing to set on the AD FS side. On TheiaCast, use Fetch against AD FS’s federation metadata URL (
https://<adfs-host>/FederationMetadata/2007-06/FederationMetadata.xml) to pull the entity ID, SSO URL and current signing cert in one step — the easiest way to stay in sync if that certificate is ever rotated, which is the single most common cause of a previously-working SAML login breaking.
Setup
In Settings → Authentication → Enable SAML 2.0 login: paste the IdP’s metadata URL and click Fetch to fill entity ID, SSO URL and signing certificate automatically — or enter all three by hand. Set the default role for auto-provisioned accounts. On the IdP, register a relying party / enterprise application with:
- Entity ID / Identifier:
https://<host>/api/auth/saml - Reply / ACS URL:
https://<host>/api/auth/saml/acs - SP metadata document: served at
https://<host>/api/auth/saml/metadata
Both SP URLs can be overridden in settings if the deployment sits behind a reverse proxy with a different public address. The assertion must be signed (response- or assertion-level) and carry a NameID; email and displayNameattributes are recommended for account linking, same as OIDC.
10. Managing users once SSO is live
- Bootstrapping the first SSO admin. Either (a) make sure your existing local Admin account has the same email as your SSO identity, so the first SSO login links to it and inherits Admin; or (b) let the first SSO login auto-provision as Viewer, then have an existing Admin promote it in Admin → Users.
- Promoting / changing roles. With no group/role mapping configured, this is always done in Admin → Users and sticks until changed again. If a group/role mapping is configured (Entra shown; also works for generic OIDC and SAML), the role is instead re-derived from the provider’s group/role claims on every login — a manual change in Admin → Users only lasts until their next sign-in.
- Deactivating someone. Removing or disabling them at the identity provider stops new logins immediately. Delete or disable the TheiaCast user as well to end their current session and be tidy.
- Duplicate accounts. These appear when the email in the token doesn’t match an existing user (typo, changed address, missing
emailclaim). Fix the email on the intended account first, delete the stray one, and have the person sign in again to re-link. - Break-glass. Keep one local Admin whose password is in your password manager, even in SSO-only mode, so a provider outage can’t lock you out of your own signage.
11. Device / client rollout
SSO covers people. Display devices authenticate with a per-device token instead — they never sign in interactively. Full platform steps are in the Installation Guide; the essentials:
- In TheiaCast: Devices → Add Device, give it a name and ID, and copy the generated device token.
- On the device, run the installer and provide the server URL and that token.
Linux / Raspberry Pi (one line, as root):
curl -sSL https://gist.githubusercontent.com/jimmyeao/9a6922cdf1c69da92f1be9c7e3463052/raw/install-theiacast-client.sh | sudo bash
The installer detects the hardware and display setup, installs Chromium, writes /opt/theiacast-client/.env, and registers a systemd service that survives reboots and self-updates.
Windows:
Download the Windows client from the Download page, run the setup, enter the server URL and device token, and it installs as a Windows service.
Server URL for devices: if TheiaCast is only reachable on HTTPS via your reverse proxy, point devices at https://signage.example.com and make sure the proxy forwards WebSocket upgrades. If devices are on the same LAN as the server you can also use http://<server-ip>:5001 directly.
Fleet tips
- Use device groups and tag rules so a device gets the right playlist automatically from its name or tags — you avoid assigning each one by hand.
- Devices ship telemetry (CPU, memory, disk, screenshots) on a schedule; the intervals are configurable in Settings.
- Turn on device-offline email alerts (Settings) so you hear about a dead screen before a visitor does.
- Client updates roll out from the server — see the Installation Guide’s Updating the Client section.
12. Troubleshooting
Where to look first
- Backend logs:
docker compose logs -f backend— token validation failures are logged here with the reason. - In-app: the Logs page and the audit trail. Successful SSO logins record
login.msal,login.oidc, orlogin.saml; provisioning and account-linking are logged too. - Browser dev tools on the
/loginpage — the Network tab shows the redirect to the provider and the callback, and any provider error is in the callback URL’s query string.
SSO
“Redirect URI mismatch” / AADSTS50011 / redirect_uri error at the provider
The provider has no exact match for https://<the-origin-you-loaded>/login. Register that precise value (scheme + host + /login, no trailing slash) as a SPA redirect URI. If staff reach the app by more than one hostname, every one needs its own entry. Loading the app by IP when only the DNS name is registered triggers this.
“Invalid Microsoft token” / “Invalid OIDC token” after returning to TheiaCast
The backend rejected the ID token. Common causes: the Client ID in TheiaCast doesn’t match the app’s (audience check), the Tenant/Issuer is wrong (issuer check), server clock skew (expiry check — sync NTP), or the server cannot reach the provider’s discovery / JWKS endpoint. The backend log line names which check failed.
OIDC: token valid at the provider but always “Invalid OIDC token”
Almost always the issuer string. Open <issuer>/.well-known/openid-configurationand copy the "issuer" value verbatim into TheiaCast. If that value ends with /, this provider cannot be used directly (see Known limitations).
Discovery fetch fails / long hang then failure on the server
The server needs outbound HTTPS to the identity provider. Check egress firewall rules and, if you use one, the outbound proxy. Test from the host: curl -v <issuer>/.well-known/openid-configuration.
“No subject claim found in OIDC token”
The provider isn’t sending a sub claim in the ID token. Make sure the request includes the openid scope (TheiaCast always asks for it) and that no provider-side claim policy strips sub from the ID token.
Login works but the person lands as Viewer / a brand-new account
Expected for a first-time SSO user — role comes from the configured default, and linking to an existing account only happens when the email matches. Fix the email on the intended account, delete the duplicate, promote the user in Admin → Users, and have them sign in again.
“Cannot disable local login without … configured and enabled”
The anti-lockout guard. Enable Entra, OIDC, or SAML and fill in every required field (client ID + tenant/issuer for Entra/OIDC; IdP entity ID, SSO URL and certificate for SAML), save that first, confirm a real SSO login works, then disable local login.
SAML: bounces straight back to /login?saml_error=…
The saml_error text names which check failed. Most common: the response is signed by a different certificate than the one configured (re-run Fetch against the IdP metadata URL, or re-paste the current signing certificate — on AD FS this usually means the token-signing cert rotated), or the assertion is unsigned (TheiaCast requires the assertion itself to be signed, not just the outer response). IdP-initiated (unsolicited) sign-in is not supported — always start the login from TheiaCast’s own button, not a tile/bookmark on the IdP side.
SAML: sign-in succeeds at the IdP but the new account has no usable name
The assertion’s NameID is empty. On AD FS specifically this means the Transform an Incoming Claim → Name ID rule is missing — see the AD FS walkthrough. On other IdPs, confirm the SSO configuration sends a NameID, not just attribute claims.
Browser bounces back to /login and nothing happens
Usually a silent callback error. Open dev tools → Network, repeat the login, and inspect the request to /login?code=… (or ?error=…) and the follow-up call to /auth/msal or /auth/oidc. Third-party-cookie or storage restrictions in a locked-down browser profile can also break the flow — test in a normal private window.
interaction_in_progress on clicking the Microsoft button
A previous sign-in redirect was interrupted — most often an untrusted-certificate interstitial on the trip back — and left a stale marker in the browser. Clear the site’s Session Storage (dev tools → Application, or sessionStorage.clear()), reload, and retry. Serving the app over HTTPS with a certificate the browser trusts prevents it recurring; current frontend builds also clear the marker automatically on load.
More than one SSO button appears (Entra / OIDC / SAML)
That’s intended if you enabled more than one. They’re fully independent; each callback is matched to whichever flow started it. Disable the ones you don’t want in Settings → Authentication.
Server
502 / blank page behind your reverse proxy
Proxy can’t reach the frontend container, or WebSocket upgrade headers aren’t forwarded. Confirm proxy_pass target, Upgrade/Connection headers, and a generous proxy_read_timeout.
Mixed-content warnings / API calls blocked
The app is loaded over HTTPS but something is calling HTTP. Make sure the whole path is HTTPS and the proxy sets X-Forwarded-Proto https.
Containers won’t start / DB errors
docker compose logs postgres and … backend. A wrong POSTGRES_PASSWORD vs the backend connection string is the usual culprit after an .env edit.
Devices
Device shows offline
Check the device can reach the server URL (curl the health endpoint), the device token is right, and the path allows WebSockets. On Linux: sudo journalctl -u theiacast-client -f.
Auto-login / kiosk didn’t come up on a Pi or mini-PC
The installer needs a graphical session to attach Chromium to. See the Installation Guide’s Raspberry Pi / Linux notes — on a desktop-less box it sets up a minimal X server; on a desktop distro it may need autologin enabled.
13. Appendix
Redirect URI cheat-sheet
| Setting at the provider | Value |
|---|---|
| Redirect / callback URI (Entra, OIDC) | https://<your-host>/login |
| Post-logout redirect URI (Entra, OIDC) | https://<your-host>/login (or /) |
| Allowed web origin (if asked) | https://<your-host> |
| Client type | SPA / public, Authorization Code + PKCE, no secret — or a confidential Web client with a server-side secret (Google Workspace) |
| Scopes | openid profile email |
| Entity ID / Identifier (SAML) | https://<your-host>/api/auth/saml |
| Reply / ACS URL (SAML) | https://<your-host>/api/auth/saml/acs |
Claim mapping
| TheiaCast uses | Entra ID claim | Generic OIDC claim | SAML attribute |
|---|---|---|---|
| Stable identity (required) | oid | sub | NameID |
| Email (for linking) | preferred_username → email | email → preferred_username | email attribute (optional, recommended) |
| Display name | name | name | displayName attribute (optional) |
| Role | Not from a claim — set from the configured default role at first login, then managed in Admin → Users. | ||
Authentication settings (stored server-side)
All set from Settings → Authentication; listed here for reference and for reading the database if you ever need to.
| Key | Meaning |
|---|---|
auth:local:enabled | Username/password login on/off |
auth:msal:enabled | Microsoft Entra ID login on/off |
auth:msal:clientId | Entra Application (client) ID |
auth:msal:tenantId | Entra Directory (tenant) ID |
auth:msal:defaultRole | Role for auto-provisioned Entra users |
auth:oidc:enabled | Generic OIDC login on/off |
auth:oidc:providerName | Label on the login button |
auth:oidc:issuer | OIDC issuer URL (discovery base) |
auth:oidc:clientId | OIDC client ID |
auth:oidc:clientSecret | Optional server-side client secret for confidential providers (Google Workspace). Blank = public / PKCE client |
auth:oidc:defaultRole | Role for auto-provisioned OIDC users |
auth:saml:enabled | SAML 2.0 login on/off |
auth:saml:providerName | Label on the login button |
auth:saml:idpEntityId / auth:saml:idpSsoUrl / auth:saml:idpCertificate | IdP identity, SSO URL and signing cert (auto-filled from the IdP metadata URL) |
auth:saml:spEntityId / auth:saml:acsUrl | Optional public-URL overrides (defaults derived from the request) |
auth:saml:defaultRole | Role for auto-provisioned SAML users |
Ports & URLs
| What | Default |
|---|---|
| Frontend (admin web app) | 80 → 443 |
| Backend API + WebSocket (host-exposed) | 5001 (container 8080) |
| PostgreSQL | 5432 (keep internal) |
| SSO redirect target | https://<your-host>/login |
| Swagger / API reference | http://<server>:5001/swagger |
Related documentation
- Installation Guide — full server and client install steps
- User Guide — content, playlists, scheduling, broadcasts, remote control
- Administrator Guide — settings, branding, licensing, backups, database
- API Documentation — JWT and API-key auth for automation
- FAQ — quick answers and common fixes
Need help?
Stuck on an identity-provider quirk we haven’t covered? Email [email protected] with the backend log line from the failed login and the name of your identity provider.