feat: Integrate CIE authentication flow

This commit is contained in:
gabdevele
2026-06-17 11:01:28 +00:00 Unverified
parent 8a94f36015
commit 917a12e3a8
20 changed files with 946 additions and 15 deletions
+4
View File
@@ -35,6 +35,10 @@ Authorization: Bearer <token>
sends OTP through Mailpit
- `POST /auth/otp/verify`
consumes OTP, returns bearer token
- `POST /auth/cie/start`
creates local CIE login state and returns Django RP authorization URL
- `GET /auth/cie/session/{state}`
frontend polling endpoint; returns final Visione bearer token on success
- `GET /me`
current user profile, roles, permissions, points
- `PATCH /me`
+7 -1
View File
@@ -25,6 +25,10 @@ generate public read-only intervention links.
## Runtime Model
- Auth uses bearer tokens from `api_tokens`.
- OTP login uses `otp_challenges` and Mailpit in local dev.
- CIE login uses external Django RP service:
- Visione creates local `state`
- Django RP completes CIE/OIDC and sends signed identity payload to Visione
- Visione creates or links local user, issues `api_tokens`, frontend polls session status
- DB access uses raw parameterized SQL through `drogon::orm::DbClient`.
- No Drogon generated ORM models. No `Mapper<T>`.
@@ -61,7 +65,7 @@ Effective permission helpers:
## Main Persistence Objects
- `users`, `roles`, `permissions`, `user_roles`, `role_permissions`
- `api_tokens`, `otp_challenges`
- `api_tokens`, `otp_challenges`, `cie_auth_sessions`, `user_external_identities`
- `report_categories`, `areas`, `team_categories`, `teams`
- `reports`, `report_status_history`, `assignments`, `report_photos`
- `messages`, `feedback`, `reward_events`, `notifications`
@@ -97,3 +101,5 @@ Output path:
still exposes citizen phone by design.
- Allowed-area containment for admin areas is vertex-based, simple not strict GIS-hard.
- No frontend doc set here beyond API/OpenAPI docs.
- CIE bridge trusts HMAC-signed callback from Django RP service; keep shared
secret rotated and never expose it to frontend.
+182
View File
@@ -0,0 +1,182 @@
# Visione CIE Authentication Integration
## Goal
Keep Visione backend simple.
Visione does not implement OIDC Federation or CIE protocol details directly.
`spid-cie-oidc-django` handles CIE login. Visione only manages:
- local login `state`
- user provisioning
- external identity linking
- local bearer token issuance
## Components
- Visione backend: `POST /api/v1/auth/cie/start`, `POST /api/v1/auth/cie/exchange`,
`GET /api/v1/auth/cie/session/{state}`
- Django RP service: `examples/relying_party`
- Frontend: browser redirect + polling
## End-to-End Flow
1. Frontend calls `POST /api/v1/auth/cie/start` with `frontend_redirect_uri`.
2. Visione stores row in `cie_auth_sessions` and returns `authorization_url`.
3. Frontend redirects browser to returned Django URL.
4. Django RP performs CIE login with provider.
5. On success Django extracts user attributes and sends signed JSON payload to
Visione `POST /api/v1/auth/cie/exchange`.
6. Visione verifies HMAC signature, validates `state`, finds or creates user,
stores `user_external_identities`, issues `api_tokens`, marks session authenticated.
7. Visione responds to Django with HTTP redirect to
`frontend_redirect_uri?state=<state>`.
8. Frontend reads `state` from query string and polls
`GET /api/v1/auth/cie/session/{state}` until authenticated.
9. Frontend stores returned Visione bearer token and calls `GET /api/v1/me`.
## Why This Architecture
- No OIDC/Federation crypto in C++ backend.
- No CIE protocol tables spread across Visione domain code.
- Django demo repo stays reusable and isolated.
- Visione keeps final auth authority for application tokens and user model.
## Visione Backend Config
`config.json`:
```json
"cie_auth": {
"service_base_url": "http://127.0.0.1:8001",
"start_path": "/visione/start",
"shared_secret": "change-me-for-local-dev",
"state_ttl_minutes": 10,
"default_frontend_redirect_uri": "http://localhost:3000/auth/cie/callback"
}
```
Meaning:
- `service_base_url`: Django RP base URL
- `start_path`: Django entrypoint used by frontend redirect
- `shared_secret`: HMAC secret shared only between Visione and Django
- `state_ttl_minutes`: local login state lifetime
- `default_frontend_redirect_uri`: fallback if frontend omits value
## Visione DB Additions
- `cie_auth_sessions`
stores local state, frontend redirect, final user id, final bearer token
- `user_external_identities`
stores `provider`, `issuer`, `subject`, `fiscal_code`, raw claims
User matching strategy:
1. exact external identity on `provider + issuer + subject`
2. fallback reunification on `fiscal_code`
3. otherwise create new `citizen`
## Signed Exchange Contract
Django sends `POST /api/v1/auth/cie/exchange` with JSON:
```json
{
"state": "visione-login-state",
"issuer": "http://127.0.0.1:8002/oidc/op",
"subject": "provider-subject",
"fiscal_code": "RSSMRA80A01H501U",
"first_name": "Mario",
"last_name": "Rossi",
"email": "[email protected]"
}
```
Headers:
- `Content-Type: application/json`
- `X-Visione-Signature: <UPPERCASE_HEX_HMAC_SHA256>`
Signature algorithm:
- payload bytes exactly as sent
- key = `cie_auth.shared_secret`
- HMAC-SHA256
- uppercase hex output
## Django Example Customization
Modified files in `spid-cie-oidc-django/examples/relying_party`:
- `relying_party/settingslocal.py.example`
- `relying_party/urls.py`
- `relying_party/visione_views.py`
Added endpoints:
- `GET /visione/start`
stores Visione `state` in Django session, then redirects to `/oidc/rp/authorization`
- `GET /visione/complete`
reads `oidc_rp_user_attrs`, signs payload, calls Visione exchange endpoint,
then redirects browser to frontend callback URL returned by Visione
Important:
- default OIDC callback remains managed by `spid_cie_oidc.relying_party`
- `LOGIN_REDIRECT_URL` now points to `/visione/complete`
- no need to modify RP metadata dump callback URI for this integration
## Frontend Responsibilities
1. Call:
```http
POST /api/v1/auth/cie/start
Content-Type: application/json
```
```json
{
"frontend_redirect_uri": "http://localhost:3000/auth/cie/callback"
}
```
2. Redirect browser to `authorization_url`.
3. On frontend callback page, read `state` from query string.
4. Poll:
```http
GET /api/v1/auth/cie/session/{state}
```
5. Handle states:
- `pending`: keep polling
- `authenticated`: save `auth.token`
- `failed`: show error and restart flow
6. After token, call `GET /api/v1/me`.
Frontend does not need:
- CIE secrets
- Django shared secret
- OIDC token parsing
## Local Dev Setup
1. Start Visione dependencies:
```bash
docker compose up -d
```
2. Run Visione:
```bash
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./build/visione-backend
```
3. In separate repo checkout for `spid-cie-oidc-django`, configure:
- federation authority example
- provider example
- relying party example with `VISIONE_BACKEND_EXCHANGE_URL`
- same `VISIONE_SHARED_SECRET` as Visione `cie_auth.shared_secret`
4. Start Django RP on `http://127.0.0.1:8001`.
## Security Notes
- Shared secret must never reach frontend.
- `cie_auth.shared_secret` must change outside local dev.
- Keep `state_ttl_minutes` short.
- Visione bearer token stays out of query string; only `state` goes to frontend.
- Django service is trusted bridge. Protect it as internal auth component.
## Future Improvements
- Mark failed sessions explicitly when Django login fails
- Rotate HMAC secret through environment variables
- Add replay protection with timestamp/nonce in signed payload
- Add integration tests between Django RP and Visione
+6
View File
@@ -11,6 +11,8 @@ Schema truth is `migrations/*.sql`. This file is compact human map of current DB
notifications, moderation flags, reward uniqueness
- `007_reference_data_and_intervention_links.sql`
category-to-team-category mapping, public intervention links
- `011_cie_auth_bridge.sql`
lightweight CIE bridge sessions and external identity links
## Core Auth Tables
- `users`
@@ -23,6 +25,10 @@ Schema truth is `migrations/*.sql`. This file is compact human map of current DB
OTP flow, hashed codes, expiry, consumption
- `api_tokens`
bearer token hashes, expiry, revocation
- `cie_auth_sessions`
frontend-visible CIE login state, final issued token, expiry
- `user_external_identities`
links local users to external providers using `provider + issuer + subject`
## Reference Data
- `report_categories`
+10
View File
@@ -31,9 +31,18 @@ public revocable link generated by operator/admin.
- Registration creates `citizen`.
- Login path:
- OTP email via Mailpit
- CIE via external `spid-cie-oidc-django` relying party service
- Auth token: bearer token from `api_tokens`
- Banned users cannot authenticate.
### CIE Bridge Flow
1. Frontend calls `POST /api/v1/auth/cie/start`.
2. Visione creates `cie_auth_sessions.state` and returns Django start URL.
3. Browser completes CIE login on Django RP service.
4. Django sends signed payload to `POST /api/v1/auth/cie/exchange`.
5. Visione finds or creates user, stores `user_external_identities`, issues token.
6. Frontend polls `GET /api/v1/auth/cie/session/{state}` until `authenticated`.
## Current Report Workflow
### Citizen flow
1. Register/login.
@@ -116,6 +125,7 @@ public revocable link generated by operator/admin.
## Files To Read First
- `docs/API_SPEC.md`
- `docs/ARCHITECTURE.md`
- `docs/CIE_AUTH_INTEGRATION.md`
- `docs/openapi/visione.openapi.yaml`
- `controllers/*.h`
- `services/*.cc`
+197 -1
View File
@@ -1496,6 +1496,99 @@
],
"type": "object"
},
"CieStartRequest": {
"additionalProperties": false,
"properties": {
"frontend_redirect_uri": {
"type": "string"
}
},
"required": [
"frontend_redirect_uri"
],
"type": "object"
},
"CieStartResponse": {
"additionalProperties": false,
"properties": {
"authorization_url": {
"type": "string"
},
"ok": {
"type": "boolean"
},
"state": {
"type": "string"
},
"status": {
"type": "string"
}
},
"required": [
"ok",
"authorization_url",
"state",
"status"
],
"type": "object"
},
"CieExchangeRequest": {
"additionalProperties": false,
"properties": {
"email": {
"type": "string"
},
"fiscal_code": {
"type": "string"
},
"first_name": {
"type": "string"
},
"issuer": {
"type": "string"
},
"last_name": {
"type": "string"
},
"state": {
"type": "string"
},
"subject": {
"type": "string"
}
},
"required": [
"state",
"issuer",
"subject",
"fiscal_code"
],
"type": "object"
},
"CieSessionResponse": {
"additionalProperties": false,
"properties": {
"auth": {
"additionalProperties": true,
"type": "object"
},
"ok": {
"type": "boolean"
},
"status": {
"type": "string"
},
"user_id": {
"format": "int64",
"type": "integer"
}
},
"required": [
"ok",
"status"
],
"type": "object"
},
"UpdateModerationRequest": {
"additionalProperties": false,
"properties": {
@@ -1610,7 +1703,110 @@
"version": "0.1.0"
},
"openapi": "3.0.3",
"paths": {
"paths": {
"/api/v1/auth/cie/exchange": {
"post": {
"description": "Completes CIE bridge exchange after Django RP verifies the identity and posts signed user claims.",
"operationId": "AuthController.completeCieLogin",
"parameters": [ ],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CieExchangeRequest"
}
}
},
"required": true
},
"responses": {
"302": {
"description": "Redirect to frontend callback URL with state query parameter."
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
},
"summary": "Complete CIE login",
"tags": [
"Auth"
]
}
},
"/api/v1/auth/cie/session/{id}": {
"get": {
"description": "Returns status for CIE login session started by Visione.",
"operationId": "AuthController.cieSessionStatus",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CieSessionResponse"
}
}
},
"description": "Current login session status."
},
"404": {
"$ref": "#/components/responses/NotFound"
}
},
"summary": "CIE session status",
"tags": [
"Auth"
]
}
},
"/api/v1/auth/cie/start": {
"post": {
"description": "Creates a local CIE login session and returns Django RP authorization URL.",
"operationId": "AuthController.startCieLogin",
"parameters": [ ],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CieStartRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CieStartResponse"
}
}
},
"description": "Authorization URL and local state."
},
"400": {
"$ref": "#/components/responses/BadRequest"
}
},
"summary": "Start CIE login",
"tags": [
"Auth"
]
}
},
"/api/v1/admin/areas": {
"post": {
"description": "Creates a municipal area with GeoJSON boundary.",