External OIDC Authentication
Datasance PoT v3.9.0 ships with Embedded OIDC Authentication enabled by default. Use this page when your organization requires a dedicated identity provider (Okta, Microsoft Entra ID, Keycloak, or any OIDC-compliant IdP).
Edgelet agent routes (/api/v3/agent/*) use fog-token auth. OIDC does not apply to field agents.
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.
For default group names, claim order, and per-IdP recipes, see External OIDC providers.
When to use external OIDC
| Scenario | Use external OIDC |
|---|---|
| Corporate SSO required | Yes |
| Greenfield lab or quick start | No. Use Embedded OIDC |
| IdP-managed MFA and password policy | Yes |
| No external IdP available | No |
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 in Learn. Runtime env mapping: Controller configuration.
Do not set legacy KC_*, auth.realm, or auth.realmKey.
Controller environment (minimum)
| Variable | Required | Example |
|---|---|---|
AUTH_MODE | Yes | external |
OIDC_ISSUER_URL | Yes | Full issuer URL (not the IdP homepage) |
OIDC_CLIENT_ID | Yes | pot-controller |
OIDC_CLIENT_SECRET | Yes | Confidential client secret |
CONTROLLER_PUBLIC_URL | Yes | https://controller.example.com |
CONSOLE_URL | Yes (browser login) | https://console.example.com |
TRUST_PROXY | When TLS terminates at ingress | true |
AUTH_SESSION_STORE_TYPE | HA + browser login | database (mysql/postgres) |
AUTH_INSECURE_ALLOW_HTTP | Development only | true when using http://localhost:* |
Discovery uses GET {OIDC_ISSUER_URL}/.well-known/openid-configuration.
Flows and endpoints
| Use case | Grant / flow | Controller endpoint |
|---|---|---|
| Browser (EdgeOps Console) | Authorization code + PKCE S256 | GET /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 refresh | Refresh token | POST /api/v3/user/refresh |
| Profile | Bearer access token (+ UserInfo) | GET /api/v3/user/profile |
| User APIs and operator WebSockets | Bearer access JWT | /api/v3/* except agent routes |
Browser login uses the OAuth BFF. The Console does not POST passwords directly to the IdP. Legacy browser POST /user/login was removed on platform train v3.8.0.
In external mode there is no local AuthUsers row for IdP users. MFA and password policy are owned by the IdP.
IdP client - required settings
Client type
Create one confidential Web client for Controller. Public SPA or native clients are not supported for the OAuth BFF.
| Setting | Required value |
|---|---|
| Protocol | OpenID Connect |
| Client authentication | On (client_secret_basic or client_secret_post) |
| PKCE | S256 required or allowed |
| Implicit / hybrid / device code | Off |
Enabled grant types / flows
| Flow | Required for | Notes |
|---|---|---|
| Standard flow (authorization code) | Browser Sign in | Required |
Refresh token (offline_access) | Console/CLI refresh | Required for POST /api/v3/user/refresh |
| Direct access grants (ROPC) | CLI POST /user/login | Required if potctl uses password login against the IdP |
| Implicit flow | — | Off |
If your organization disabled ROPC (common on Microsoft Entra ID), browser login still works; CLI password login may not.
Redirect URIs
Register exactly:
{CONTROLLER_PUBLIC_URL}/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}.
Scopes
Controller sends this scope string on authorize (not configurable):
openid profile email groups offline_access
Every scope must be assigned to the client as default and/or optional client scope. The scope name must be groups, not group.
Access token requirements
Controller validates the access token, not the ID token, on API calls. Groups and roles on the ID token alone are ignored for authorization.
The access token must:
- Be a JWT (opaque tokens fail).
- Have
issequal to the discovered issuer. - Have
audequal toOIDC_CLIENT_ID(string or array containing it). - Carry RBAC claims on the access token (see External OIDC providers).
- Not be a refresh token (
token_usemust not berefresh).
If aud is Microsoft Graph, Okta api://default, or any API identifier that is not the client ID, Bearer validation fails with 401.
System roles (summary)
If the access token contains a group or role named admin, sre, developer, or viewer (case-insensitive), Controller grants that system role without a RoleBinding. Do not assign agent-admin or microservice to human IdP users.
Full mapping, custom RoleBindings, and IdP recipes: External OIDC providers.
Issuer discovery (minimum metadata)
| Endpoint | Used for |
|---|---|
authorization_endpoint | Browser OAuth BFF |
token_endpoint | Code exchange, ROPC, refresh |
jwks_uri | Bearer JWT validation |
userinfo_endpoint | GET /user/profile in external mode |
revocation_endpoint | Optional; best-effort logout |
MFA and forced password change
- Browser: enforced by the IdP during authorize.
- CLI: IdP password-grant policy applies.
- Controller does not run embedded interaction UI in external mode.
Verification
Browser
- EdgeOps Console Sign in → IdP login (no
invalid_scopeor PKCE errors). - Callback →
{CONSOLE_URL}/login#accessToken=...&refreshToken=.... - Decode the access token:
audisOIDC_CLIENT_ID;issmatches the issuer; roles/groups claim contains a system role name when expected. GET /api/v3/user/profilewithAuthorization: Bearer <accessToken>→ 200.
CLI
curl -sS -X POST '{CONTROLLER_PUBLIC_URL}/api/v3/user/login' \
-H 'Content-Type: application/json' \
-d '{"email":"<login-id>","password":"<pass>","totp":""}'
email is the IdP login identifier (username or UPN). Expect { "accessToken", "refreshToken" }.
Refresh
curl -sS -X POST '{CONTROLLER_PUBLIC_URL}/api/v3/user/refresh' \
-H 'Content-Type: application/json' \
-d '{"refreshToken":"<refresh>"}'
Troubleshooting
| Error | Likely cause |
|---|---|
invalid_scope | Missing groups or offline_access on the client; wrong scope name; provider requires a full URI scope |
Missing parameter: code_challenge_method | PKCE misconfiguration on IdP |
redirect_uri mismatch | Callback not registered exactly; URI registered as SPA instead of Web |
Login works, no refreshToken in browser hash | offline_access not assigned on the IdP client |
| 401 on API routes | Opaque access token; aud is not OIDC_CLIENT_ID; wrong iss |
| 403 on API routes | RBAC groups/roles not on the access token |
| CLI login fails, browser works | ROPC / direct access grants disabled |
OAuth state errors on HA | AUTH_SESSION_STORE_TYPE still memory with multiple replicas |
HA notes
For multiple Controller replicas with browser login, set AUTH_SESSION_STORE_TYPE=database (mysql/postgres). memory is only safe for a single replica.
Related docs
| Document | Topic |
|---|---|
| Embedded OIDC Authentication | Default auth mode |
| External OIDC providers | Groups, roles, Keycloak, Entra ID, Okta, Auth0 |
| Controller configuration | Full env var tables |
| Controller API | OAuth and user endpoints |
| EdgeOps Console configuration | consoleUrl and runtime auth.* paths |