Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ build/
# Local environment secrets
.env

# PlantUML local cache for diagram generation
docs/diagrams/.plantuml-cache/
107 changes: 16 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Reactive OAuth2 proxy/service built with Spring Boot 4 + Java 25, using WebFlux,
- `SECURITY.md` - security policy and runtime security model
- `CONTRIBUTING.md` - contribution workflow and coding/testing standards
- `CHANGELOG.md` - notable project changes
- `docs/architecture.md` - architecture and sequence diagrams (PlantUML + PNG)

This project keeps the same business scenarios as `jwt-demo`, but the implementation is fully reactive:
- WebFlux controllers (`Mono<AppResponse<...>>`)
Expand Down Expand Up @@ -205,83 +206,31 @@ Authorization options:

### Login flow

```mermaid
sequenceDiagram
actor C as Client
participant S as Spring Boot (AuthController)
participant K as Keycloak

C->>S: 1) POST /api/auth/login<br/>{username, password, clientId, clientSecret}
S->>K: 2) KeycloakReactiveAuthService.login()
K->>K: 3) POST /realms/my-realm/protocol/openid-connect/token<br/>grant_type=password<br/>username, password<br/>client_id, client_secret
K-->>S: 4) 200 OK<br/>{access_token, refresh_token}
S-->>C: 5) AppResponse(code=0, data=tokens)
```
![Login flow](docs/diagrams/sequence-auth-login.png)

Source: `docs/diagrams/sequence-auth-login.puml`

### Refresh flow

```mermaid
sequenceDiagram
actor C as Client
participant S as Spring Boot (AuthController)
participant K as Keycloak

C->>S: 1) POST /api/auth/refresh<br/>{refreshToken, clientId, clientSecret}
S->>K: 2) KeycloakReactiveAuthService.refresh()
K->>K: 3) POST /realms/my-realm/protocol/openid-connect/token<br/>grant_type=refresh_token<br/>refresh_token<br/>client_id, client_secret
K-->>S: 4) 200 OK<br/>{new_access_token, new_refresh_token}
S-->>C: 5) AppResponse(code=0, data=tokens)
```
![Refresh flow](docs/diagrams/sequence-auth-refresh.png)

Source: `docs/diagrams/sequence-auth-refresh.puml`

### Logout flow

```mermaid
sequenceDiagram
actor C as Client
participant S as Spring Boot (AuthController)
participant K as Keycloak

C->>S: 1) POST /api/auth/logout<br/>{refreshToken, clientId, clientSecret}
S->>K: 2) KeycloakReactiveAuthService.logout()
K->>K: 3) POST /realms/my-realm/protocol/openid-connect/logout<br/>client_id, client_secret<br/>refresh_token
K-->>S: 4) 200 OK (Keycloak behavior)
S-->>C: 5) AppResponse(code=0)
```
![Logout flow](docs/diagrams/sequence-auth-logout.png)

Source: `docs/diagrams/sequence-auth-logout.puml`

---

## 📬 Asynchronous Client Creation Flow

`POST /api/clients` does not create a client synchronously.

```mermaid
sequenceDiagram
actor C as Caller
participant A as API (/api/clients)
participant D as request table (PostgreSQL)
participant W as Request Worker

C->>A: POST /api/clients
A->>A: Validate payload
A->>D: INSERT type=CLIENT_CREATE, status=PENDING
A-->>C: AppResponse(code=0, data={requestId})

loop Poll until terminal status
C->>A: GET /api/requests/{id}
A->>D: SELECT status by id
A-->>C: status=PENDING|PROCESSING|COMPLETED|FAILED
end

W->>D: Reclaim stale PROCESSING rows
W->>D: Claim PENDING batch (FOR UPDATE SKIP LOCKED)
W->>D: UPDATE status=PROCESSING

alt Success
W->>D: UPDATE status=COMPLETED, response_json
else Failure
W->>D: UPDATE status=FAILED, error_json
end
```
![Asynchronous client creation flow](docs/diagrams/sequence-async-client-create.png)

Source: `docs/diagrams/sequence-async-client-create.puml`

For multi-instance safety, stale `PROCESSING` reclaim is implemented and indexed (`V2__add_request_reclaim_index.sql`).

Expand Down Expand Up @@ -345,32 +294,9 @@ Recommended settings:
- `management.otlp.metrics.export.enabled=false`
- `management.tracing.sampling.probability=1.0`

```mermaid
flowchart LR
subgraph App[Spring Boot jwt-demo-reactive]
A1[HTTP metrics\nActuator /prometheus]
A2[Traces OTLP\nmanagement.otlp.tracing.endpoint]
A3[Logs OTLP\nmanagement.otlp.logging.endpoint]
end

subgraph Infra[Observability Infra]
C[OTel Collector]
T[Tempo]
L[Loki]
P[Prometheus]
G[Grafana]
end

A1 -->|pull /actuator/prometheus| P
A2 -->|OTLP traces| C
A3 -->|OTLP logs| C
C -->|traces| T
C -->|logs| L

P --> G
T --> G
L --> G
```
![Observability data flow](docs/diagrams/sequence-observability-flow.png)

Source: `docs/diagrams/sequence-observability-flow.puml`

---

Expand Down Expand Up @@ -472,4 +398,3 @@ Main integration suites:
- This is the fastest path to diagnose `401`, `403`, and `429` scenarios across API + security filters.

---

39 changes: 39 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Architecture Diagrams

Diagrams are stored in `docs/diagrams/` as PlantUML source and exported PNG files.

To regenerate PNG files:

```pwsh
./docs/diagrams/generate-diagrams.ps1
```

## Sequence: Login flow

