Sign-in with Keycloak¶
The authentication tutorial signs tokens with a shared secret. In production an identity provider usually issues them: users sign in there, your application gets a token, and FirebirdREST checks it against the provider's published keys. This tutorial does that with Keycloak, with a role claim that selects the Firebird role. The same settings work for any OpenID Connect provider (below).
You need the getting-started setup: Docker with Compose, curl, and the
repository checked out.
1. Start Keycloak next to FirebirdREST¶
examples/keycloak/compose.yml
adds Keycloak to the example stack and changes how FirebirdREST checks tokens:
firebirdrest:
environment:
FBREST_JWT_SECRET: ""
FBREST_JWT_JWKS_URL: http://keycloak:8080/realms/bookstore/protocol/openid-connect/certs
FBREST_JWT_AUD: firebirdrest
jwt-jwks-urlis where Keycloak publishes its signing keys (a JWKS). FirebirdREST fetches it at startup, every hour (jwt-jwks-refresh), and when a token names a key it hasn't seen.jwt-audaccepts only tokens issued for this API. Without it, any token from the realm would be accepted, including ones meant for other applications.
Keycloak takes about 15 seconds to start; its admin console is at http://localhost:8180
(admin / admin). Until the realm below exists, FirebirdREST logs cannot fetch jwt-jwks-url
and retries every 30 seconds; requests with a token are refused meanwhile.
2. Create the realm, the client and the users¶
examples/keycloak/setup.sh
does it with Keycloak's admin CLI:
docker compose -f docker-compose.yml -f examples/keycloak/compose.yml exec keycloak bash /setup/setup.sh
It creates:
realm bookstore |
where the users and the application live |
client bookstore-app |
the application users sign in to: a public client (a browser or mobile app has no secret to keep) |
| a User Attribute mapper | copies the user's firebird_role attribute into the token's role claim |
| an Audience mapper | adds firebirdrest to the token's aud |
user ana, password ana-password |
firebird_role = web_user |
user bob, password bob-password |
no firebird_role |
FirebirdREST reads the role from a claim that holds one string (jwt-role-claim-key, role by
default, or a dotted path such as app_metadata.role). Keycloak's own roles come as a list
(realm_access.roles), so a user attribute mapped to a role claim is the direct way to name
one Firebird role per user. In the admin console these are under Users → ana → Attributes and
Clients → bookstore-app → Client scopes → bookstore-app-dedicated.
3. Sign in and call the API¶
A real application sends the user through Keycloak's login page (the authorization code flow, for example with keycloak-js or any OpenID Connect library) and receives the token at the end. From a terminal, the password grant gets one directly:
TOKEN=$(curl -s -d client_id=bookstore-app -d grant_type=password \
-d username=ana -d password=ana-password \
localhost:8180/realms/bookstore/protocol/openid-connect/token | jq -r .access_token)
Its payload (decode the middle part at jwt.io) has what FirebirdREST uses:
{
"aud": ["firebirdrest", "account"],
"sub": "3f38e4ec-2243-4e66-9bec-b9df995cb795",
"role": "web_user",
"email": "ana@example.com",
"azp": "bookstore-app"
}
Ana's requests run as the web_user role:
curl -X POST 'localhost:3000/orders?select=id,customer_id' -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -H 'Prefer: return=representation' -d '{"customer_id":1}'
Bob's token has no role claim, so his requests use the anonymous role, web_anon, which may
not read orders:
BOB_TOKEN=$(curl -s -d client_id=bookstore-app -d grant_type=password \
-d username=bob -d password=bob-password \
localhost:8180/realms/bookstore/protocol/openid-connect/token | jq -r .access_token)
curl localhost:3000/orders -H "Authorization: Bearer $BOB_TOKEN"
{"code":"28000","message":"no permission for SELECT access to TABLE ORDERS","details":null,"hint":null}
That fallback is db-anon-role. Without it, a token without a role is refused with 401. A
token that was tampered with, has expired, or was issued for another audience gets 401
(FBRST301) before any database work.
4. Use the claims in the database¶
Every claim reaches PSQL as a context variable, so procedures, views and triggers can act on the signed-in user (see Request context in PSQL):
-- the orders of the signed-in customer only
CREATE VIEW my_orders AS
SELECT o.* FROM orders o JOIN customers c ON c.id = o.customer_id
WHERE c.email = RDB$GET_CONTEXT('USER_TRANSACTION', 'request.jwt.claim.email');
sub is Keycloak's stable user id; email changes when the user changes it, so key long-lived
data on sub.
5. Rotate keys¶
In Keycloak, Realm settings → Keys → Add provider → rsa-generated with a higher priority than
the current key makes it the signing key. FirebirdREST picks it up when the first token signed
with it arrives (at most one fetch every 30 seconds, so a burst of tokens with unknown keys
can't flood Keycloak); keep the old key enabled until the tokens it signed have expired, then
disable it. Keycloak's encryption key (use: enc) in the same JWKS is ignored.
6. Clean up¶
Other providers¶
FirebirdREST needs three things from any OpenID Connect provider: its JWKS URL, a claim with the Firebird role, and an audience for the API.
| Provider | jwt-jwks-url |
Role claim | jwt-aud |
|---|---|---|---|
| Keycloak | https://<host>/realms/<realm>/protocol/openid-connect/certs |
a User Attribute (or Hardcoded claim) mapper, as above | an Audience mapper |
| Auth0 | https://<tenant>.auth0.com/.well-known/jwks.json |
a post-login Action: api.accessToken.setCustomClaim('role', ...) |
the API's identifier, requested as audience at sign-in |
| Microsoft Entra ID | https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys |
app roles arrive as a list (roles): map the one to use to a single-valued claim |
the application ID URI |
| Google, GitHub | not suited: their tokens carry no role, so every user would get the same one |
A provider that only emits roles as a list needs one more step on its side: map the role (or a
group) to a single-valued claim, since one string is what jwt-role-claim-key reads. The role
is checked by Firebird's grants on every statement, so a claim naming a role the authenticator
user wasn't granted is refused (403, FBRST300).