Keycloak Setup for Octopus

This document describes what the IAM/infrastructure team needs to configure in Keycloak to enable keycloak in AUTH_PROVIDER for an Octopus deployment.

This setup assumes Octopus is being registered as a new client inside an existing realm that already hosts other applications. Octopus uses the Authorization Code + PKCE flow — no client secret is required.

Placeholders used throughout this document

Placeholder

Replace with

https://keycloak.example.org

Your Keycloak server URL

myrealm

Your Keycloak realm name

octopus

The client ID you choose for Octopus


1. Register the Octopus client

In the Keycloak Admin Console (https://keycloak.example.org/admin):

  1. Select the target realm (myrealm).

  2. Go to Clients → Create client.

  3. Fill in the General settings tab:

    Setting

    Value

    Client type

    OpenID Connect

    Client ID

    octopus

    Name

    Octopus

    Description

    Octopus monitoring dashboard

  4. Capability config tab — see Public client explained below:

    Setting

    Value

    Client authentication

    Off

    Standard flow

    On

    Direct access grants

    Off

    All others

    Off

  5. Login settings tab — see Multiple deployments below:

    Setting

    Value

    Root URL

    (leave blank)

    Home URL

    (leave blank)

    Valid redirect URIs

    one entry per deployment (see below)

    Valid post logout redirect URIs

    one entry per deployment (see below)

    Web origins

    + (special value — auto-derives from redirect URIs)

  6. Save the client.


Public client explained

What “Client authentication: Off” means

Keycloak supports two client types:

Type

Client authentication

How it proves its identity

Public (what we use)

Off

Does not prove its identity at all

Confidential

On

Authenticates with a client secret or certificate

A public client cannot hold a secret safely — the Octopus frontend runs entirely in the user’s browser, so any secret embedded in the JavaScript bundle or config.js would be visible to anyone who opens DevTools. There is no server-side component that could keep a secret on the frontend’s behalf.

Is this insecure? No, as long as PKCE is used (which Octopus does).

PKCE (Proof Key for Code Exchange) replaces the client secret for public clients:

  1. Before redirecting to Keycloak, the browser generates a random code_verifier.

  2. It sends a hashed version (code_challenge) to Keycloak with the login request.

  3. After the user authenticates, Keycloak returns a one-time code.

  4. The browser exchanges that code for a token by also sending the original code_verifier. Keycloak checks the hash — only the browser that started the flow has the unhashed code_verifier, so a third party who intercepts the code cannot use it.

This is the same mechanism used by mobile apps and desktop applications. Keycloak enforces PKCE automatically when the client is public.

Turn on Standard flow, everything else off. “Standard flow” is the Authorization Code flow — the only flow compatible with PKCE. “Direct access grants” (Resource Owner Password) bypasses PKCE and the Keycloak login page; it should be off for browser clients.


Multiple deployments

Why Root URL and Home URL are usually left blank

These two fields serve as a base prefix that Keycloak prepends to any relative paths you enter in “Valid redirect URIs”. If you set Root URL = https://octopus.example.org and then enter /callback as a redirect URI, Keycloak expands it to https://octopus.example.org/callback.

When you have multiple deployments at different origins, leave both fields blank. Enter each deployment as its own full absolute URI instead — Keycloak won’t prepend anything, and all of them are accepted.

Configuring all Octopus deployment URLs

Add one wildcard entry per deployment origin to Valid redirect URIs:

http://localhost:3000/*
http://192.168.0.2/*
http://192.168.0.3/*
http://192.168.49.2/integration-octopus/ska-octopus-frontend/*
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/*
https://k8s.miditf.internal.skao.int/octopus/ska-octopus-frontend/*
https://k8s.lowitf.internal.skao.int/octopus/ska-octopus-frontend/*

The trailing /* lets Keycloak accept any path under that origin, which covers both the root and deployments under a sub-path.

Add the same list (without /*) to Valid post logout redirect URIs — these are the pages Keycloak redirects to after a user logs out:

http://localhost:3000
http://192.168.0.2
http://192.168.0.3
http://192.168.49.2/integration-octopus/ska-octopus-frontend
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
https://k8s.miditf.internal.skao.int/octopus/ska-octopus-frontend
https://k8s.lowitf.internal.skao.int/octopus/ska-octopus-frontend

For Web origins, enter the special value +. This tells Keycloak to derive the allowed CORS origins automatically from the redirect URI list above — you do not need to maintain a separate list. Every origin you added as a redirect URI will be permitted to make cross-origin requests to Keycloak’s token endpoint.

One client for all deployments: all deployments share the same octopus client. You do not need a separate client per environment — Keycloak matches the specific URL at login time and returns to exactly that URL. Each deployment has its own config.js pointing to the same KEYCLOAK_CLIENT_ID.

Per-deployment config.js

Each deployment’s public/config.js is identical except for AUTH_PROVIDER if some environments use only local auth. The Keycloak settings are always the same:

// All deployments use the same Keycloak settings
KEYCLOAK_URL: 'https://keycloak.example.org',
KEYCLOAK_REALM: 'myrealm',
KEYCLOAK_CLIENT_ID: 'octopus',

The redirect URI Keycloak uses is determined at runtime from the browser’s window.locationkeycloak-js sends the current page URL automatically. You do not need to configure it anywhere in config.js.


2. Create client roles

Octopus maps Keycloak roles to its own internal role levels (Viewer → Admin). The recommended approach is to use client roles inside the octopus client — this keeps Octopus roles isolated from other applications in the realm.

Role naming and ROLE_PREFIX

Before creating the roles, decide whether this deployment uses a role prefix. A prefix scopes role names to a specific telescope or site so that a user in a shared Keycloak realm can hold assignments for multiple deployments without them conflicting.

ROLE_PREFIX

Role names 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.

Creating the roles

Go to Clients → octopus → Roles → Create role and create these five roles (example uses ROLE_PREFIX=low — substitute your prefix or omit it):

Role name

Octopus role

Access level

octopus-low-viewer

Viewer (level 1)

Read-only

octopus-low-user

User (level 2)

Standard operations

octopus-low-operator

Operator (level 3)

Telescope operator access

octopus-low-engineer

Engineer (level 4)

Engineering / configuration

octopus-low-admin

Admin (level 5)

Full administrative access

Role hierarchy: levels are inclusive — an Operator can do everything a User and Viewer can. Users only need to be assigned to their highest role.

Alternative: realm roles

If you prefer realm-level roles (shared across applications), create the same role names under Realm roles instead. The backend resolves roles from both realm_access.roles and resource_access.<client_id>.roles, so either approach works. Set KEYCLOAK_ROLE_* environment variables to match whatever names you choose if they differ from the ROLE_PREFIX-derived defaults.



4. Assign users

For each user who needs Octopus access:

  1. Go to Users → <username> → Role mapping.

  2. Click Assign role → Filter by clients → octopus.

  3. Assign exactly one Octopus client role (their highest level).

Users with no Octopus role can still authenticate via Keycloak and receive Viewer access by default. Set KEYCLOAK_ROLE_VIEWER to an empty string in the backend environment to reject users without an explicit role assignment.


5. Backend environment variables

# Enable Keycloak (alongside or instead of local auth)
AUTH_PROVIDER=local,keycloak

# Keycloak connection
KEYCLOAK_URL=https://keycloak.example.org
KEYCLOAK_REALM=myrealm
KEYCLOAK_CLIENT_ID=octopus

# Audience claim validation (set only after adding the audience mapper above)
KEYCLOAK_AUDIENCE=octopus

# Deployment-specific role prefix — must match the role names created in step 2
# and the ROLE_PREFIX set in the frontend config.js.
# When set, role names default to octopus-<prefix>-viewer, octopus-<prefix>-admin, …
# Leave empty for legacy unprefixed names (octopus-viewer, octopus-admin, …).
ROLE_PREFIX=low

# Map Keycloak role names to Octopus role levels.
# Only needed if you used non-standard names that don't follow the ROLE_PREFIX pattern.
# KEYCLOAK_ROLE_ADMIN=octopus-low-admin
# KEYCLOAK_ROLE_ENGINEER=octopus-low-engineer
# KEYCLOAK_ROLE_OPERATOR=octopus-low-operator
# KEYCLOAK_ROLE_USER=octopus-low-user
# KEYCLOAK_ROLE_VIEWER=octopus-low-viewer

The backend fetches JWKS automatically from:

https://keycloak.example.org/realms/myrealm/protocol/openid-connect/certs

JWKS keys are cached in memory and refreshed automatically on key rotation.


6. Frontend configuration (public/config.js)

AUTH_PROVIDER: ['local', 'keycloak'],   // or just ['keycloak'] to disable local login

KEYCLOAK_URL: 'https://keycloak.example.org',
KEYCLOAK_REALM: 'myrealm',
KEYCLOAK_CLIENT_ID: 'octopus',

// Must match ROLE_PREFIX in the backend environment.
// The frontend uses this to derive the expected Keycloak role names from JWT claims
// before the backend role sync completes. Leave empty for legacy unprefixed names.
ROLE_PREFIX: 'low',

// No KEYCLOAK_REDIRECT_URI needed — keycloak-js uses the current page URL automatically.

If the role names in your Keycloak realm differ from the ROLE_PREFIX-derived pattern, you can pin them individually:

// Only needed when role names don't follow the octopus-<prefix>-<role> pattern:
KEYCLOAK_ROLE_ADMIN:    'my-custom-admin-role',
KEYCLOAK_ROLE_ENGINEER: 'my-custom-engineer-role',
KEYCLOAK_ROLE_OPERATOR: 'my-custom-operator-role',
KEYCLOAK_ROLE_USER:     'my-custom-user-role',
KEYCLOAK_ROLE_VIEWER:   'my-custom-viewer-role',

For a production deployment that disables the local username/password fallback, set AUTH_PROVIDER: ['keycloak'] in both the frontend config and the backend AUTH_PROVIDER environment variable.


7. How the authentication flow works

User clicks "Login (Keycloak)"
  └─→ Frontend redirects to Keycloak login page (PKCE, code_challenge)
        └─→ User authenticates in Keycloak
              └─→ Keycloak redirects back to the current deployment URL with ?code=...
                    └─→ keycloak-js exchanges code for access token (RS256 JWT)
                          └─→ Token stored in localStorage (octopus.jwt)
                                └─→ Every API request includes Authorization: Bearer <token>
                                      └─→ Backend fetches JWKS, verifies RS256 signature
                                            └─→ Maps client roles → Octopus role level
                                                  └─→ GraphQL resolvers enforce minimum level

Token refresh is handled automatically by keycloak-js — the user is silently re-authenticated before the token expires without needing to log in again.


8. Verification checklist

After completing the setup, verify the integration:

  • Open Octopus — a Keycloak login button is visible alongside (or instead of) Local.

  • Click Keycloak — browser redirects to the Keycloak realm login page.

  • Authenticate as a test user — redirect returns to Octopus.

  • User is logged in and their name appears in the header.

  • Check the Profile tab in Settings — it shows the Keycloak issuer URL.

  • GET /auth/me returns the correct role and role_level.

  • Log out — browser redirects to the Keycloak logout endpoint and the session is cleared.

  • Repeat the login test from a second deployment URL (e.g. localhost and one of the k8s URLs).

If authentication fails, check:

  1. Redirect URI mismatch — the most common error. Keycloak rejects the login if the URL it redirected to is not in the Valid redirect URIs list. The error message will say Invalid parameter: redirect_uri. Add the missing URL to the list.

  2. CORS error in the browser console — the origin is not in Web origins. Ensure you used + (not a manual list) so all redirect URI origins are included.

  3. Backend logs — look for Failed to fetch Keycloak JWKS (network/firewall issue) or Could not validate credentials (wrong audience or expired token).

  4. Audience mismatch — if KEYCLOAK_AUDIENCE is set but the token aud claim differs, validation fails silently. Inspect the token with:

    GET /auth/validate?token=<paste-token-here>
    

    or paste it into jwt.io to read the aud claim.


9. Running multiple auth providers simultaneously

Octopus supports running local, keycloak, and msal at the same time. Each provider shows its own login button; the backend detects the provider from the JWT iss claim at validation time.

# Backend: all three providers enabled
AUTH_PROVIDER=local,keycloak,msal
// Frontend config.js
AUTH_PROVIDER: ['local', 'keycloak', 'msal'],

Users from any provider can log in concurrently. Each session is completely independent.