Skip to content

Commit 863bdb8

Browse files
committed
docs: add ADR-0009 for Akrites CDP public API authentication (CM-1333)
Signed-off-by: Joana Maia <jmaia@contractor.linuxfoundation.org>
1 parent 7a486b3 commit 863bdb8

2 files changed

Lines changed: 205 additions & 0 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# ADR-0009: Akrites → CDP public API authentication
2+
3+
**Date**: 2026-07-15
4+
**Status**: proposed
5+
**Deciders**: CDP team, LF Auth, Akrites team
6+
7+
## Context
8+
9+
Akrites is a new upstream consumer that needs read-only access to CDP's public
10+
API. LF Auth provisions LF-managed M2M credentials using RSA keypair
11+
authentication: Akrites signs a JWT `client_assertion` with a vault-resident
12+
private key, exchanges it at LFX Auth0 for a short-lived Bearer token, then
13+
calls CDP. CDP verifies the token, enforces a single-consumer restriction, and
14+
gates endpoints on a dedicated scope.
15+
16+
The Auth0 audience (resource-server identifier) is not yet finalized — it is
17+
referenced throughout as `{{AKRITES_CDP_AUDIENCE}}`.
18+
19+
High-level overview: https://linuxfoundation-dxwx.dsp.so/fkNtkFCx-akrites-cdp-auth
20+
21+
## Decision
22+
23+
Authenticate Akrites via **LFX Auth0 with a dedicated resource server and
24+
required scopes**. On the CDP side, add an `azp` allowlist middleware
25+
after JWT verification that asserts the token was issued specifically to
26+
`{{AKRITES_AUTH0_CLIENT_ID}}`, so no other M2M client granted the same
27+
audience can reach the `/akrites` router.
28+
29+
## Auth Flow
30+
31+
```mermaid
32+
sequenceDiagram
33+
participant Vault as LF Vault
34+
participant Akrites
35+
participant Auth0 as LFX Auth0
36+
participant CDP
37+
38+
Note over Akrites,Vault: On startup or after rotation
39+
Akrites->>Vault: read RSA private key
40+
Vault-->>Akrites: private key
41+
42+
Note over Akrites,Auth0: Token exchange (repeated on expiry)
43+
Akrites->>Akrites: sign client_assertion JWT (RS256)
44+
Akrites->>Auth0: POST /oauth/token (client_credentials + jwt-bearer)
45+
Auth0-->>Akrites: Bearer access_token (scope=read:<resource>)
46+
47+
Note over Akrites,CDP: API call
48+
Akrites->>CDP: GET /public/v1/akrites/* + Bearer token
49+
CDP->>Auth0: fetch JWKS (cached)
50+
CDP->>CDP: oauth2Middleware: verify sig + iss + aud
51+
CDP->>CDP: azpAllowlistMiddleware: assert azp == AKRITES_CLIENT_ID
52+
CDP->>CDP: requireScopes: assert read:<resource>
53+
CDP-->>Akrites: 200 OK
54+
55+
Note over Akrites,Auth0: On invalid_client (key rotated by LF)
56+
Akrites->>Vault: re-read private key
57+
Akrites->>Auth0: retry token exchange once
58+
```
59+
60+
## Affected Repositories
61+
62+
### `auth0-terraform`
63+
64+
Three changes. The existing `auth0_resource_server.cdp_public_api` (lines 400–494
65+
of `resource_servers.tf`) and `grants_cdp.tf` are the direct reference — Akrites
66+
gets its own isolated resource server, not a grant on the shared CDP one.
67+
68+
**`resource_servers.tf`** — add these two blocks:
69+
```hcl
70+
resource "auth0_resource_server" "cdp_akrites_api" {
71+
name = "CDP Akrites API"
72+
identifier = {
73+
"dev" = "{{AKRITES_CDP_AUDIENCE_DEV}}"
74+
"staging" = "{{AKRITES_CDP_AUDIENCE_STAGING}}"
75+
"prod" = "{{AKRITES_CDP_AUDIENCE_PROD}}"
76+
}[terraform.workspace]
77+
78+
signing_alg = "RS256"
79+
token_lifetime = {
80+
"dev" = 3600
81+
"staging" = 10800
82+
"prod" = 10800
83+
}[terraform.workspace]
84+
token_dialect = "access_token"
85+
allow_offline_access = false
86+
87+
subject_type_authorization {
88+
user { policy = "deny_all" }
89+
client { policy = "require_client_grant" }
90+
}
91+
}
92+
93+
resource "auth0_resource_server_scopes" "cdp_akrites_api" {
94+
resource_server_identifier = auth0_resource_server.cdp_akrites_api.identifier
95+
96+
scopes {
97+
name = "read:<resource>"
98+
description = "Read-only access to CDP data via the Akrites API"
99+
}
100+
}
101+
```
102+
103+
**`clients_m2m.tf`** — add one entry to `local.m2m_clients` (line ~10):
104+
```hcl
105+
"Akrites" = { oidc_conformant = true },
106+
```
107+
The existing `auth0_client.m2m_clients` `for_each` resource instantiates it
108+
automatically with `grant_types = ["client_credentials"]`. Auth method starts
109+
as `client_secret_post`; `lfx-secrets-management` rotation converts it to
110+
Private Key JWT — same as all other M2M clients.
111+
112+
**`grants_akrites.tf`** _(new file)_
113+
```hcl
114+
resource "auth0_client_grant" "akrites_cdp" {
115+
client_id = auth0_client.m2m_clients["Akrites"].id
116+
audience = auth0_resource_server.cdp_akrites_api.identifier
117+
scopes = ["read:<resource>"]
118+
depends_on = [auth0_resource_server_scopes.cdp_akrites_api]
119+
}
120+
```
121+
122+
---
123+
124+
### `lfx-secrets-management`
125+
126+
No structural changes. Add Akrites as a new entry in the sync config:
127+
128+
- **Source**: Auth0 — uses existing `Auth0JWTConfig` model
129+
(`secretsmanagement/services/auth0.py`)
130+
- **Destinations**:
131+
- AWS Secrets Manager (prod) — `secretsmanagement/services/aws.py` via `boto3`
132+
- 1Password (dev) — `secretsmanagement/services/onepassword.py` via `op` CLI
133+
- **Orchestration**: `secretsmanagement/sync.py` lines 37–96 — Akrites follows
134+
same source → destination pattern as all other M2M clients
135+
136+
CDP holds no private key. Token verification is JWKS-only.
137+
138+
---
139+
140+
### `crowd.dev` (CDP — this repo)
141+
142+
**`backend/config/custom-environment-variables.json`** (currently lines 161–166)
143+
144+
Add alongside the existing `auth0` block:
145+
```json
146+
"auth0Akrites": {
147+
"issuerBaseURLs": "CROWD_AUTH0_AKRITES_ISSUER_BASE_URLS",
148+
"audience": "CROWD_AUTH0_AKRITES_AUDIENCE",
149+
"clientId": "CROWD_AUTH0_AKRITES_CLIENT_ID"
150+
}
151+
```
152+
153+
**`backend/src/conf/index.ts`** (line 106)
154+
155+
Add:
156+
```ts
157+
export const AKRITES_AUTH0_CONFIG: Auth0Configuration = config.get<Auth0Configuration>('auth0Akrites')
158+
```
159+
`Auth0Configuration` interface (`backend/src/conf/configTypes.ts` lines 68–73) is reused as-is.
160+
161+
**`backend/src/security/scopes.ts`**
162+
163+
Add to `SCOPES` const:
164+
```ts
165+
READ_RESOURCE: 'read:<resource>',
166+
```
167+
168+
**`backend/src/api/public/middlewares/azpAllowlistMiddleware.ts`** _(new file)_
169+
170+
Reads `req.auth.payload.azp`. Throws `UnauthorizedError` if not in the
171+
configured allowlist. Fails closed — any unknown client ID is rejected even if
172+
Auth0 is misconfigured to grant it the same audience.
173+
174+
**`backend/src/api/public/v1/index.ts`** (line 44)
175+
176+
Replace:
177+
```ts
178+
router.use('/akrites', oauth2Middleware(AUTH0_CONFIG), akritesRouter())
179+
```
180+
With:
181+
```ts
182+
router.use(
183+
'/akrites',
184+
oauth2Middleware(AKRITES_AUTH0_CONFIG),
185+
azpAllowlistMiddleware([AKRITES_AUTH0_CONFIG.clientId]),
186+
requireScopes([SCOPES.READ_AKRITES], 'all'),
187+
akritesRouter(),
188+
)
189+
```
190+
191+
---
192+
193+
### Akrites (external repo)
194+
195+
Implement the token exchange described in the Auth Flow diagram above:
196+
197+
1. Read RSA private key from vault (AWS Secrets Manager in prod via
198+
`lfx-secrets-management`, 1Password in dev).
199+
2. Build and sign `client_assertion` JWT (RS256).
200+
3. POST to Auth0 `/oauth/token` — cache the returned Bearer token until it
201+
expires.
202+
4. Attach Bearer token to all CDP requests via `Authorization` header.
203+
5. On `invalid_client`: discard cached key → re-read from vault → retry token
204+
exchange once. LF rotates the keypair without notice.

docs/adr/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Use the `/adr` skill in Claude Code to record new ADRs or query past decisions.
1515
| [ADR-0006](./0006-database-schema-types-as-source-of-truth.md) | Database schema types as the source of truth | accepted | 2026-07-09 |
1616
| [ADR-0007](./0007-test-factory-primitives-and-defaults.md) | Test factory primitives and defaults | accepted | 2026-07-10 |
1717
| [ADR-0008](./0008-how-we-write-unit-tests.md) | How we write unit tests | accepted | 2026-07-13 |
18+
| [ADR-0009](./0009-akrites-cdp-public-api-authentication.md) | Akrites → CDP public API authentication | proposed | 2026-07-14 |
1819

1920
## Why ADRs?
2021

0 commit comments

Comments
 (0)