Skip to content

Commit 8e28338

Browse files
authored
Merge pull request #117 from bitloops/modernise/pnpm-frontend-foundation
Adopt Keycloak IAM and fixed to UI style issue
2 parents b615a20 + fa89c1c commit 8e28338

163 files changed

Lines changed: 3000 additions & 4340 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,43 @@ All notable changes to this project are documented in this file. The format is
44
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
55
project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
9+
## [1.0.1] - 2026-07-31
10+
11+
### Added
12+
13+
- Keycloak 26.7 with an imported development realm, isolated PostgreSQL
14+
database, Compose services, and Kubernetes manifests.
15+
- OpenID Connect Authorization Code Flow with PKCE in the frontend and
16+
standards-based RS256/JWKS access-token validation in the backend.
17+
- An IAM anti-corruption layer that maps Keycloak issuer/subject identities to
18+
internal user UUIDs.
19+
- A transactional IAM outbox for user registration and email-change
20+
integration events.
21+
- OIDC validation tests covering issuer, audience, authorised client, expiry,
22+
verified email, and signing-key rotation, plus identity reconciliation
23+
integration tests.
24+
- React 19.2 and React Router 8.3, including the patched router release for the
25+
current RSC action security advisory.
26+
27+
### Changed
28+
29+
- Replaced application-owned credentials and sessions with Keycloak while
30+
retaining the IAM bounded context and `/auth/me` reconciliation endpoint.
31+
- Kept browser access tokens in memory and limited session storage to transient
32+
PKCE state.
33+
- Restored an existing Keycloak SSO session non-interactively when the
34+
application opens in another browser window.
35+
- Regenerated the OpenAPI client after removing local authentication routes.
36+
- Aligned the root, backend, frontend, and frontend-test package versions at
37+
`1.0.1`.
38+
39+
### Removed
40+
41+
- Passport strategies, password hashing, local registration/login commands,
42+
locally issued JWTs, and the old `/auth/login` and `/auth/register` routes.
43+
744
## [1.0.0] - 2026-07-31
845

946
### Added

K8s.md

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,60 @@
11
# Deploying on Kubernetes
22

3-
The manifests under `k8s` deploy the frontend, backend, PostgreSQL, NATS, and
4-
the observability services. MongoDB and the former Envoy/gRPC edge are no
5-
longer part of the deployment.
3+
The sample manifests under `k8s` deploy the frontend, backend, application
4+
PostgreSQL, NATS, Keycloak with its own PostgreSQL database, and the
5+
observability services. MongoDB and the former Envoy/gRPC edge are not part of
6+
the deployment.
67

7-
## Prepare images and secrets
8+
## Prepare images and identity settings
89

9-
Build and publish the application images, or load them into your local cluster:
10+
Build and publish the application images, or load them into a local cluster:
1011

1112
```bash
1213
docker build -f backend/Dockerfile -t todo-backend:latest .
13-
docker build -f frontend/Dockerfile -t todo-frontend:latest .
14+
docker build -f frontend/Dockerfile \
15+
--build-arg VITE_API_BASE_URL=http://localhost:8080 \
16+
--build-arg VITE_OIDC_AUTHORITY=http://localhost:8090/realms/bitloops \
17+
--build-arg VITE_OIDC_CLIENT_ID=todo-frontend \
18+
-t todo-frontend:latest .
1419
```
1520

16-
For a remote registry, change the image names in
17-
`k8s/deployment-todo-backend.yaml` and
18-
`k8s/deployment-todo-frontend.yaml` to immutable, registry-qualified tags.
21+
For a registry, change both deployment image references to immutable,
22+
registry-qualified tags. Before applying, replace every value in
23+
`k8s/secret-bl-postgres-secret.yaml` and
24+
`k8s/secret-bl-keycloak-secret.yaml`; production secrets belong in an external
25+
secret manager, not Git.
1926

20-
Before applying the manifests, replace every development value in
21-
`k8s/secret-bl-postgres-secret.yaml`, especially `POSTGRES_PASSWORD` and
22-
`JWT_SECRET`. Do not commit production secrets. PostgreSQL is the sole
23-
application database and uses the `bitloops` database by default.
27+
The committed realm and hostnames are for local development. For an exposed
28+
cluster, update all of the following to the same public HTTPS identity origin:
29+
30+
- `KC_HOSTNAME` in `deployment-bl-keycloak.yaml`;
31+
- `OIDC_ISSUER` in `deployment-todo-backend.yaml`;
32+
- the frontend `VITE_OIDC_AUTHORITY` build argument; and
33+
- redirect URIs, post-logout URIs, and web origins in the realm import.
34+
35+
Keep `OIDC_JWKS_URI` on the cluster-internal Keycloak service. Enable realm
36+
email verification and `OIDC_REQUIRE_VERIFIED_EMAIL` together in production.
2437

