MAQPNADocs

Keycloak and OIDC single sign-on

Sign in to the MAQPNA console and CLI with Keycloak or any OIDC identity provider, map groups to admin roles, and use the same identity provider for approvals, user approval (CIBA) and external MCP clients.

By default the gateway admin API accepts one static admin token. That is fine on a laptop, but in production every person who lists sessions, approves a tool call or revokes an agent should sign in as themselves. MAQPNA does this with OpenID Connect (OIDC): the gateway verifies access tokens from your identity provider and turns the groups in each token into MAQPNA roles. The console and the maqpna CLI then sign in through the identity provider, and the audit ledger records the real approver.

This page uses Keycloak, which the MAQPNA interop suite tests every night. The gateway's verifier is a generic OIDC verifier, so the same settings work for other OIDC providers such as Entra ID, Okta or Google. Only Keycloak is tested end to end.

flowchart LR
    U["Person<br/>browser or terminal"] -->|"1 sign in<br/>code + PKCE"| K["Keycloak<br/>realm maqpna"]
    K -->|"2 access token<br/>aud, groups"| U
    U -->|"3 Bearer token"| C["Console or CLI"]
    C -->|"4 /v1 admin API"| G["MAQPNA gateway"]
    G -->|"discovery, JWKS"| K
    G --> R["roles from groups<br/>viewer, approver, auditor,<br/>admin, killswitch"]
    G --> L[("audit ledger<br/>approver = preferred_username")]

When to use it#

  • More than one person uses the console or the admin API.
  • You need separation of duties: approvers who cannot approve their own calls, and four-eyes approvals.
  • You want the person an agent acts for to confirm a sensitive call on their own device (user approval, CIBA).
  • IDE or desktop coding agents should call MCP servers through the gateway with tokens from your identity provider.

Prerequisites#

  • A Keycloak server that the gateway can reach over HTTPS, or any OIDC provider that publishes /.well-known/openid-configuration.
  • MAQPNA installed with the Helm chart (see Production install with Helm).
  • The maqpna CLI on your machine.

Step 1: Create the realm, groups and client#

In Keycloak, create a realm (here maqpna) with these objects:

Object Settings
Groups maqpna-viewers, maqpna-approvers, maqpna-auditors, maqpna-admins, maqpna-sre. These are the chart's default role groups.
Client maqpna-admin Public client, standard flow (authorization code) with PKCE. Valid redirect URIs: the console URL (for example http://127.0.0.1:8088/*) and http://127.0.0.1/* for the CLI loopback login.
Mapper: Audience On client maqpna-admin: Included Client Audience maqpna-admin, added to the access token.
Mapper: Group Membership Claim name groups, Full group path off.

Two Keycloak defaults break MAQPNA if you leave them:

  1. Issuer. Keycloak builds iss from the host name of the request. Pin it to the URL the gateway uses (--hostname=https://idp.example.eu). Otherwise tokens fetched through another host name fail with token issuer mismatch.
  2. Audience and groups. Access tokens carry aud: account by default, and group paths start with /. Add the Audience mapper and turn Full group path off. You can instead set adminAuth.oidc.groupsClaim to a claim with plain names, such as realm_access.roles (dotted paths are supported).

Step 2: Set the Helm values#

adminAuth:
  mode: oidc                       # token | oidc | both
  oidc:
    issuer: https://idp.example.eu/realms/maqpna
    audience: maqpna-admin
    clientID: maqpna-admin         # published to the console and CLI
    scopes: []                     # optional extra scopes for the login
    groupsClaim: groups
    usernameClaim: ""              # default: preferred_username, then email, then sub
    caSecret: ""                   # Secret with key ca.crt for a private IdP CA
  roles:                           # role -> groups, or "user:<name>"
    viewer: ["maqpna-viewers"]
    approver: ["maqpna-approvers"]
    auditor: ["maqpna-auditors"]
    admin: ["maqpna-admins"]
    killswitch: ["maqpna-sre"]
  namespaceRoles:                  # roles in one namespace only
    team-a:
      approver: ["team-a-leads"]
Mode What the admin API accepts
token (default) Only the static admin token (MAQPNA_ADMIN_TOKEN).
oidc Only OIDC tokens. The chart does not inject the static token.
both OIDC tokens, plus the static token as a break-glass credential. Each use of it is logged as break-glass admin token used and recorded with issuer urn:maqpna:break-glass.

The chart refuses to render oidc or both without adminAuth.oidc.issuer. The gateway fetches discovery and the provider's public keys (JWKS) through its residency-checked dialer. If you set sovereignty.allowedEgressHosts, add the Keycloak host to it.

Apply the values:

helm upgrade maqpna oci://$REGISTRY/charts/maqpna -n maqpna-system --reuse-values -f oidc-values.yaml

REGISTRY is the release registry listed on maqpna.com/download.

Step 3: Sign in with the CLI#

The CLI reads the issuer, client ID and scopes from the gateway (GET /v1/auth/config), so you only pass the gateway URL:

maqpna login --gateway https://gateway.example.eu

It opens the browser for the authorization code flow with PKCE on a 127.0.0.1 loopback redirect. On a host without a browser, use the device flow:

maqpna login --gateway https://gateway.example.eu --device

The token is stored per CLI context in tokens/<context>.json with mode 0600 and refreshed with the refresh token when one was issued. Flags --issuer, --client-id, --scopes and --audience override what the gateway publishes. See maqpna login.

Step 4: Sign in to the console#

maqpna console --open

The console reads the same /v1/auth/config and offers single sign-on. It uses the authorization code flow with PKCE in the browser and keeps the access token in the tab's session storage. Single sign-on needs a secure context: https or localhost.

Verify#

maqpna whoami

The output shows the user name, issuer, groups and roles the gateway derived from your token. Then check one role boundary: a member of maqpna-viewers only can run maqpna approvals list, but maqpna approvals approve answers 403.

Use the same identity provider for approvals#

  • Separation of duties. The user a session acts for can never approve its calls (409 self_approval), and one person cannot vote twice (409 duplicate_approver).
  • Four-eyes. Add approval: {minApprovers: 2, approverGroups: [...]} to a require_approval rule, or set approvals.quorum in the chart. See Human approvals.
  • User approval (CIBA). approval: {userConfirmation: ciba} on a rule asks the delegating user to confirm on their own device. Configure delegation.ciba (enabled, issuer, clientID, clientSecretFile, fallback: approvers | deny) and a confidential Keycloak client with the OpenID CIBA grant. Keycloak also needs a CIBA authentication channel that reaches the user.
  • External MCP clients. delegation.trustedIssuers lets IDE agents call /mcp/<server> with tokens from your identity provider. Set delegation.resourceBaseURL to the gateway's public URL; the token audience must be <resourceBaseURL>/mcp/<server>. A tool listed in stepUpScopes answers 403 insufficient_scope until the client gets the scope.
  • Verified session users. principalBinding.userOIDC makes the operator verify ID tokens referenced by AgentSession.spec.userAssertionRef.
delegation:
  resourceBaseURL: https://maqpna.example.eu
  trustedIssuers:
    - issuer: https://idp.example.eu/realms/maqpna
      namespace: team-a
      agent: ide
      servers: ["github"]
      stepUpScopes: {"github/delete_*": ["tools:github:admin"]}
  clientSecretsSecret: maqpna-delegation     # key "ciba"
  ciba:
    enabled: true
    issuer: https://idp.example.eu/realms/maqpna
    clientID: maqpna-ciba
    clientSecretFile: /etc/maqpna/delegation-secrets/ciba
    fallback: deny

Troubleshooting#

Symptom Cause Fix
401 token issuer mismatch iss differs from adminAuth.oidc.issuer (often a Keycloak host name issue) Pin Keycloak's --hostname; copy the exact issuer from the discovery document
401 token audience mismatch The access token has aud: account Add the Audience mapper with maqpna-admin
whoami shows no roles Groups arrive as /maqpna-admins, or in another claim Turn Full group path off, or set groupsClaim
401 identity provider keys unavailable The gateway cannot fetch discovery or JWKS Check egress to the IdP, sovereignty.allowedEgressHosts and caSecret for a private CA
token lacks the <claim> claim usernameClaim names a claim the token does not have Clear usernameClaim or add a mapper
Console says it needs a secure context The console is served over plain http on a non-local host Use maqpna console on localhost, or serve it over https
503 admin API disabled mode: token without an admin token Set adminAuth.mode: oidc, or enable adminAuth.breakGlassToken

Next steps#