Keycloak
Keycloak is the recommended self-hosted IdP for swsrs. It has every primitive we need: custom OAuth scopes per realm, device authorization grant, OIDC discovery, and JWKS.
This guide walks you through the setup that powers connect.bimmerz.app (the reference deployment).
Prerequisites
- A Keycloak instance reachable from your relay (so OIDC discovery works). Cloud-hosted, self-hosted, or
kc.sh start-devfor local testing. - Admin access to a realm — we'll call it
myproduct.
1. Create the swsrs realm or pick an existing one
Pick (or create) the realm you'll use. The issuer URL will be:
https://your-keycloak.example.com/realms/myproductThat value goes in SWSRS_OIDC_ISSUER on the relay.
2. Create a Keycloak client for swsrs
This single client serves two purposes:
- Resource server — its name becomes the
audclaim on access tokens; swsrs uses it to enforce audience. - Public OAuth client — your CLI / SDK clients use it as
client_idfor device flow.
In Keycloak admin console:
- Clients → Create client
- Client type:
OpenID Connect - Client ID:
swsrs(this becomes both the audience and theclient_id_hint) - Next → Capability config:
- Client authentication: OFF (public client — required for device flow without a secret)
- Authentication flow: check OAuth 2.0 Device Authorization Grant. Uncheck
Standard flowandDirect access grantsunless you actually need them. - Service accounts roles: off (this client represents end-user identities, not the relay itself)
- Save.
3. Define the swsrs scopes
Keycloak calls these "Client Scopes."
- Client scopes → Create client scope, three times — once for each of:
swsrs:session:createswsrs:session:readswsrs:session:delete
- For each:
- Type:
Optional(so users get them only when assigned the matching role) - Display on consent screen: off (CLI users shouldn't see a consent prompt for device flow)
- Include in token scope: on (so the scope appears in the
scopeclaim on access tokens — this is what swsrs reads)
- Type:
4. Add the scopes to the swsrs client
- Clients →
swsrs→ Client scopes → Add client scope - Add all three swsrs scopes as Optional (the client asks for them per-request via the
scope=parameter; the IdP grants them based on the user's roles).
5. Create realm roles for scope mapping
We want a Keycloak role to act as "permission to create swsrs sessions."
- Realm roles → Create role
- Role name:
swsrs-creator
- Role name:
- Repeat for
swsrs-readerandswsrs-adminif you want fine-grained access. For most setups, one role with all three scopes is enough — call itswsrs-operator.
6. Map roles to scopes (the bridge)
For each swsrs client scope you defined, add a Mapper that emits the scope name when the user has the corresponding role.
A simple way: use a Hardcoded scope mapper.
- Client scopes →
swsrs:session:create→ Mappers → Add mapper - By configuration → Hardcoded claim
- Set:
- Token claim name:
scope - Claim value:
swsrs:session:create - Claim JSON Type:
String - Add to access token: on
- Add to ID token: off
- Multivalued: off (Keycloak appends to the existing scope string when it's a
Stringclaim with the same name)
- Token claim name:
- Save.
- Repeat for
swsrs:session:readandswsrs:session:delete.
Actually — the simpler path is to give the client scope's name the same value as the scope you want emitted (which we already did), and use the default "Audience" + "Roles" + "User Realm Role" mappers Keycloak ships. Read this back: any scope listed in a token's scope claim was either default-included on the client or explicitly requested. Since we made these scopes Optional and asked for them by name, they appear automatically when granted.
If a user shouldn't have a scope, don't grant the role in the next step — the access token simply won't carry that scope, and swsrs returns 403.
7. Assign the role(s) to users
For each user (or group) that should mint sessions:
- Users → <user> → Role mapping → Assign role
- Assign
swsrs-operator(or the specific subset you defined).
For most apps you only need to grant swsrs:session:create — your end users don't need read/delete. See Authentication.
8. (Optional) Configure audience claim explicitly
By default, Keycloak puts the client_id in the aud claim of access tokens only when the same client requests the token (i.e., a token issued for that client, not just by it).
If you ever see swsrs reject tokens with audience mismatch errors, add an Audience mapper to the swsrs client:
- Clients →
swsrs→ Client scopes → swsrs-dedicated → Add mapper - By configuration → Audience
- Included Client Audience:
swsrs - Add to access token: on
9. Configure the relay
Wire it all together:
SWSRS_OIDC_ISSUER=https://your-keycloak.example.com/realms/myproduct \
SWSRS_OIDC_AUDIENCE=swsrs \
SWSRS_OIDC_CLIENT_ID=swsrs \
swsrs serveOr via Docker:
services:
swsrs:
image: ghcr.io/emdzej/swsrs:latest
environment:
SWSRS_OIDC_ISSUER: https://your-keycloak.example.com/realms/myproduct
SWSRS_OIDC_AUDIENCE: swsrs
SWSRS_OIDC_CLIENT_ID: swsrs
SWSRS_ALLOWED_ORIGINS: app.example.com,*.dev.example.com10. Smoke test
# Discovery should return Keycloak's endpoints
curl -s https://relay.example.com/.well-known/swsrs-config | jq
# Run device flow
swsrs auth --relay https://relay.example.com
# follow the Keycloak login URL, sign in, get the code
# Create a session
swsrs create --admin-url https://relay.example.com
# {"id":"...", "initiator_token":"...", "responder_token":"...", ...}If you got back JSON with two tokens, you're done.
Troubleshooting
| Symptom | Likely cause |
|---|---|
401 invalid token: oidc: id token signed by alg "RS256" cannot be verified | Issuer URL mismatch. The iss claim in the token must exactly equal SWSRS_OIDC_ISSUER. Trailing-slash differences count. |
401 invalid token: oidc: id token issued by ... | Same as above. |
403 missing required scope: swsrs:session:create | User doesn't have the role that maps to the scope. Check Users → <user> → Role mappings. |
401 invalid token: oidc: audience claim ... | Token's aud doesn't include swsrs. Add the audience mapper from step 8. |
swsrs auth says "IdP does not advertise device_authorization_endpoint" | Verify in your IdP that device authorization grant is enabled for the client (step 2). |
Discovery returns device_authorization_endpoint: "" | Keycloak's discovery doc lists device endpoint only when at least one client has the grant enabled. Recycle (/admin/clear-keys) if needed. |