From a6d8b9d9947bb4f87b8383c109cca75a6acbbe31 Mon Sep 17 00:00:00 2001 From: Federico Date: Wed, 19 Aug 2026 11:25:55 +0200 Subject: [PATCH] keycloak added --- docs/.vitepress/config.mts | 7 + docs/public/logos/keycloaklogo.png | Bin 0 -> 10506 bytes .../databases/authentication/keycloak.md | 554 ++++++++++++++++++ 3 files changed, 561 insertions(+) create mode 100644 docs/public/logos/keycloaklogo.png create mode 100644 docs/stacks/databases/authentication/keycloak.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 925aabb..ed88c27 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -110,6 +110,13 @@ export default defineConfig({ text: 'Databases', collapsed: true, items: [ + { + text: 'Authentication', + collapsed: true, + items: [ + { text: 'Keycloak', link: '/stacks/databases/authentication/keycloak' }, + ] + }, { text: 'Fundamentals', link: '/stacks/databases/fundamentals' }, { text: 'PostgreSQL', link: '/stacks/databases/postgres' }, { text: 'PostgREST', link: '/stacks/databases/postgrest' }, diff --git a/docs/public/logos/keycloaklogo.png b/docs/public/logos/keycloaklogo.png new file mode 100644 index 0000000000000000000000000000000000000000..48e18430cb06664c10a32008e13dd7b02ab3d879 GIT binary patch literal 10506 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 Keycloak + +Keycloak is the company-standard identity and access management (IAM) platform for applications that need +centralized login, user management, roles, single sign-on (SSO), and password flows. It implements the OpenID +Connect (OIDC) and OAuth 2.0 protocols and can also federate users from external identity providers and directories. + +This document is the baseline procedure for creating a Keycloak configuration for a new project. It also documents +the delegated `admin-manager` pattern used when an application must let a customer administrator manage users without +granting access to the whole realm. + +> [!WARNING] +> The exact Admin Console labels, REST fields, and fine-grained permission endpoints can change between Keycloak +> versions. Record the Keycloak version for every environment and verify version-specific API documentation before +> automating a production deployment. + +## Contents + +- [Keycloak concepts](#keycloak-concepts) +- [Application architecture](#application-architecture) +- [Standard setup for a new project](#standard-setup-for-a-new-project) +- [FastAPI integration](#fastapi-integration) +- [User and password flows](#user-and-password-flows) +- [Delegated user administration](#delegated-user-administration) +- [CLI and Docker operations](#cli-and-docker-operations) +- [Production checklist](#production-checklist) +- [Troubleshooting](#troubleshooting) + +## Keycloak Concepts + +### Realm + +A realm is an isolated security domain containing users, groups, roles, clients, identity providers, login settings, +and sessions. Create one realm per application or per security boundary. Do not use the `master` realm for normal +application users; it is reserved for administering the Keycloak installation. + +Example realm names are `test-app`, `inewsense-dev`, and `inewsense-prod`. Keep development, staging, and production +realms separate so that users, credentials, redirect URLs, and permissions cannot cross environments accidentally. + +### Client + +A client represents an application or service that uses Keycloak. For an OIDC application, configure: + +- `Client ID`: stable identifier used in authorization requests and tokens. +- `Client authentication`: enabled for confidential server-side applications; disabled for public browser-only apps. +- `Valid redirect URIs`: exact callback URLs permitted after login. Use the narrowest patterns possible. +- `Web origins`: approved browser origins for CORS-related OIDC requests. +- `Client secret`: only for confidential clients; store it in a secret manager, never in frontend code. + +The client is not the same thing as a user. Users authenticate; clients request tokens on behalf of users or services. + +### Tokens and claims + +Keycloak issues signed JWT access tokens. Common claims include: + +| Claim | Meaning | +| --- | --- | +| `iss` | Issuer URL for the realm. | +| `sub` | Immutable subject identifier for the user. | +| `aud` | Intended audience, usually one or more clients. | +| `exp` | Expiration timestamp. | +| `preferred_username` | Display/login username. | +| `realm_access.roles` | Realm roles assigned to the user. | +| `resource_access` | Client-specific roles assigned to the user. | + +Applications must use `sub` as the stable user identifier. Usernames and email addresses may change. + +### Roles and permissions + +Realm roles apply across the realm. Client roles belong to one client and are normally used for application +authorization. A role in `realm-management`, such as `view-users` or `manage-users`, is an administrative permission +for Keycloak itself and must not be confused with an application role such as `operator` or `viewer`. + +Use the least powerful role that satisfies the use case. Prefer application roles for application authorization and +Fine-Grained Admin Permissions (FGAP) for scoped administration of Keycloak resources. + +## Application Architecture + +Keep identity data and application data conceptually separate: + +- Keycloak stores identity, authentication credentials, email verification, groups, and protocol roles. +- The application database stores domain data such as organization, customer, ASL, subscription, and business role. +- The application links the two systems using the Keycloak user `sub` value. + +The request flow for a protected API is: + +1. The user authenticates through the OIDC client. +2. Keycloak returns an access token to the application. +3. The application sends the token as `Authorization: Bearer `. +4. The API validates the JWT signature using the realm JWKS endpoint. +5. The API validates issuer, audience, expiration, and any required roles. +6. The route uses the verified claims and the `sub` value for application lookups. + +Never trust decoded claims before signature and standard-claim validation. Never accept a user ID from the request body +as the authenticated identity for self-service operations; derive it from `sub`. + +## Standard Setup For A New Project + +### Prerequisites + +- A supported Keycloak instance and its exact version recorded. +- Administrator access to the `master` realm, or a deployment service account with the required permissions. +- An application domain for each environment. +- A secure location for client secrets and SMTP credentials. +- `kcadm.sh` access, or access to the Admin REST API. + +### 1. Create the realm + +In the Admin Console: + +1. Open the realm selector and choose **Create realm**. +2. Set the realm name, for example `test-app`. +3. Enable the realm and save it. + +CLI equivalent: + +```bash +kcadm.sh create realms \ + -s realm=test-app \ + -s enabled=true \ + -s displayName="Test App" +``` + +### 2. Create the application client + +In **Realm > Clients > Create client**: + +1. Choose **OpenID Connect**. +2. Set the client ID, for example `test-app-client`. +3. Enable client authentication when the application can securely store a secret. +4. Add only the redirect URIs and web origins required by the environment. +5. Copy the generated secret into the application secret store when applicable. + +Example: + +```bash +kcadm.sh create clients -r test-app \ + -s clientId=test-app-client \ + -s enabled=true \ + -s publicClient=false \ + -s 'redirectUris=["https://app.example.com/*"]' \ + -s 'webOrigins=["https://app.example.com"]' +``` + +For a public SPA, use `publicClient=true`, do not create a secret, and use Authorization Code Flow with PKCE. Do not +use the deprecated implicit flow. + +### 3. Configure login settings + +Review **Realm settings > Login** and configure the policies required by the project: + +- user registration, only if the application requires it; +- email verification, if email ownership is part of the trust model; +- password policy and password history; +- `Forgot password`, if users must recover access without an administrator; +- session and token lifetimes appropriate to the risk of the application. + +Do not enable self-registration by default. If it is enabled, define email verification and an approval or onboarding +process before granting application roles. + +### 4. Configure SMTP + +Configure **Realm settings > Email** with an approved SMTP provider. Test both connectivity and a real password-reset +message. For Gmail, a two-factor-protected account requires a dedicated App Password, not the normal account password. + +Use one consistent transport mode: + +- port `587`: STARTTLS enabled, implicit SSL disabled; +- port `465`: implicit SSL enabled, STARTTLS disabled. + +The `from` address must be the authenticated mailbox or a verified alias. + +Example CLI configuration: + +```bash +kcadm.sh update realms/test-app \ + -s 'smtpServer.host=smtp.example.com' \ + -s 'smtpServer.port=587' \ + -s 'smtpServer.from=no-reply@example.com' \ + -s 'smtpServer.auth=true' \ + -s 'smtpServer.starttls=true' \ + -s 'smtpServer.ssl=false' \ + -s 'smtpServer.user=' \ + -s 'smtpServer.password=' +``` + +### 5. Create application roles + +Create only the roles used by the application, for example `admin`, `operator`, and `viewer`. Keep them simple and +document their meaning. Do not assign `realm-admin` or broad `realm-management` roles to normal users. + +Assign roles through groups when possible. Group-based assignment makes onboarding, offboarding, and audits easier. + +### 6. Configure application environment variables + +For the FastAPI backend template, create `.env` from `.env.example` and set: + +```dotenv +KEYCLOAK_URL=https://keycloak.example.com +REALM=test-app +CLIENT_ID=test-app-client +``` + +`KEYCLOAK_URL` is the Keycloak base URL, `REALM` is the target realm, and `CLIENT_ID` is the API audience/client +identifier expected by the backend. Never commit `.env` or client secrets. + +## FastAPI Integration + +The backend template validates Keycloak access tokens with RS256 and the realm's JWKS endpoint. Authentication code is +kept under `core/auth/`; route handlers only need the `verify_token` dependency: + +```python +from fastapi import Depends + +from core.auth import verify_token + + +@router.get("/protected") +def protected_route(user=Depends(verify_token)): + return {"user": user["preferred_username"]} +``` + +The dependency must validate: + +- the JWT signature against the current JWKS keys; +- `iss` against the configured realm issuer; +- `aud` against the configured client/audience; +- `exp` and token validity; +- required roles for routes that need authorization. + +Key rotation is normal. The verifier should refresh JWKS keys when a previously unknown `kid` is encountered rather than +requiring a deployment. Use the optional `core/auth/schemas.py` `TokenPayload` model when typed claim access is useful. + +### Configure the API audience + +The FastAPI verifier validates the JWT `aud` claim against the backend `CLIENT_ID`. Keycloak must therefore add that +client ID as an audience to access tokens issued for the application. Configure this once as a realm client scope. + +#### Create the audience client scope + +In the Keycloak Admin Console: + +1. Open **Client scopes > Create client scope**. +2. Set the name to `api-audience` (or the project-approved equivalent). +3. Set **Protocol** to **OpenID Connect**. +4. Save the scope. + +#### Add the Audience mapper + +Open the new scope, select **Mappers > Configure a new mapper**, and choose **Audience**. Configure: + +- **Included Client Audience**: the exact value of the FastAPI backend `CLIENT_ID`; +- **Add to access token**: enabled; +- **Add to token introspection**: enabled. + +The included client audience must match the value configured in the backend. For example, if the API environment has: + +```dotenv +CLIENT_ID=test-app-api +``` + +the mapper must use `test-app-api` as its included client audience. Do not use the frontend client ID unless the +frontend is also the protected API audience. + +#### Make the scope a realm default + +In **Client scopes**, set the `api-audience` assigned type to **Default**, or add it through **Realm default client +scopes**. New clients created in the realm will then inherit the mapper automatically. + +This default does not update clients that already exist. For each existing client: + +1. Open **Clients** and select the client. +2. Open the **Client scopes** tab. +3. Select **Add client scope**. +4. Choose `api-audience`. +5. Set **Assigned type** to **Default** and save. + +Verify the result by obtaining an access token for the client and checking that its `aud` claim contains the FastAPI +`CLIENT_ID`. A token can be correctly signed and still be rejected with `401` if its audience is missing or points to a +different client. + +> [!NOTE] +> Use a dedicated client scope for the API audience rather than adding unrelated claims to the default scope. This +> keeps token contents predictable and makes the configuration reusable across projects and environments. + +### Admin REST API from an application + +Use the Admin REST API only for explicit administrative use cases such as creating, updating, or deleting another user. +An application token needs both an appropriate `realm-management` permission and an audience that includes +`realm-management`. Configure that audience with a dedicated client scope and Audience Mapper, or use a controlled +token-exchange flow. + +Do not give every logged-in user `view-users` and `manage-users`. These roles are realm-wide and allow access to users +other than the caller. Use a confidential backend service account or FGAP when the operation is administrative. + +## User And Password Flows + +### Administrative user operations + +Creating, editing, and deleting another user uses the Admin REST API. The caller must be authorized for the specific +operation. If a new user has application access level `admin`, the application may assign `view-users` and +`manage-users` automatically as part of its controlled provisioning flow, provided that the calling administrative +principal is allowed to assign those roles. + +### Self-edit profile + +For a user changing their own first name or last name, use the Account REST API under: + +```text +/realms/{realm}/account +``` + +The account API is self-scoped by the token subject, so it does not require `realm-management` roles. This is preferable +to granting global `view-users` and `manage-users` to `operator` or `viewer` users. + +### Change password while logged in + +Use an OIDC Application Initiated Action instead of attempting to modify credentials from the application. Redirect the +authenticated browser to the authorization endpoint with: + +```text +kc_action=UPDATE_PASSWORD +``` + +Reuse the already approved OIDC callback URI. With an active SSO session, the user is taken to Keycloak's password form +and then returned to the application. This flow needs no `realm-management` role. + +The direct Account API password endpoint is not available in every Keycloak version. If the installed version returns +`404` or `405`, use `UPDATE_PASSWORD` and verify it again after major Keycloak upgrades. + +### Forgot password + +For a user who is not logged in, enable **Realm settings > Login > Forgot password** and configure SMTP. The reset email +flow is distinct from the logged-in `UPDATE_PASSWORD` action. A redirect URI that is not whitelisted causes an +`Invalid redirect uri` error. + +## Delegated User Administration + +Use this pattern when a customer or tenant administrator must manage users but must not manage realms, clients, or +powerful administrative roles. The example creates a realm role `admin-manager` that can: + +- create, view, edit, and delete users within the allowed scope; +- assign `admin-manager` to other users; +- not assign `realm-admin`, `manage-realm`, client administration, or unrelated roles. + +### Enable fine-grained admin permissions + +In **Realm settings**, enable **Admin Permissions** and save. The **Permissions** menu then becomes available. + +CLI field names vary by release; where supported: + +```bash +kcadm.sh update realms/test-app -s adminPermissionsEnabled=true +``` + +If this has no effect, inspect `kcadm.sh get realms/test-app` and use the Admin Console for this one-time operation. + +### Create the delegated role + +Create a simple realm role. Do not make it a composite role containing `realm-management` roles. + +```bash +kcadm.sh create roles -r test-app \ + -s name=admin-manager \ + -s 'description=Scoped user administration and delegation of admin-manager' +``` + +### Create the role policy + +In **Permissions > Policies > Create policy**: + +1. Select policy type **Role**. +2. Set the name to `policy-admin-manager`. +3. Select the realm role `admin-manager`. + +FGAP policy and permission REST endpoints are release-sensitive. Prefer the Admin Console or a tested Terraform +provider for the installed version. If REST automation is required, the authorization resources are exposed through +the `realm-management` client under the target realm, for example: + +```text +/admin/realms/{realm}/clients/{realm-management-client-id}/authz/resource-server/ +``` + +### Grant user permissions + +In **Permissions > Users**, create a permission with: + +- resource: **All users**, or a group/tenant-specific resource; +- scopes: `view` and `manage`; +- policy: `policy-admin-manager`; +- decision strategy: `AFFIRMATIVE`. + +This permits CRUD operations only for the resource selected. Prefer a group-specific resource when one realm contains +users from multiple customers. + +### Restrict role assignment + +This is the most important security step. In **Permissions > Roles**, create a permission with: + +- resource: only the `admin-manager` role; +- scope: `map-role`; +- policy: `policy-admin-manager`; +- no generic permission over all roles. + +A broad user-management permission without this restriction can permit privilege escalation by assigning a more powerful +role to a newly created user. Review **Roles > admin-manager > Permissions** after saving and ensure no broader inherited +permission exists. + +### Create the first delegated administrator + +In **Users > Create new user**, create the user, set a temporary password, and assign the `admin-manager` realm role. + +```bash +kcadm.sh create users -r test-app \ + -s username=customer-admin \ + -s enabled=true \ + -s email=customer-admin@example.com + +kcadm.sh set-password -r test-app \ + --username customer-admin \ + --new-password '' \ + --temporary + +kcadm.sh add-roles -r test-app \ + --uusername customer-admin \ + --rolename admin-manager +``` + +### Verify the delegation + +Test with a real token for `customer-admin`: + +1. Create a user. +2. View, edit, and delete a permitted user. +3. Assign `admin-manager` to a new user. +4. Attempt to assign `realm-admin` or another administrative role and confirm HTTP `403`. +5. Attempt to access clients, realm settings, and unrelated permissions and confirm denial. +6. Confirm audit logs record who created users and who delegated `admin-manager`. + +Do not grant raw `manage-users` as a substitute for this design. Review FGAP permissions after every realm change. + +## CLI And Docker Operations + +`kcadm.sh` is included in the official Keycloak image at `/opt/keycloak/bin/kcadm.sh`. It is not a separate tool. +When Keycloak runs in Docker on an EC2 host: + +```bash +ssh @ +docker exec -it /bin/bash +``` + +Inside the container, use the internal Keycloak address, usually `http://localhost:8080`, rather than the public host +and mapped port: + +```bash +/opt/keycloak/bin/kcadm.sh config credentials \ + --server http://localhost:8080 \ + --realm master \ + --user \ + --password '' +``` + +On ECS or Kubernetes, the access method changes to ECS Exec or `kubectl exec`; the `kcadm.sh` commands remain the same. +The credentials cache is stored in `~/.keycloak/kcadm.config`; protect it and repeat login when the token expires. + +Useful commands: + +```bash +# Inspect the realm configuration. +kcadm.sh get realms/test-app + +# Inspect an existing client before changing redirect URIs. +kcadm.sh get clients -r test-app -q clientId=test-app-client \ + --fields id,redirectUris,webOrigins + +# Locate a user ID. +kcadm.sh get users -r test-app -q username= --fields id + +# Assign management roles only to a controlled administrator. +kcadm.sh add-roles -r test-app \ + --uusername \ + --cclientid realm-management \ + --rolename view-users --rolename manage-users + +# Remove roles from a test operator/viewer. +kcadm.sh remove-roles -r test-app \ + --uusername \ + --cclientid realm-management \ + --rolename view-users --rolename manage-users +``` + +When updating array fields such as `redirectUris`, `-s` replaces the entire array. Read the current value first and +write back the complete desired list, including every environment that must remain valid. + +## Production Checklist + +- Pin and record the Keycloak version for each environment. +- Use separate realms, clients, secrets, and SMTP credentials for development, staging, and production. +- Use HTTPS for public Keycloak and application URLs. +- Register exact production redirect URIs and web origins; avoid broad wildcards. +- Use Authorization Code Flow with PKCE for browser applications. +- Store client secrets and SMTP passwords in a secret manager. +- Keep the `master` realm restricted to platform administrators. +- Grant only the minimum client roles and application roles required. +- Do not grant `view-users` or `manage-users` for self-edit or logged-in password change. +- Test JWT issuer, audience, signature, expiration, and key rotation in every environment. +- Configure and test SMTP with a real reset email. +- Enable email verification and forgot-password only when the product requires them. +- Test disabled users, expired tokens, revoked sessions, and unauthorized role changes. +- Enable and retain Keycloak audit events according to company retention requirements. +- Review FGAP permissions after every change and after every Keycloak upgrade. +- Back up realm configuration and maintain an infrastructure-as-code representation where practical. + +## Troubleshooting + +### `401 Unauthorized` + +Usually means the token is absent, malformed, expired, signed with an unknown key, or issued by another realm. Check +the `Authorization` header, token `exp`, `iss`, JWKS URL, and Keycloak clock synchronization. + +### `403 Forbidden` + +The token is valid but the caller lacks the required role or FGAP permission. Inspect realm roles, client roles under +`resource_access`, the target resource/scope, and the permission decision strategy. + +### `Invalid redirect uri` + +The callback URL in the request does not match the client's **Valid redirect URIs**. Compare scheme, host, port, path, +and trailing slash exactly. Add every environment explicitly. + +### Password email does not arrive + +Verify SMTP host, credentials, sender address, port, and TLS mode. For Gmail, use an App Password. Test with a real +reset flow rather than relying only on the Admin Console connection button. + +### Role assignment fails despite `manage-users` + +Assigning client roles can require additional permissions in the installed Keycloak version. Test the exact operation +with the actual administrative principal, and do not solve the problem by granting `realm-admin` broadly. + +### Password endpoint returns `404` or `405` + +The direct Account API password endpoint is not universally supported. Use the OIDC Application Initiated Action with +`kc_action=UPDATE_PASSWORD`, then repeat the flow after major Keycloak upgrades. + +## References + +- [Keycloak documentation](https://www.keycloak.org/documentation) +- [Keycloak securing applications and services](https://www.keycloak.org/docs/latest/securing_apps/) +- [Keycloak Admin REST API](https://www.keycloak.org/docs-api/latest/rest-api/) +- [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html)