Microsoft Entra ID Setup for Octopus (MSAL Mode)
This document describes what the IT/IAM team needs to configure in Microsoft Entra ID
to enable msal in AUTH_PROVIDER for an Octopus deployment.
Octopus uses the Authorization Code + PKCE flow — no client secret is required on either the frontend or the backend. The backend validates tokens by fetching Azure’s public signing keys (JWKS) directly from Microsoft’s well-known endpoint, the same approach used for Keycloak.
Placeholders used throughout this document
Placeholder
Replace with
octopusYour app registration display name
<client-id>The Application (client) ID GUID of the app registration
<tenant-id>Your Entra tenant ID (GUID)
1. App Registration
There is one app registration for Octopus. It acts as both the API resource (the backend validates tokens issued for it) and the public SPA client (the frontend logs in with it via PKCE).
Create (or update) an app registration:
Setting |
Value |
|---|---|
Display name |
|
Supported account types |
Single tenant (recommended) |
Platform |
Single-page application |
Client authentication |
Off (public client — no secret required) |
Expose an API
Under Expose an API, set the Application ID URI. Entra defaults it to
api://<client-id>— accept the default or choose a friendly name (e.g.api://octopus-prod).Add a scope:
Setting
Value
Scope name
octopusWho can consent
Admins and users
Display/description
Access Octopus on behalf of the user
The full scope string becomes
<Application ID URI>/octopus, for exampleapi://<client-id>/octopus. This is the value forMSENTRA_API_SCOPEinconfig.js.The backend’s
OCTOPUS_AAD_AUDIENCEis the Application ID URI alone — i.e.MSENTRA_API_SCOPEwith the scope name (/octopus) removed:MSENTRA_API_SCOPE = api://<client-id>/octopus OCTOPUS_AAD_AUDIENCE = api://<client-id>
Redirect URIs
Under Authentication → Redirect URIs, add one entry per deployment origin. Entra requires exact URIs — no wildcards:
http://localhost:3000
https://k8s.stfc.skao.int/integration-octopus/ska-octopus-frontend
https://k8s.mid.internal.skao.int/octopus/ska-octopus-frontend
https://k8s.low.internal.skao.int/octopus/ska-octopus-frontend
No client secret needed
The backend only validates the token the frontend sends. Azure publishes its public signing keys at:
https://login.microsoftonline.com/<tenant-id>/discovery/v2.0/keys
The backend fetches these keys automatically from the token’s iss claim. No secret,
certificate, or client credential is required.
2. Role Mapping — Choose One Strategy
Octopus maps Entra identity to five internal role levels. Two strategies are available.
Role naming and ROLE_PREFIX
Before creating app roles or security groups, decide whether this deployment uses a role prefix. A prefix scopes role names to a specific telescope or site so that a user who belongs to multiple deployments can hold all their assignments in a single Entra tenant without them conflicting.
|
App role values to create |
|---|---|
(empty) |
|
|
|
|
|
|
|
Set ROLE_PREFIX to the same value in both the backend environment and the frontend
config.js. Leave it empty to keep the legacy unprefixed names.
Selection rules when a user holds roles from multiple deployments:
Only roles matching the deployment’s own prefix are considered; others are silently ignored.
If a user holds several roles under the same prefix (e.g.
octopus-low-operatorandoctopus-low-admin), the highest level wins — no need to remove lower roles.
Strategy 1 — App roles (recommended)
Define App roles directly on the app registration. Entra places them in the token’s
roles claim — no security group GUIDs required.
Go to the app registration → App roles → Create app role.
Using
ROLE_PREFIX=lowas an example (adjust the values for your prefix):Display name
Value
Description
Allowed member types
Octopus LOW Viewer
octopus-low-viewerRead-only access
Users/Groups
Octopus LOW User
octopus-low-userStandard user operations
Users/Groups
Octopus LOW Operator
octopus-low-operatorTelescope operator access
Users/Groups
Octopus LOW Engineer
octopus-low-engineerEngineering/configuration
Users/Groups
Octopus LOW Admin
octopus-low-adminFull administrative access
Users/Groups
Without a prefix, use
octopus-viewer,octopus-user, etc. as the values.Under Enterprise applications → octopus → Users and groups, assign users or groups to each role.
Set
ROLE_PREFIXin the backend environment (see §3). TheMSENTRA_ROLE_*variables are computed automatically fromROLE_PREFIXand do not need to be set explicitly unless you used non-standard values in step 1.
Role hierarchy: levels are inclusive upward — an Operator can do everything a User and Viewer can do. Users only need their highest role assigned.
Strategy 2 — Security groups (backward-compatible)
If you prefer to manage access via Entra security groups, Octopus reads the token’s
groups claim and matches Object IDs against environment variables.
Create 5 Security groups (not Microsoft 365):
Group name
Octopus role
Level
octopus-viewerViewer
1
octopus-userUser
2
octopus-operatorOperator
3
octopus-engineerEngineer
4
octopus-adminAdmin
5
Note each group’s Object ID (GUID).
In the app registration manifest, set:
"groupMembershipClaims": "SecurityGroup"
This ensures security group memberships appear in the
groupsclaim.Assign users to exactly one group (their highest role level).
Set the backend environment variables:
OCTOPUS_ROLE_VIEWER_ID=<Object ID of octopus-viewer> OCTOPUS_ROLE_USER_ID=<Object ID of octopus-user> OCTOPUS_ROLE_OPERATOR_ID=<Object ID of octopus-operator> OCTOPUS_ROLE_ENGINEER_ID=<Object ID of octopus-engineer> OCTOPUS_ROLE_ADMIN_ID=<Object ID of octopus-admin>
If both
rolesandgroupsclaims are present, app roles take priority.
3. Backend Environment Variables
# Enable MSAL (alongside or instead of other providers)
AUTH_PROVIDER=msal
# Expected audience claim — the Application ID URI from §1 (MSENTRA_API_SCOPE without the scope name)
# e.g. if MSENTRA_API_SCOPE = api://<client-id>/octopus, then:
OCTOPUS_AAD_AUDIENCE=api://<client-id>
# Deployment-specific role prefix — must match the app role values created in §2
# and ROLE_PREFIX in the frontend config.js.
# Computes octopus-<prefix>-viewer, octopus-<prefix>-admin, … by default.
# Leave empty for legacy unprefixed names.
ROLE_PREFIX=low
# --- Strategy 1: app role name overrides (only needed if you used non-standard names) ---
# MSENTRA_ROLE_VIEWER=octopus-low-viewer
# MSENTRA_ROLE_USER=octopus-low-user
# MSENTRA_ROLE_OPERATOR=octopus-low-operator
# MSENTRA_ROLE_ENGINEER=octopus-low-engineer
# MSENTRA_ROLE_ADMIN=octopus-low-admin
# --- Strategy 2: security group Object IDs (omit if using strategy 1) ---
# OCTOPUS_ROLE_VIEWER_ID=<guid>
# OCTOPUS_ROLE_USER_ID=<guid>
# OCTOPUS_ROLE_OPERATOR_ID=<guid>
# OCTOPUS_ROLE_ENGINEER_ID=<guid>
# OCTOPUS_ROLE_ADMIN_ID=<guid>
# Optional: grant authenticated Entra users with no role assignment a minimum role.
# Values: viewer, user, operator, engineer, admin. Leave empty to return HTTP 403.
MSENTRA_DEFAULT_ROLE=
The backend derives the Azure JWKS URL automatically from the iss claim in the token —
no additional configuration is needed for that.
4. Frontend Configuration (public/config.js)
AUTH_PROVIDER: ['msal'], // or ['local', 'msal'] to keep local login as fallback
MSENTRA_CLIENT_ID: '<client-id>', // Application (client) ID of the app registration
MSENTRA_TENANT_ID: '<tenant-id>', // Entra tenant ID
MSENTRA_REDIRECT_URI: 'https://<your-octopus-origin>',
MSENTRA_API_SCOPE: 'api://<client-id>/octopus', // Application ID URI + scope name
// Must match ROLE_PREFIX in the backend environment.
// For MSAL, role resolution is done entirely server-side, so this value is used
// only by Keycloak role derivation (if Keycloak is also enabled). Leave empty
// to use legacy unprefixed names.
ROLE_PREFIX: 'low',
No MSENTRA_CLIENT_SECRET is needed — the app registration is a public client.
5. How the authentication flow works
User clicks "Login (Microsoft)"
└─→ Frontend redirects to Entra login page (PKCE, code_challenge)
└─→ User authenticates in Entra
└─→ Entra redirects back with ?code=...
└─→ MSAL exchanges code for access token (RS256 JWT)
└─→ Token stored in localStorage (octopus.jwt)
└─→ Every API request includes Authorization: Bearer <token>
└─→ Backend fetches JWKS from:
https://login.microsoftonline.com/<tid>/discovery/v2.0/keys
└─→ Validates RS256 signature, expiry, audience
└─→ Maps roles/groups → Octopus role level
└─→ GraphQL resolvers enforce minimum level
JWKS keys are cached in memory and refreshed automatically on key rotation.
6. Running multiple auth providers simultaneously
Octopus supports running local, keycloak, and msal at the same time.
Each shows its own login button; the backend detects the provider from the JWT iss
claim at validation time.
# Backend
AUTH_PROVIDER=local,keycloak,msal
// Frontend config.js
AUTH_PROVIDER: ['local', 'keycloak', 'msal'],
7. Verification checklist
Open Octopus — a Microsoft login button is visible.
Click Microsoft — browser redirects to the Entra login page.
Authenticate — redirect returns to Octopus.
User is logged in and their name appears in the header.
Check Profile tab in Settings — shows the Entra issuer URL and role.
GET /auth/mereturns the correctroleandrole_level.Log out — session is cleared and the login button reappears.
If authentication fails, check:
Invalid redirect URI — Entra rejects the login if the redirect URI is not in the app registration. Add the missing URL under Authentication → Redirect URIs.
Audience mismatch — if
OCTOPUS_AAD_AUDIENCEdoesn’t match theaudclaim, validation fails. Inspect the token at jwt.ms to read theaud. It is usuallyapi://<client-id>— the Application ID URI without the scope name.No role assigned — users authenticated but with no app role or matching group receive HTTP 403. Assign them to at least the Viewer role.
JWKS unavailable — the backend logs
Failed to fetch Entra JWKS. Check network access from the backend container tologin.microsoftonline.com.