Skip to main content
Version: v3.8.0

External OIDC Authentication

Appendix - not the default path

PoT v3.8.0 ships with Embedded OIDC Authentication enabled by default. Use this page only when your organization requires a dedicated identity provider (Okta, Azure AD, Keycloak, or any OIDC-compliant IdP).

External mode sets AUTH_MODE=external. Controller uses one confidential OIDC client for browser (EdgeOps Console) and CLI (potctl) authentication. Access and refresh tokens are issued by the IdP. Controller validates access JWTs via the issuer JWKS and maps claims to RBAC.

When to use external OIDC

ScenarioUse external OIDC
Corporate SSO requiredYes
Greenfield lab or quick startNo - use Embedded OIDC
IdP-managed MFA and password policyYes
No external IdP availableNo

Minimal Control Plane YAML

Set auth.mode: external and point to your IdP issuer. potctl maps these fields to Controller environment variables at deploy time.

auth:
mode: external
issuerUrl: https://auth.example.com/realms/myrealm
client:
id: pot-controller
secret: "<confidential-client-secret>"
controller:
publicUrl: https://controller.example.com:51121
consoleUrl: https://console.example.com:8008

Local Docker installs can set auth.insecureAllowHttp: true when publicUrl uses http://. Production deploys should use HTTPS and valid TLS certificates.

Full YAML field reference: Control Plane YAML. Runtime env mapping: Controller configuration.

Controller environment (minimum)

VariableRequiredExample
AUTH_MODEYesexternal
OIDC_ISSUER_URLYeshttps://auth.example.com/realms/myrealm
OIDC_CLIENT_IDYespot-controller
OIDC_CLIENT_SECRETYesConfidential client secret
CONTROLLER_PUBLIC_URLYeshttps://controller.example.com
CONSOLE_URLYes (browser login)https://console.example.com
AUTH_INSECURE_ALLOW_HTTPDevelopment onlytrue when using http://localhost:*

Flows and endpoints

Use caseGrant / flowController endpoint
Browser (EdgeOps Console)Authorization code + PKCE S256GET /api/v3/user/oauth/authorize → IdP → GET /api/v3/user/oauth/callback
CLI (potctl)Resource owner password (direct access)POST /api/v3/user/login
Session refreshRefresh tokenPOST /api/v3/user/refresh
ProfileBearer access token (+ UserInfo)GET /api/v3/user/profile

Browser login uses the OAuth BFF. The Console does not POST passwords directly to the IdP. Legacy browser POST /user/login is removed in v3.8.

IdP client - required settings

Client type

  • OpenID Connect
  • Confidential client (client authentication enabled; uses OIDC_CLIENT_SECRET)
  • Public clients are not supported for the Controller OAuth BFF

Enabled grant types / flows

FlowRequired forNotes
Standard flow (authorization code)Browser Sign inRequired
Direct access grants (ROPC)CLI POST /user/loginRequired if potctl uses password login against the IdP
Implicit flow-Off (not used)

PKCE

Controller always sends PKCE S256 on browser authorize (code_challenge, code_challenge_method=S256).

ProviderSetting
KeycloakPKCE Method = S256 (Capability config)
OthersAllow or require PKCE S256 on the authorization code flow

Disabling PKCE on the IdP is a development workaround only, not recommended for production.

Redirect URIs

Register exactly:

{CONTROLLER_PUBLIC_URL}/api/v3/user/oauth/callback

Examples:

  • Production: https://controller.example.com/api/v3/user/oauth/callback
  • Local: http://localhost:51121/api/v3/user/oauth/callback

Avoid overly broad wildcards in production.

Web origins (CORS)

If the Console calls the Controller API from the browser, allow:

{CONSOLE_URL}

Example: http://localhost:8008

Scopes

Requested by Controller (browser OAuth BFF)

Controller sends this scope string on authorize:

openid profile email groups offline_access
ScopePurpose
openidOIDC baseline; sub, id_token
profilepreferred_username, display name
emailEmail claim; identity linking
groupsRBAC group membership (see below)
offline_accessRefresh token on authorization code flow

IdP client scope assignment

Every requested scope must be assigned to the client as a default and/or optional client scope.

ScopeTypical assignment
openid, profile, email, rolesDefault
groupsOptional (scope name must be groups, not group)
offline_accessOptional

Common error: invalid_scope when a scope is requested but not assigned to the client, or when the scope name is wrong (group vs groups).

RBAC / groups mapping

Controller resolves RBAC Group subjects from the access token (in order):

  1. resource_access[{OIDC_CLIENT_ID}].roles - client roles on the OAuth client (recommended)
  2. Top-level roles array (if present)
  3. groups array - from the groups scope and a group mapper

Group names are lowercased before RBAC lookup. IdP role and group names should align with Controller RBAC groups (for example admin, viewer).

User identity (User subject)

Controller reads the User subject from JWT claims (first match):

preferred_usernameusernameemailsub

Ensure at least one stable identifier is present for RBAC User bindings.

Provider notes

IdP patternRBAC mapping
Client roles on the OAuth client matching OIDC_CLIENT_IDUses resource_access (simplest)
Realm or group mappersCreate a client scope named groups with a Group Membership mapper that emits a groups claim

Issuer discovery (minimum metadata)

The issuer at OIDC_ISSUER_URL must expose:

EndpointUsed for
authorization_endpointBrowser OAuth BFF
token_endpointCode exchange, ROPC, refresh
jwks_uriBearer JWT validation
userinfo_endpointGET /user/profile in external mode
revocation_endpointOptional; best-effort logout

MFA and forced password change

  • Browser: enforced by the IdP during authorize (for example a required password update action)
  • CLI: IdP password-grant policy applies
  • Controller does not run embedded interaction UI in external mode

Verification

Browser

  1. EdgeOps Console Sign in → IdP login page (no invalid_scope or PKCE errors)
  2. Callback → {CONSOLE_URL}/login#accessToken=...&refreshToken=...
  3. GET /api/v3/user/profile with Authorization: Bearer <accessToken> → 200

CLI

curl -sS -X POST '{CONTROLLER_PUBLIC_URL}/api/v3/user/login' \
-H 'Content-Type: application/json' \
-d '{"email":"<user>","password":"<pass>","totp":""}'

Expect { "accessToken", "refreshToken" } (IdP tokens).

Refresh

curl -sS -X POST '{CONTROLLER_PUBLIC_URL}/api/v3/user/refresh' \
-H 'Content-Type: application/json' \
-d '{"refreshToken":"<refresh>"}'

Troubleshooting

ErrorLikely cause
invalid_scopeMissing groups or offline_access on the client; wrong scope name (group vs groups)
Missing parameter: code_challenge_methodPKCE required on IdP but not sent by Controller (upgrade Controller)
redirect_uri mismatchCallback URL not registered exactly
Login works, no refreshToken in browser hashoffline_access not requested or not assigned on the IdP client
403 on API routesToken valid but RBAC groups/roles not mapped; check client roles or groups claim

HA notes

For multi-replica browser login with an external IdP, set AUTH_SESSION_STORE_TYPE=database. See Controller configuration.

DocumentTopic
Embedded OIDC AuthenticationDefault auth mode
Controller configurationFull env var tables
Controller REST API/user/oauth/*, /user/login, /user/profile
EdgeOps Console configurationconsoleUrl and runtime auth.* paths
Group 3See anything wrong with the document? Help us improve it!