2538
## Validate and apply
2639

27-
Perform a client-side validation first:
40+
Perform client-side validation first:
2841

2942
```bash
3043
kubectl apply --dry-run=client --validate=false -f k8s
3144
```
3245

33-
Apply the namespace and resources in dependency order with:
46+
Apply resources in dependency order:
3447

3548
```bash
3649
./apply_k8s_files.sh
3750
```
3851

39-
The application image currently initialises its schemas idempotently at
40-
startup. For a production deployment, introduce a dedicated migration job
41-
before scaling the backend beyond one replica.
52+
Keycloak health and metrics use management port `9000`; do not expose that
53+
port publicly in a production ingress. The sample uses one replica and a realm
54+
import for clarity. A production installation should use externally managed
55+
secrets, TLS, backups, suitable high availability, and a supported ingress
56+
configuration.
4257

43-
Keycloak is not included in the 1.0.0 manifests. Its proposed database and
44-
deployment boundary are documented in
45-
[`docs/keycloak-iam-roadmap.md`](./docs/keycloak-iam-roadmap.md).
58+
The backend currently creates schemas idempotently at startup. Introduce a
59+
dedicated migration job before scaling it beyond one replica. More detail is
60+
in [Keycloak IAM architecture](./docs/keycloak-iam-roadmap.md).

README.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
Complete working example of using Domain Driven Design (DDD), Hexagonal Architecture, CQRS, Event Sourcing (ES), Event Driven Architecture (EDA), Behaviour Driven Development (BDD) using TypeScript and NestJS.
66

7-
Current release: **1.0.0**. See the [changelog](./CHANGELOG.md), the
8-
[backend architecture](./docs/backend-architecture.md), and the proposed
9-
[Keycloak IAM roadmap](./docs/keycloak-iam-roadmap.md).
7+
Current release: **1.0.1**. See the [changelog](./CHANGELOG.md), the
8+
[backend architecture](./docs/backend-architecture.md), and the
9+
[Keycloak IAM architecture](./docs/keycloak-iam-roadmap.md).
1010

