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

octopus

Your 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

octopus

Supported account types

Single tenant (recommended)

Platform

Single-page application

Client authentication

Off (public client — no secret required)

Expose an API

  1. 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).

  2. Add a scope:

    Setting

    Value

    Scope name

    octopus

    Who can consent

    Admins and users

    Display/description

    Access Octopus on behalf of the user

    The full scope string becomes <Application ID URI>/octopus, for example api://<client-id>/octopus. This is the value for MSENTRA_API_SCOPE in config.js.

    The backend’s OCTOPUS_AAD_AUDIENCE is the Application ID URI alone — i.e. MSENTRA_API_SCOPE with 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.

ROLE_PREFIX

App role values to create

(empty)

octopus-viewer, octopus-user, … octopus-admin

low

octopus-low-viewer, octopus-low-user, … octopus-low-admin

mid

octopus-mid-viewer, octopus-mid-user, … octopus-mid-admin

stfc

octopus-stfc-viewer, octopus-stfc-user, … octopus-stfc-admin

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-operator and octopus-low-admin), the highest level wins — no need to remove lower roles.



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.

  1. Create 5 Security groups (not Microsoft 365):

    Group name

    Octopus role

    Level

    octopus-viewer

    Viewer

    1

    octopus-user

    User

    2

    octopus-operator

    Operator

    3

    octopus-engineer

    Engineer

    4

    octopus-admin

    Admin

    5

  2. Note each group’s Object ID (GUID).

  3. In the app registration manifest, set:

    "groupMembershipClaims": "SecurityGroup"
    

    This ensures security group memberships appear in the groups claim.

  4. Assign users to exactly one group (their highest role level).

  5. 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 roles and groups claims 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/me returns the correct role and role_level.

  • Log out — session is cleared and the login button reappears.

If authentication fails, check:

  1. 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.

  2. Audience mismatch — if OCTOPUS_AAD_AUDIENCE doesn’t match the aud claim, validation fails. Inspect the token at jwt.ms to read the aud. It is usually api://<client-id> — the Application ID URI without the scope name.

  3. No role assigned — users authenticated but with no app role or matching group receive HTTP 403. Assign them to at least the Viewer role.

  4. JWKS unavailable — the backend logs Failed to fetch Entra JWKS. Check network access from the backend container to login.microsoftonline.com.