Needed together with #26. Stacks on #26; until #26 merges the diff also shows #26's commit. This PR's own change is the top commit. Verified end to end locally (see the comment below): without this mapper the device-code token gets aud: "account" and the forte-drop-mcp sidecar answers 401; with it, the full chain works.
What
Adds one protocol mapper to the forte-cli client from #26:
The auth sidecar in mcp mode verifies bearer JWTs with audience = AUTH_MCP_AUDIENCE, defaulting to AUTH_MCP_RESOURCE (internal/config/config.go, internal/auth/mcp.go in Forte/auth-sidecar). For forte-drop-mcp that is https://mcp.drop.forteapps.net/mcp.
forte-drop #67 makes the skill send the RFC 8707 resource= parameter on the device-authorization, token and refresh requests. Keycloak 26.3.3 (the version Bitnami chart 25.2.0 deploys) ignores it: tested locally, the device-code token comes back with aud: "account" only, and the sidecar rejects it with oidc: expected audience "…/mcp" got ["account"]. This mapper is what puts the MCP resource into aud.
Trade-off
The mapper is unconditional: every forte-cli access token carries the forte-drop-mcp audience, whichever service the user logged in for. That is fine while forte-drop is the only consumer. When more sidecar-fronted services adopt forte-cli (mcp10x, ts-mcp …), a per-service optional client scope that carries the audience mapper, requested explicitly via <PREFIX>_SCOPE, keeps tokens least-privilege and should replace this.
Verification done
helm template with the Bitnami keycloak chart 25.2.0 and both value files, as in the Argo Application, renders cleanly.
Rendered forte-realm.json parses; compared with #26's render, the only difference is the protocolMappers array on forte-cli. All other clients and realm settings are byte-identical.
keycloak-config-cli runs with IMPORT_MANAGED_PROTOCOL_MAPPER=no-delete, the same path the existing inline clients (gitea, grafana, argocd) use for their mappers.
> **Needed together with [#26](https://git.forteapps.net/Forte/launchpad/pulls/26).** Stacks on #26; until #26 merges the diff also shows #26's commit. This PR's own change is the top commit. Verified end to end locally (see the comment below): without this mapper the device-code token gets `aud: "account"` and the forte-drop-mcp sidecar answers 401; with it, the full chain works.
## What
Adds one protocol mapper to the `forte-cli` client from #26:
```json
{
"name": "audience-forte-drop-mcp",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.custom.audience": "https://mcp.drop.forteapps.net/mcp",
"access.token.claim": "true",
"id.token.claim": "false",
"introspection.token.claim": "true"
}
}
```
## Why it is needed
The auth sidecar in `mcp` mode verifies bearer JWTs with `audience = AUTH_MCP_AUDIENCE`, defaulting to `AUTH_MCP_RESOURCE` (`internal/config/config.go`, `internal/auth/mcp.go` in `Forte/auth-sidecar`). For forte-drop-mcp that is `https://mcp.drop.forteapps.net/mcp`.
forte-drop [#67](https://git.forteapps.net/Forte/forte-drop/pulls/67) makes the skill send the RFC 8707 `resource=` parameter on the device-authorization, token and refresh requests. **Keycloak 26.3.3 (the version Bitnami chart 25.2.0 deploys) ignores it**: tested locally, the device-code token comes back with `aud: "account"` only, and the sidecar rejects it with `oidc: expected audience "…/mcp" got ["account"]`. This mapper is what puts the MCP resource into `aud`.
## Trade-off
The mapper is unconditional: every `forte-cli` access token carries the forte-drop-mcp audience, whichever service the user logged in for. That is fine while forte-drop is the only consumer. When more sidecar-fronted services adopt `forte-cli` (mcp10x, ts-mcp …), a per-service **optional client scope** that carries the audience mapper, requested explicitly via `<PREFIX>_SCOPE`, keeps tokens least-privilege and should replace this.
## Verification done
- `helm template` with the Bitnami keycloak chart 25.2.0 and both value files, as in the Argo Application, renders cleanly.
- Rendered `forte-realm.json` parses; compared with #26's render, the **only** difference is the `protocolMappers` array on `forte-cli`. All other clients and realm settings are byte-identical.
- keycloak-config-cli runs with `IMPORT_MANAGED_PROTOCOL_MAPPER=no-delete`, the same path the existing inline clients (`gitea`, `grafana`, `argocd`) use for their mappers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
jorgen.stensrud
changed title from WIP: feat(keycloak): audience mapper for forte-drop-mcp on forte-cli (fallback, stacks on #26) to feat(keycloak): audience mapper for forte-drop-mcp on forte-cli (stacks on #26)2026-10-01 10:30:39 +00:00
Ran the whole login chain locally against the same components as prod:
Keycloak 26.3.3 (quay.io/keycloak/keycloak:26.3.3, the version Bitnami chart 25.2.0 deploys), realm imported from the helm template render of this branch (03a1ceb): forte-cli exactly as in #26 / this PR, plus a local test user.
auth-sidecar built from Forte/auth-sidecarb89e6a8, AUTH_MODE=mcp, AUTH_MCP_AUDIENCE=https://mcp.drop.forteapps.net/mcp (the value prod checks).
forte-drop from forte-drop #67 (top of the #65 ← #66 ← #67 stack) in docker-compose.local, driven by the skill's drop.sh login (device code, approved in a browser) and drop.sh list / create.
Keycloak 26.3.3 ignores the RFC 8707 resource= parameter the skill sends, so the audience has to come from this mapper. Without it, every user gets a 401 right after a successful login. Merge order: #26, then this PR (or both together).
## End-to-end test: this PR is needed
Ran the whole login chain locally against the same components as prod:
- **Keycloak 26.3.3** (`quay.io/keycloak/keycloak:26.3.3`, the version Bitnami chart 25.2.0 deploys), realm imported from the `helm template` render of this branch (`03a1ceb`): `forte-cli` exactly as in #26 / this PR, plus a local test user.
- **auth-sidecar** built from `Forte/auth-sidecar` `b89e6a8`, `AUTH_MODE=mcp`, `AUTH_MCP_AUDIENCE=https://mcp.drop.forteapps.net/mcp` (the value prod checks).
- **forte-drop** from forte-drop #67 (top of the #65 ← #66 ← #67 stack) in `docker-compose.local`, driven by the skill's `drop.sh login` (device code, approved in a browser) and `drop.sh list` / `create`.
| Keycloak config | Token `aud` | Through the sidecar |
|---|---|---|
| #26 only | `"account"` | **401** — `oidc: expected audience "…/mcp" got ["account"]` |
| #26 + this PR | `["https://mcp.drop.forteapps.net/mcp", "account"]` | `list` and `create` OK, drop served, no JWT errors |
Keycloak 26.3.3 ignores the RFC 8707 `resource=` parameter the skill sends, so the audience has to come from this mapper. Without it, every user gets a 401 right after a successful login. Merge order: #26, then this PR (or both together).
Add a shared public client `forte-cli` to the `forte` realm so downloaded
skills (forte-drop first) can do RFC 8628 device-code login through the
Auth Sidecar. Today no client in the realm has the device grant enabled,
so the flow cannot start.
Client (inline in forte-realm.json, imported verbatim by keycloak-config-cli):
- publicClient: true, standardFlowEnabled: false,
directAccessGrantsEnabled: false
- attributes: oauth2.device.authorization.grant.enabled=true
- no secret, no redirectUris/webOrigins, no k8s.secret.sync
It has to go in the realm JSON because the self-service registrar
hardcodes publicClient:false/standardFlowEnabled:true and drops
attributes. Also add forte-cli to the cleanup CronJob's protected list
(belt-and-braces; it does not match the UUID pattern anyway).
Additive only: gitea/grafana/argocd and all other realm settings are
unchanged. Keycloak is deployed only via the upc-dev overlay, which
inherits base values, so this lands on id.forteapps.net.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QciXev3MtCxo3eomcfrDRW
Adds an oidc-audience-mapper to the forte-cli client so its access
tokens carry aud=https://mcp.drop.forteapps.net/mcp, the audience the
forte-drop-mcp auth sidecar verifies. Fallback for the case where
Keycloak ignores the RFC 8707 resource= parameter the skill sends.
Stacks on #26.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
Adds one protocol mapper to the
forte-cliclient from #26:Why it is needed
The auth sidecar in
mcpmode verifies bearer JWTs withaudience = AUTH_MCP_AUDIENCE, defaulting toAUTH_MCP_RESOURCE(internal/config/config.go,internal/auth/mcp.goinForte/auth-sidecar). For forte-drop-mcp that ishttps://mcp.drop.forteapps.net/mcp.forte-drop #67 makes the skill send the RFC 8707
resource=parameter on the device-authorization, token and refresh requests. Keycloak 26.3.3 (the version Bitnami chart 25.2.0 deploys) ignores it: tested locally, the device-code token comes back withaud: "account"only, and the sidecar rejects it withoidc: expected audience "…/mcp" got ["account"]. This mapper is what puts the MCP resource intoaud.Trade-off
The mapper is unconditional: every
forte-cliaccess token carries the forte-drop-mcp audience, whichever service the user logged in for. That is fine while forte-drop is the only consumer. When more sidecar-fronted services adoptforte-cli(mcp10x, ts-mcp …), a per-service optional client scope that carries the audience mapper, requested explicitly via<PREFIX>_SCOPE, keeps tokens least-privilege and should replace this.Verification done
helm templatewith the Bitnami keycloak chart 25.2.0 and both value files, as in the Argo Application, renders cleanly.forte-realm.jsonparses; compared with #26's render, the only difference is theprotocolMappersarray onforte-cli. All other clients and realm settings are byte-identical.IMPORT_MANAGED_PROTOCOL_MAPPER=no-delete, the same path the existing inline clients (gitea,grafana,argocd) use for their mappers.🤖 Generated with Claude Code
WIP: feat(keycloak): audience mapper for forte-drop-mcp on forte-cli (fallback, stacks on #26)to feat(keycloak): audience mapper for forte-drop-mcp on forte-cli (stacks on #26)End-to-end test: this PR is needed
Ran the whole login chain locally against the same components as prod:
quay.io/keycloak/keycloak:26.3.3, the version Bitnami chart 25.2.0 deploys), realm imported from thehelm templaterender of this branch (03a1ceb):forte-cliexactly as in #26 / this PR, plus a local test user.Forte/auth-sidecarb89e6a8,AUTH_MODE=mcp,AUTH_MCP_AUDIENCE=https://mcp.drop.forteapps.net/mcp(the value prod checks).docker-compose.local, driven by the skill'sdrop.sh login(device code, approved in a browser) anddrop.sh list/create.aud"account"oidc: expected audience "…/mcp" got ["account"]["https://mcp.drop.forteapps.net/mcp", "account"]listandcreateOK, drop served, no JWT errorsKeycloak 26.3.3 ignores the RFC 8707
resource=parameter the skill sends, so the audience has to come from this mapper. Without it, every user gets a 401 right after a successful login. Merge order: #26, then this PR (or both together).03a1cebf17to95db80fb53New commits pushed, approval review dismissed automatically according to repository settings