Skip to content

Commit 098f77f

Browse files
committed
Deploy gds-draft docs: 78e650e944a2690c60b9fc64310c4ca5ea99828d
1 parent 240533c commit 098f77f

4 files changed

Lines changed: 91 additions & 18 deletions

File tree

gds-draft/approval-workflow.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ Response:
9494

9595
Gather the following before creating an approval request:
9696

97+
For GDS, EUID is the chosen identifier format. Use EUID consistently for `organizationId`, `issuerId`, `subjectId`, and `serviceProvider`.
98+
9799
### Requester information (your platform's user)
98100

99101
| Field | Description | Example |

gds-draft/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ sequenceDiagram
145145
participant AR as Authorization Registry
146146
147147
David->>Charlie: GET /buildings/{vboId} + Bearer token
148-
Charlie->>Charlie: Extract organization from token
148+
Charlie->>Charlie: Derive the organization EUID from token claim
149149
Charlie->>AR: GET /api/authorization/explained-enforce
150150
AR-->>Charlie: {allowed: true/false, policies: [...]}
151151
alt Allowed

gds-draft/authorization.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ views {
2929
variant sequence
3030
3131
david -> charlie 'GET /buildings/{vboId} + Bearer token'
32-
charlie -> charlie 'Validate token & extract organization'
32+
charlie -> charlie 'Validate token & derive organization EUID'
3333
charlie -> ar 'GET /api/authorization/explained-enforce'
3434
ar -> charlie 'HTTP 200: {allowed: true/false, policies}'
3535
charlie -> david 'If allowed: 200 + data / If denied: 403'
@@ -41,6 +41,8 @@ views {
4141

4242
When a building owner (Bob) approves an access request through Keyper, policies are registered in the GDS Authorization Registry. Each policy specifies:
4343

44+
For GDS, EUID is the chosen identifier format for policy identities. This means `issuerId`, `subjectId`, and `serviceProvider` are EUID values.
45+
4446
| Field | Description | Example |
4547
|-------|-------------|---------|
4648
| `issuerId` | Building owner who granted access | `NLNHR.87654321` |
@@ -76,9 +78,9 @@ GET https://gds-preview.poort8.nl/api/authorization/explained-enforce?issuer={BU
7678

7779
| Parameter | Description | Example |
7880
|-----------|-------------|---------|
79-
| `issuer` | Building owner who granted access (Bob) | `NLNHR.87654321` |
80-
| `subject` | Organization requesting data (David) | `NLNHR.12345678` |
81-
| `serviceProvider` | Your organization (Charlie) | `NLNHR.23456789` |
81+
| `issuer` | Building owner who granted access (Bob), as EUID | `NLNHR.87654321` |
82+
| `subject` | Organization requesting data (David), as EUID | `NLNHR.12345678` |
83+
| `serviceProvider` | Your organization (Charlie), as EUID | `NLNHR.23456789` |
8284
| `action` | Requested action | `GET` |
8385
| `resource` | Resource identifier (VBO ID or asset ID) | `0363010000659001` |
8486
| `type` | Resource type | `building` |
@@ -134,7 +136,7 @@ The endpoint always returns HTTP 200 — even when authorization is denied. When
134136
| Check | Requirement |
135137
|-------|-------------|
136138
| **Allowed** | `allowed` must be `true` |
137-
| **Subject match** | `explainPolicies[].subjectId` must match the organization extracted from the incoming request's bearer token |
139+
| **Subject match** | `explainPolicies[].subjectId` must match the EUID derived from the incoming request's `organization` claim |
138140

139141
### Recommended responses to your consumers
140142

@@ -155,7 +157,7 @@ Putting it all together — a simplified enforcement flow:
155157
```
156158
1. Receive request with Bearer token and resource identifier
157159
2. Validate token (signature, expiry, issuer, audience)
158-
3. Extract organization EUID from token's `organization` claim
160+
3. Derive the organization EUID from the token's `organization` claim
159161
4. Determine the building owner (issuer) for the requested resource
160162
5. Call explained-enforce (no auth required) with subject=consumer, serviceProvider=you, resource=vboId
161163
6. Enforce always returns HTTP 200 — check the `allowed` field

gds-draft/validating-api-tokens.md

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ views {
4141
4242
david -> api 'API call + Authorization: Bearer {token}'
4343
api -> api 'Verify signature, expiry, issuer, audience'
44-
api -> api 'Extract organization claim'
44+
api -> api 'Derive the organization EUID from organization claim'
4545
api -> david 'API response'
4646
}
4747
}
@@ -57,7 +57,7 @@ Perform these checks in order. Reject the request immediately if any check fails
5757
| 2 | **Expiration** | `exp` claim is in the future | `401 Unauthorized` |
5858
| 3 | **Issuer** | `iss` equals `https://auth.poort8.nl/realms/gds-preview` | `401 Unauthorized` |
5959
| 4 | **Audience** | `aud` contains your API's client ID | `403 Forbidden` |
60-
| 5 | **Organization** | `organization` claim is present | Use for business logic |
60+
| 5 | **Organization** | `organization` claim is present and contains an `EUID` value | Use for business logic |
6161

6262
> **Step 4 is critical.** Without audience validation, a token intended for a different API could be used to access yours. Always verify that your API's client ID appears in the `aud` claim.
6363
@@ -87,6 +87,15 @@ A decoded access token from a GDS consumer:
8787
"client_id": "CONSUMER_APP_CLIENT_ID",
8888
"organization": {
8989
"NLNHR.12345678": {
90+
"KVK": [
91+
"12345678"
92+
],
93+
"EORI": [
94+
"NL123456789"
95+
],
96+
"EUID": [
97+
"NLNHR.12345678"
98+
],
9099
"id": "550e8400-e29b-41d4-a716-446655440000"
91100
}
92101
}
@@ -102,23 +111,52 @@ A decoded access token from a GDS consumer:
102111
| `iat` | number | Issued-at time (Unix timestamp) |
103112
| `scope` | string | Space-separated granted scopes |
104113
| `client_id` | string | Consumer application's client ID |
105-
| `organization` | object | Consumer's verified organization identity |
114+
| `organization` | object | Consumer's verified organization identity with one or more identifier types |
106115

107-
## Organization claim
116+
## Deriving the organization identifier
108117

109-
The `organization` claim identifies the consumer's organization:
118+
After successful token validation, derive the caller organization identifier from the `organization` claim.
119+
120+
The `organization` claim is a Keycloak-specific JSON structure. For each organization it can contain multiple identifier types represented as arrays. The chosen identifier in GDS is EUID.
121+
122+
In GDS, EUID is the canonical identifier for authorization. Use the derived EUID consistently as:
123+
124+
- `subject` in explained-enforce requests
125+
- `organizationId` in approval request payloads
126+
- `subjectId`, `issuerId`, and `serviceProvider` values in policy payloads
127+
128+
Use this algorithm:
129+
130+
1. Iterate through organizations in the `organization` object.
131+
2. For each organization, check whether the EUID attribute exists and has a non-empty array.
132+
3. Take the first array item as the caller organization identifier.
133+
134+
Example structure:
110135

111136
```json
112137
{
113138
"organization": {
114139
"NLNHR.12345678": {
140+
"KVK": [
141+
"12345678"
142+
],
143+
"EORI": [
144+
"NL123456789"
145+
],
146+
"EUID": [
147+
"NLNHR.12345678"
148+
],
115149
"id": "550e8400-e29b-41d4-a716-446655440000"
116150
}
117151
}
118152
}
119153
```
120154

121-
The key (e.g., `NLNHR.12345678`) is the organization's **EUID** — derived from the official KvK registration number and verified during onboarding. This is not a self-declared value.
155+
Reject the request with `403 Forbidden` when:
156+
157+
- The `organization` claim is missing
158+
- The `organization` claim is not a valid JSON object
159+
- The `EUID` attribute is missing or empty for all organizations
122160

123161
Use this to:
124162
- Identify which organization is calling your API
@@ -144,9 +182,23 @@ const jwtCheck = auth({
144182
app.use(jwtCheck);
145183

146184
app.get("/data", (req, res) => {
147-
const organization = req.auth.payload.organization;
148-
const orgId = Object.keys(organization)[0]; // e.g., "NLNHR.12345678"
149-
res.json({ message: `Data for ${orgId}` });
185+
const organizationClaim = req.auth.payload.organization;
186+
const identifierAttribute = "EUID";
187+
188+
let organizationIdentifier = null;
189+
for (const org of Object.values(organizationClaim || {})) {
190+
const identifiers = org[identifierAttribute];
191+
if (Array.isArray(identifiers) && identifiers.length > 0) {
192+
organizationIdentifier = identifiers[0];
193+
break;
194+
}
195+
}
196+
197+
if (!organizationIdentifier) {
198+
return res.status(403).json({ error: "organization identifier not found" });
199+
}
200+
201+
res.json({ message: `Data for ${organizationIdentifier}` });
150202
});
151203

152204
app.listen(3000);
@@ -173,8 +225,25 @@ app.UseAuthorization();
173225

174226
app.MapGet("/data", (HttpContext ctx) =>
175227
{
176-
var organization = ctx.User.FindFirst("organization")?.Value;
177-
return Results.Ok(new { message = $"Data for {organization}" });
228+
var organizationClaim = ctx.User.FindFirst("organization")?.Value;
229+
var identifierAttribute = "EUID";
230+
231+
if (string.IsNullOrWhiteSpace(organizationClaim))
232+
return Results.Forbid();
233+
234+
using var document = JsonDocument.Parse(organizationClaim);
235+
foreach (var organization in document.RootElement.EnumerateObject())
236+
{
237+
if (organization.Value.TryGetProperty(identifierAttribute, out var identifiers)
238+
&& identifiers.ValueKind == JsonValueKind.Array
239+
&& identifiers.GetArrayLength() > 0)
240+
{
241+
var identifier = identifiers[0].GetString();
242+
return Results.Ok(new { message = $"Data for {identifier}" });
243+
}
244+
}
245+
246+
return Results.Forbid();
178247
}).RequireAuthorization();
179248

180249
app.Run();

0 commit comments

Comments
 (0)