1111
![ddd-hexagonal-cqrs-es-eda](https://storage.googleapis.com/bitloops-github-assets/ddd-hexagonal-cqrs-es-eda-2.gif)
1212

@@ -79,15 +79,15 @@ When a todo is completed, if this is the first completed todo, an email should b
7979
- **Easy switching between modular monolith and microservices**
8080
- **Authentication**
8181
- **Authorization** (Even at the repository level)
82-
- **Automatic JWT renewal**
82+
- **OpenID Connect session renewal**
8383
- **Automatic client code generation using OpenAPI**
8484
- **Event-sourced Todo aggregate with a transactional outbox**
8585

8686
## Technologies Used - Overview
8787

8888
Here are listed some of the specific technologies used for the implementation of the project:
8989

90-
- **Authentication**: Application-issued JWTs in 1.0.0, with a documented migration path to [Keycloak](./docs/keycloak-iam-roadmap.md)
90+
- **Authentication**: [Keycloak](https://www.keycloak.org/) through OpenID Connect Authorization Code Flow with PKCE
9191
- **Database - Persistence**: [PostgreSQL](https://www.postgresql.org/)
9292
- **Testing**: [JEST](https://jestjs.io/)
9393
- **External Communication Protocols**: [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) and server-sent events
@@ -143,6 +143,11 @@ from the terminal inside the project **in order to download and run the necessar
143143

144144
Then the ReactJS front-end application will be visible at: `http://localhost:4173`.
145145

146+
The development realm includes `demo@example.com` with password
147+
`Todo-Demo-2026!`. Keycloak runs at `http://localhost:8090`; its development
148+
admin credentials are `admin` / `admin-development-only`. These credentials
149+
must never be used outside local development.
150+
146151
<p align="center" style="margin-bottom: 0px !important;">
147152
<img width="400" alt="image" src="https://github.com/bitloops/ddd-hexagonal-cqrs-es-eda/assets/1571105/4570473b-4e67-4050-9935-967acfe0b7c6" alt="Frontend application" align="center">
148153
</p>
@@ -192,12 +197,12 @@ The problem in this case is that the email information belongs to the IAM bounde
192197

193198
In this project the decision was to keep a local repository in the Marketing bounded context, of the users and their emails, updated by listening to integration events from the IAM bounded contexts (**user registered** and **user email changed**).
194199

195-
Authentication is currently implemented inside the application for continuity
196-
with the original example. It is now treated as a transitional adapter rather
197-
than the intended long-term identity system. The next IAM tranche will move
198-
credential and session ownership to Keycloak while retaining the IAM bounded
199-
context and translating OIDC claims through an anti-corruption layer. See the
200-
[Keycloak IAM roadmap](./docs/keycloak-iam-roadmap.md).
200+
Keycloak owns credentials and browser sessions, while the IAM bounded context
201+
owns the application's user identity. An anti-corruption layer validates OIDC
202+
claims and maps Keycloak's issuer/subject pair to an internal UUID. IAM
203+
registration and email changes reach Marketing through an application-owned
204+
transactional outbox, so downstream contexts do not depend on Keycloak. See
205+
the [Keycloak IAM architecture](./docs/keycloak-iam-roadmap.md).
201206

202207
# V. Running in development mode
203208

@@ -243,7 +248,8 @@ cd ddd-hexagonal-cqrs-es-eda
243248
The frontend is then available at `http://localhost:4173` and the backend at `http://localhost:8080`.
244249
- For local backend development, start only its infrastructure dependencies:
245250
```bash
246-
docker compose -p bitloops-todo-app up -d bl-nats bl-postgres
251+
docker compose -p bitloops-todo-app up -d \
252+
bl-nats bl-postgres bl-keycloak-postgres bl-keycloak
247253
```
248254
- For local backend development, copy `backend/.template-env` to `backend/.development.env` and replace the development-only values.
249255
- Run:
@@ -257,7 +263,9 @@ cd ddd-hexagonal-cqrs-es-eda
257263
### Test the application is running
258264

259265
In order to test the application is running we could use a client
260-
The application uses **REST** for authentication and todo operations, with **server-sent events (SSE)** for realtime client updates.
266+
The application uses **OpenID Connect** with Keycloak for authentication,
267+
**REST** for Todo operations, and **server-sent events (SSE)** for realtime
268+
client updates.
261269

262270
Those tools could be helpful in the development process as well.
263271

apply_k8s_files.sh

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,28 @@ kubectl apply -f namespace-bitloops.yaml
88

99
# Apply ConfigMaps, Secrets, and Persistent Volume Claims
1010
kubectl apply -f configmap-bl-grafana-config.yaml
11+
kubectl apply -f configmap-bl-keycloak-realm.yaml
1112
kubectl apply -f secret-bl-postgres-secret.yaml
13+
kubectl apply -f secret-bl-keycloak-secret.yaml
1214
kubectl apply -f pvc-bl-nats-data.yaml
1315
kubectl apply -f pvc-bl-postgres-data.yaml
16+
kubectl apply -f pvc-bl-keycloak-postgres-data.yaml
1417
kubectl apply -f pvc-bl-prometheus-data.yaml
1518
kubectl apply -f pvc-bl-grafana-data.yaml
1619

1720
# Apply Deployments and Services
18-
kubectl apply -f deployment-todo-frontend.yaml
19-
kubectl apply -f service-todo-frontend.yaml
20-
kubectl apply -f deployment-todo-backend.yaml
21-
kubectl apply -f service-todo-backend.yaml
2221
kubectl apply -f deployment-bl-nats.yaml
2322
kubectl apply -f service-bl-nats.yaml
2423
kubectl apply -f deployment-bl-postgres.yaml
2524
kubectl apply -f service-bl-postgres.yaml
25+
kubectl apply -f deployment-bl-keycloak-postgres.yaml
26+
kubectl apply -f service-bl-keycloak-postgres.yaml
27+
kubectl apply -f deployment-bl-keycloak.yaml
28+
kubectl apply -f service-bl-keycloak.yaml
29+
kubectl apply -f deployment-todo-backend.yaml
30+
kubectl apply -f service-todo-backend.yaml
31+
kubectl apply -f deployment-todo-frontend.yaml
32+
kubectl apply -f service-todo-frontend.yaml
2633
kubectl apply -f deployment-bl-prometheus-nats-exporter.yaml
2734
kubectl apply -f service-bl-prometheus-nats-exporter.yaml
2835
kubectl apply -f deployment-bl-jaeger.yaml

backend/.template-env

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
# copy and rename this to .development.env to add your configurations
2-
JWT_SECRET=p2s5v8x/A?D(G+KbPeShVmYq3t6w9z$B # !!! replace this with your own secret
3-
JWT_LIFETIME_SECONDS=3600
1+
# Copy and rename this file to .development.env for local backend development.
2+
OIDC_ISSUER=http://localhost:8090/realms/bitloops
3+
OIDC_AUDIENCE=todo-api
4+
OIDC_CLIENT_ID=todo-frontend
5+
OIDC_JWKS_URI=http://localhost:8090/realms/bitloops/protocol/openid-connect/certs
6+
OIDC_REQUIRE_VERIFIED_EMAIL=false
7+
OIDC_CLOCK_TOLERANCE_SECONDS=5
48
HTTP_IP=localhost
59
PG_HOST=localhost
610
PG_PORT=5432

backend/development-steps.md

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- Corepack with the repository-pinned pnpm version
77
- Docker and Docker Compose
88

9-
Install the complete workspace from the repository root:
9+
Install the workspace from the repository root:
1010

1111
```bash
1212
corepack enable
@@ -15,44 +15,47 @@ pnpm install --frozen-lockfile
1515

1616
## Configuration and infrastructure
1717

18-
Copy `backend/.template-env` to `backend/.development.env` and replace the
19-
development JWT secret. Start PostgreSQL and NATS:
18+
Copy `backend/.template-env` to `backend/.development.env`. Start the backend's
19+
dependencies, including the OIDC issuer:
2020

2121
```bash
22-
docker compose -p bitloops-todo-app up -d bl-postgres bl-nats
22+
docker compose -p bitloops-todo-app up -d \
23+
bl-postgres bl-nats bl-keycloak-postgres bl-keycloak
2324
```
2425

25-
PostgreSQL is the only application database. The backend creates the IAM,
26-
Marketing, Todo event-store, projection, and outbox tables idempotently during
27-
startup.
26+
The default issuer is `http://localhost:8090/realms/bitloops`; the backend uses
27+
the internal Keycloak URL only for JWKS retrieval. PostgreSQL is the only
28+
application database. Keycloak owns a separate database that application code
29+
must not access.
2830

2931
Start the backend in watch mode:
3032

3133
```bash
3234
pnpm --dir backend start:dev
3335
```
3436

35-
The REST API listens on `http://localhost:8080`, Swagger UI is available at
36-
`/api`, and the OpenAPI document at `/api-json`.
37+
The REST API listens on `http://localhost:8080`, Swagger UI is at `/api`, and
38+
the OpenAPI document is at `/api-json`.
3739

3840
## Module boundaries
3941

4042
- `src/api` contains REST and SSE driving adapters.
4143
- `src/lib/bounded-contexts` contains application and domain code.
42-
- `src/bounded-contexts` contains PostgreSQL, NATS, and service adapters.
44+
- `src/bounded-contexts` contains OIDC, PostgreSQL, NATS, and service adapters.
4345
- `src/lib/infra` contains reusable NestJS infrastructure.
4446

45-
Controllers dispatch commands and queries through NATS request/reply. The Todo
46-
write adapter reconstructs aggregates from `todo_events`, then commits events,
47-
the query projection, and outbox rows in one PostgreSQL transaction. See
48-
[`../docs/backend-architecture.md`](../docs/backend-architecture.md).
47+
The OIDC guard validates the external access token and the IAM repository
48+
reconciles it to an internal UUID. Controllers dispatch Todo commands and
49+
queries through NATS. The Todo write adapter reconstructs aggregates from
50+
`todo_events`, then commits events, projection updates, and outbox rows in one
51+
transaction. See [backend architecture](../docs/backend-architecture.md).
4952

5053
## Validation
5154

5255
```bash
53-
pnpm --dir backend run build
54-
pnpm --dir backend run lint
55-
pnpm --dir backend run test
56+
pnpm --dir backend build
57+
pnpm --dir backend lint
58+
pnpm --dir backend test
5659
```
5760

5861
Run the database integration lane against an isolated test database:
@@ -61,19 +64,16 @@ Run the database integration lane against an isolated test database:
6164
PG_DATABASE=bitloops_test \
6265
PG_USER=user \
6366
PG_PASSWORD=postgres \
64-
pnpm --dir backend run test:integration
67+
pnpm --dir backend test:integration
6568
```
6669

67-
## Tracing
70+
## IAM configuration
6871

69-
Register `TracingModule` with the system message bus and apply `@Traceable` to
70-
async application or adapter methods. Tracing publication is deliberately
71-
isolated from business results: an observability outage must not turn a valid
72-
command into an HTTP failure.
72+
`OIDC_ISSUER`, `OIDC_AUDIENCE`, and `OIDC_CLIENT_ID` are mandatory.
73+
`OIDC_JWKS_URI` supports an internal JWKS endpoint while issuer validation
74+
continues to use the public URL. Enable `OIDC_REQUIRE_VERIFIED_EMAIL` in
75+
environments whose Keycloak realm enforces email verification.
7376

74-
## IAM direction
75-
76-
The application-owned password and JWT implementation is transitional. New IAM
77-
work should follow the
78-
[`../docs/keycloak-iam-roadmap.md`](../docs/keycloak-iam-roadmap.md) and keep
79-
Keycloak behind an OpenID Connect anti-corruption adapter.
77+
Keycloak remains behind `IdentityProviderPort`; application and domain code
78+
must not import Keycloak libraries or consume Keycloak database records. See
79+
[Keycloak IAM architecture](../docs/keycloak-iam-roadmap.md).

0 commit comments

Comments
 (0)