![Login flow](./diagrams/sequence-auth-login.png)

Source: `docs/diagrams/sequence-auth-login.puml`

## Sequence: Refresh flow

![Refresh flow](./diagrams/sequence-auth-refresh.png)

Source: `docs/diagrams/sequence-auth-refresh.puml`

## Sequence: Logout flow

![Logout flow](./diagrams/sequence-auth-logout.png)

Source: `docs/diagrams/sequence-auth-logout.puml`

## Sequence: Asynchronous client creation

![Asynchronous client creation](./diagrams/sequence-async-client-create.png)

Source: `docs/diagrams/sequence-async-client-create.puml`

## Sequence: Observability data flow

![Observability data flow](./diagrams/sequence-observability-flow.png)

Source: `docs/diagrams/sequence-observability-flow.puml`
23 changes: 23 additions & 0 deletions docs/diagrams/generate-diagrams.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
param(
[string]$PlantUmlVersion = "1.2025.3"
)

$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$cacheDir = Join-Path $scriptDir ".plantuml-cache"
$tempJar = Join-Path $cacheDir "plantuml-$PlantUmlVersion.jar"
$downloadUrl = "https://github.com/plantuml/plantuml/releases/download/v$PlantUmlVersion/plantuml-$PlantUmlVersion.jar"
Comment thread
igorsatsyuk marked this conversation as resolved.

if (-not (Test-Path $cacheDir)) {
New-Item -ItemType Directory -Path $cacheDir | Out-Null
}

if (-not (Test-Path $tempJar)) {
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempJar -ErrorAction Stop
}

$pumlFiles = Get-ChildItem -Path $scriptDir -Filter "*.puml" | Select-Object -ExpandProperty FullName
if (-not $pumlFiles) {
throw "No .puml files found in $scriptDir"
}
Comment thread
igorsatsyuk marked this conversation as resolved.

java -jar $tempJar -charset UTF-8 -tpng @pumlFiles
Comment thread
igorsatsyuk marked this conversation as resolved.
Binary file added docs/diagrams/sequence-async-client-create.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions docs/diagrams/sequence-async-client-create.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@startuml
title Asynchronous client creation flow

autonumber
actor "Caller" as C
participant "API\nPOST /api/clients" as A
database "request table\n(PostgreSQL)" as D
participant "Request Worker" as W

C -> A: POST /api/clients
A -> A: Validate payload
A -> D: INSERT type=CLIENT_CREATE,\nstatus=PENDING
A --> C: AppResponse(code=0,\ndata={requestId})

loop Poll until terminal status
C -> A: GET /api/requests/{id}
A -> D: SELECT status by id
A --> C: status=PENDING|PROCESSING|COMPLETED|FAILED
end

W -> D: Reclaim stale PROCESSING rows
W -> D: Claim PENDING batch\n(FOR UPDATE SKIP LOCKED)
W -> D: UPDATE status=PROCESSING

alt Success
W -> D: UPDATE status=COMPLETED,\nresponse_json
else Failure
W -> D: UPDATE status=FAILED,\nerror_json
end

@enduml
Binary file added docs/diagrams/sequence-auth-login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions docs/diagrams/sequence-auth-login.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@startuml
title Login flow

autonumber
actor "Client" as C
participant "jwt-demo-reactive\nAuthController" as S
participant "Keycloak\nToken endpoint" as K

C -> S: POST /api/auth/login\n{username, password, clientId, clientSecret}
S -> K: grant_type=password\nusername, password\nclient_id, client_secret
K --> S: 200 OK\n{access_token, refresh_token}
S --> C: AppResponse(code=0, data=tokens)

@enduml
Binary file added docs/diagrams/sequence-auth-logout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions docs/diagrams/sequence-auth-logout.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@startuml
title Logout flow

autonumber
actor "Client" as C
participant "jwt-demo-reactive\nAuthController" as S
participant "Keycloak\nLogout endpoint" as K

C -> S: POST /api/auth/logout\n{refreshToken, clientId, clientSecret}
S -> K: client_id, client_secret\nrefresh_token
K --> S: 200 OK
Comment thread
igorsatsyuk marked this conversation as resolved.
S --> C: AppResponse(code=0)

@enduml
Binary file added docs/diagrams/sequence-auth-refresh.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions docs/diagrams/sequence-auth-refresh.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@startuml
title Refresh flow

autonumber
actor "Client" as C
participant "jwt-demo-reactive\nAuthController" as S
participant "Keycloak\nToken endpoint" as K

C -> S: POST /api/auth/refresh\n{refreshToken, clientId, clientSecret}
S -> K: grant_type=refresh_token\nrefresh_token\nclient_id, client_secret
K --> S: 200 OK\n{access_token, refresh_token}
S --> C: AppResponse(code=0, data=tokens)
Comment thread
igorsatsyuk marked this conversation as resolved.

@enduml
Binary file added docs/diagrams/sequence-observability-flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions docs/diagrams/sequence-observability-flow.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@startuml
title Observability data flow (OTLP-first)

autonumber
participant "Spring Boot app\njwt-demo-reactive" as App
participant "Prometheus" as Prom
participant "OTel Collector" as OTel
participant "Tempo" as Tempo
participant "Loki" as Loki
participant "Grafana" as Grafana

Prom -> App: pull /actuator/prometheus
App -> OTel: OTLP traces\nmanagement.otlp.tracing.endpoint
App -> OTel: OTLP logs\nmanagement.otlp.logging.endpoint
OTel -> Tempo: export traces
OTel -> Loki: export logs
Grafana -> Prom: query metrics datasource
Grafana -> Tempo: query traces datasource
Grafana -> Loki: query logs datasource

@enduml