Skip to content

Commit 4f82c03

Browse files
committed
docs: Updated docs for the token exchange endpoint
1 parent 3357a5b commit 4f82c03

3 files changed

Lines changed: 78 additions & 10 deletions

File tree

docs/auth.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,73 @@ Content-Type: application/json
10491049
}
10501050
```
10511051

1052+
### POST /auth/token_exchange
1053+
1054+
This endpoint exchanges an external OIDC `id_token` for a normal Cozy OAuth
1055+
client and token pair on the target instance.
1056+
1057+
It is intended for browser-based admin applications that authenticate with an
1058+
external identity provider, then need to call Cozy APIs directly on an
1059+
organization instance.
1060+
1061+
The target Cozy instance is the request host. The exchanged `id_token` must:
1062+
1063+
- be signed by the configured OIDC provider
1064+
- match the configured issuer and audience
1065+
- contain an `org_id` claim equal to the target instance organization id
1066+
- contain an `org_role` claim equal to `owner` or `admin`
1067+
1068+
The request body is JSON:
1069+
1070+
- `id_token`, the external OIDC token
1071+
- `scope`, currently limited to `io.cozy.files`
1072+
1073+
Example:
1074+
1075+
```http
1076+
POST /auth/token_exchange HTTP/1.1
1077+
Host: myorg123.example.com
1078+
Content-Type: application/json
1079+
Accept: application/json
1080+
1081+
{
1082+
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InRva2VuLWV4Y2hhbmdlIn0...",
1083+
"scope": "io.cozy.files"
1084+
}
1085+
```
1086+
1087+
Response:
1088+
1089+
```http
1090+
HTTP/1.1 200 OK
1091+
Content-Type: application/json
1092+
Cache-Control: no-store
1093+
Pragma: no-cache
1094+
```
1095+
1096+
```json
1097+
{
1098+
"access_token": "eyJhbGciOiJS",
1099+
"token_type": "bearer",
1100+
"refresh_token": "eyJhbGciOiJS",
1101+
"scope": "io.cozy.files",
1102+
"client_id": "64ce5cb0-bd4c-11e6-880e-b3b7dfda89d3",
1103+
"client_secret": "Oung7oi5",
1104+
"registration_access_token": "reg123"
1105+
}
1106+
```
1107+
1108+
The returned OAuth client is a normal Cozy OAuth client:
1109+
1110+
- `client_id`, `client_secret`, `access_token`, and `refresh_token` can be
1111+
used directly with `cozy-client`
1112+
- `registration_access_token` can be used with
1113+
`DELETE /auth/register/:client-id` to revoke that exchanged client
1114+
1115+
When the external `id_token` contains a `sid` claim, the created OAuth client
1116+
is bound to that upstream OIDC session so it can be revoked by OIDC
1117+
backchannel logout.
1118+
10521119
### POST /auth/session_code
10531120

10541121
This endpoint can be used by the flagship application in order to create a

web/middlewares/secure.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ func (b cspBuilder) makeCSPHeader(header, cspAllowList string, sources []CSPSour
310310
_, domain, found := strings.Cut(b.instance.Domain, ".")
311311
if found {
312312
headers = append(headers, "api-login-"+b.instance.OrgID+"."+domain)
313+
headers = append(headers, b.instance.OrgID+"."+domain)
313314
}
314315
if b.instance.OrgDomain != "" {
315316
headers = append(headers, b.instance.OrgID+"."+b.instance.OrgDomain)

web/middlewares/secure_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,8 @@ func TestSecure(t *testing.T) {
221221

222222
csp := rec.Header().Get(echo.HeaderContentSecurityPolicy)
223223

224-
// Verify that api-login-myorg123.cozy.example.com appears only once (in connect-src)
225-
expectedDomain := "api-login-myorg123.cozy.example.com"
226-
count := strings.Count(csp, expectedDomain)
227-
assert.Equal(t, 1, count,
228-
"%s should appear exactly once (in connect-src), but found %d times. CSP: %s",
229-
expectedDomain, count, csp)
224+
apiLoginDomain := "api-login-myorg123.cozy.example.com"
225+
orgInstanceDomain := "myorg123.cozy.example.com"
230226

231227
// Verify that connect-src contains the api-login domain
232228
connectSrcIndex := strings.Index(csp, "connect-src ")
@@ -238,8 +234,10 @@ func TestSecure(t *testing.T) {
238234
"connect-src should end with semicolon")
239235

240236
connectSrcContent := csp[connectSrcIndex : connectSrcIndex+connectSrcEnd]
241-
assert.Contains(t, connectSrcContent, expectedDomain,
242-
"connect-src should contain %s. Found: %s", expectedDomain, connectSrcContent)
237+
assert.Contains(t, connectSrcContent, apiLoginDomain,
238+
"connect-src should contain %s. Found: %s", apiLoginDomain, connectSrcContent)
239+
assert.Contains(t, connectSrcContent, orgInstanceDomain,
240+
"connect-src should contain %s. Found: %s", orgInstanceDomain, connectSrcContent)
243241

244242
// Verify that other directives do NOT contain the api-login domain
245243
otherDirectives := []string{
@@ -264,8 +262,10 @@ func TestSecure(t *testing.T) {
264262
directiveEnd := strings.Index(csp[directiveIndex:], ";")
265263
if directiveEnd != -1 {
266264
directiveContent := csp[directiveIndex : directiveIndex+directiveEnd]
267-
assert.NotContains(t, directiveContent, expectedDomain,
268-
"Directive %s should NOT contain %s. Found: %s", directivePattern, expectedDomain, directiveContent)
265+
assert.NotContains(t, directiveContent, apiLoginDomain,
266+
"Directive %s should NOT contain %s. Found: %s", directivePattern, apiLoginDomain, directiveContent)
267+
assert.NotContains(t, directiveContent, orgInstanceDomain,
268+
"Directive %s should NOT contain %s. Found: %s", directivePattern, orgInstanceDomain, directiveContent)
269269
}
270270
}
271271
}

0 commit comments

Comments
 (0)