Keycloak Setup
This page sets up OpenID Connect sign-in with Keycloak for the Endatix API and Endatix Hub. It assumes Keycloak 26 on http://127.0.0.1:8080, Hub on http://localhost:3000, and the API on https://localhost:5001. Paths below are from each repository root.
Use the host 127.0.0.1 in every issuer URL. Keycloak copies the host from the browser into the token iss claim. localhost and 127.0.0.1 are different issuers.
Start Keycloak
From the Endatix API repository root:
docker compose -f docker/docker-compose.keycloak.yml up
The compose file runs quay.io/keycloak/keycloak:26.8 with start-dev --import-realm and binds 127.0.0.1:8080. Admin console: http://127.0.0.1:8080, username admin, password admin.
The imported realm is docker/keycloak/endatix-realm.json:
| Realm | endatix |
| Client | endatix-hub (confidential, standard flow, direct access grants) |
| Client secret | endatix-dev-hub-client-secret |
| Redirect URI | http://localhost:3000/api/auth/callback/keycloak |
| Post logout redirect URI | http://localhost:3000/signin |
| Web origin | http://localhost:3000 |
| Users | external@endatix.com (no client role), external-admin@endatix.com (admin), external-creator@endatix.com (creator), respondent@endatix.com (respondent). Password admin@2 |
Authorization services are off. Keycloak 26 does not ship the JavaScript policy provider, so a realm export that contains a policy of type js fails with Couldn't find policy provider with type [js]. Do not replace this file with an admin-console export: Keycloak writes the client secret as **********.
start-dev --import-realm imports only when the realm is not already in the container volume. After you change the JSON, delete the volume and start again:
docker compose -f docker/docker-compose.keycloak.yml down -v
docker compose -f docker/docker-compose.keycloak.yml up
Wire the local workspace
Apply these four edits on your machine. Do not commit appsettings.Development.json in the Endatix API repo, or .env in the Endatix Hub repo, after they contain the client secret.
Endatix API, src/Endatix.WebHost/Program.cs. ConfigureEndatix() does not register Keycloak. Tokens are then validated as issuer endatix-api.
builder.Host.ConfigureEndatixWithDefaults(endatix =>
{
endatix.Infrastructure.Security.AddKeycloakAuthProvider();
});
Endatix API, src/Endatix.WebHost/appsettings.Development.json, under Endatix:Auth:Providers:
"Keycloak": {
"Enabled": true,
"RequireHttpsMetadata": false,
"Audience": "endatix-app",
"Issuer": "http://127.0.0.1:8080/realms/endatix",
"DefaultTenantId": 1,
"ClientId": "endatix-hub",
"ClientSecret": "endatix-dev-hub-client-secret"
}
Audience is the resource this access token is for, and it must be one of the token aud values. It is not the Keycloak client id. The dev realm issues a lightweight access token and still adds endatix-app and endatix-hub to aud, plus sub. Keycloak 26 returns active: false from token introspection unless the introspecting client id is in aud. Leave MapInboundClaims false. When it is true, ASP.NET renames sub, and the API's default policy still requires the sub claim. Role claims are not in this JWT. See Keycloak RBAC.
Endatix Hub, auth.ts. Confirm this line exists. Do not register the provider twice. The sign-in button appears only when AUTH_KEYCLOAK_ENABLED=true and the client id, secret, and issuer are set.
authRegistry.register(new KeycloakAuthProvider());
Endatix Hub, .env:
AUTH_KEYCLOAK_ENABLED=true
AUTH_KEYCLOAK_CLIENT_ID=endatix-hub
AUTH_KEYCLOAK_CLIENT_SECRET=endatix-dev-hub-client-secret
AUTH_KEYCLOAK_ISSUER=http://127.0.0.1:8080/realms/endatix
AUTH_URL=http://localhost:3000
Restart the API and Hub after changing them.
API properties
Turns the Keycloak JWT scheme on.
Must match the token iss claim, including host and port.
Resource name this access token is for. Must match the token aud claim. The dev realm uses endatix-app. Not the Keycloak client id.
false for this HTTP dev server. true in production.
Tenant id stored for users who sign in through Keycloak.
Keycloak client id. This realm uses endatix-hub. The API sends it when calling token introspection. See Keycloak RBAC.
Client secret for that same introspection call.
Hub variables
true activates the provider already registered in auth.ts.
Keycloak client id. This realm uses endatix-hub.
Client secret from the realm file, or from Clients → endatix-hub → Credentials if you created the client by hand.
Realm issuer. http://127.0.0.1:8080/realms/endatix for this compose file.
Public Hub origin. Federated sign-out sends the browser back to ${"{AUTH_URL}"}/signin. That exact URI must be a Valid post logout redirect URI on the client. Include a base path when Hub is not served from /, for example https://yourdomain.com/app/signin.
Other Hub variables (ENDATIX_BASE_URL, SESSION_SECRET, AUTH_SECRET) are on Hub environment variables.
Federated sign-out
Hub clears its own session, then redirects Keycloak users to the realm end-session endpoint (protocol/openid-connect/logout on the issuer). That URL is end_session_endpoint in the realm discovery document. The redirect includes id_token_hint, client_id, and post_logout_redirect_uri.
The dev realm already allows http://localhost:3000/signin. If you create the client yourself, set the same value under Valid post logout redirect URIs.
Check the flow
- Open
http://localhost:3000. The sign-in page shows Sign in with Keycloak. - Sign in as
external@endatix.com/admin@2. Hub returns to the dashboard. - Sign out. The browser passes through
http://127.0.0.1:8080/realms/endatix/protocol/openid-connect/logoutand lands on/signin. - Sign in again. Keycloak asks for credentials.
Discovery check:
curl -s http://127.0.0.1:8080/realms/endatix/.well-known/openid-configuration
issuer must be http://127.0.0.1:8080/realms/endatix. end_session_endpoint must be http://127.0.0.1:8080/realms/endatix/protocol/openid-connect/logout.
API startup should log Configured Keycloak auth provider. Hub startup should log Provider keycloak validated and activated.
Troubleshooting
Sign-in button missing. AUTH_KEYCLOAK_ENABLED is not true, or client id, secret, or issuer is empty. Confirm authRegistry.register(new KeycloakAuthProvider()) in auth.ts.
IDX10205 and ValidIssuers: endatix-api. The API validated the Keycloak token with the built-in Endatix JWT scheme. Call AddKeycloakAuthProvider() inside ConfigureEndatixWithDefaults and restart the API.
IDX10205 and the issuer is a Keycloak URL. Endatix:Auth:Providers:Keycloak:Issuer does not equal the token iss. Align Hub AUTH_KEYCLOAK_ISSUER, the API issuer, and the host you use in the browser. Do not mix localhost and 127.0.0.1.
Invalid redirect URI. The redirect URI and the post-logout URI must match the client, including http:// and with no trailing slash.
Audience validation failed. Set Audience to the aud claim in the access token. Decode the token to read it.
Import error Couldn't find policy provider with type [js]. The file enables authorization services and includes a JavaScript policy. Use docker/keycloak/endatix-realm.json, which leaves authorization off. Delete a half-created endatix realm before importing again.
Production
Run Keycloak behind HTTPS, set RequireHttpsMetadata to true, and use a distinct realm per environment. Rotate the client secret in Keycloak and in both apps together. Follow the Keycloak server guide for clustering and hostname settings.
Next steps
- Keycloak RBAC maps Keycloak roles onto Endatix roles through token introspection.
- External authorization explains that model.
- Session bridge exchanges a Keycloak token for a Hub session.