Skip to content

Commit 4c76f45

Browse files
authored
docs: add token exchange prerequisites and improve examples (#1335)
1 parent 901cc1f commit 4c76f45

File tree

3 files changed

+97
-22
lines changed

3 files changed

+97
-22
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
:::info[Prerequisites]
2+
Before using the token exchange grant, you need to enable it for your application:
3+
4+
1. Go to <CloudLink to="/applications">Console > Applications</CloudLink> and select your application.
5+
2. In the application settings, find the "Token exchange" section.
6+
3. Enable the "Allow token exchange" toggle.
7+
8+
Token exchange is disabled by default for security reasons. If you don't enable it, you will receive a "token exchange is not allowed for this application" error.
9+
:::

docs/developers/user-impersonation.mdx

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ sidebar_label: User impersonation
55
sidebar_position: 3
66
---
77

8+
import TokenExchangePrerequisites from './fragments/_token-exchange-prerequisites.mdx';
9+
810
# User impersonation
911

1012
Imagine Sarah, a support engineer at TechCorp, receives an urgent ticket from Alex, a customer who can't access a critical resource. To efficiently diagnose and resolve the issue, Sarah needs to see exactly what Alex sees in the system. This is where Logto's user impersonation feature comes in handy.
@@ -109,16 +111,37 @@ TechCorp's server should then return this subject token to Sarah's application.
109111
110112
### Step 3: Exchanging the subject token for an access token \{#step-3-exchanging-the-subject-token-for-an-access-token}
111113
114+
<TokenExchangePrerequisites />
115+
112116
Now, Sarah's application exchanges this subject token for an access token representing Alex, specifying the resource where the token will be used.
113117

114118
**Request (Sarah's application to Logto's token endpoint)**
115119

120+
For traditional web applications or machine-to-machine applications with app secret, include the credentials in the `Authorization` header:
121+
116122
```bash
117123
POST /oidc/token HTTP/1.1
118124
Host: techcorp.logto.app
119125
Content-Type: application/x-www-form-urlencoded
126+
# highlight-next-line
127+
Authorization: Basic <base64(client_id:client_secret)>
120128
121129
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
130+
&scope=resource:read
131+
&subject_token=alx_7h32jf8sK3j2
132+
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
133+
&resource=https://api.techcorp.com/customer-data
134+
```
135+
136+
For single-page applications (SPA) or native applications without app secret, include `client_id` in the request body:
137+
138+
```bash
139+
POST /oidc/token HTTP/1.1
140+
Host: techcorp.logto.app
141+
Content-Type: application/x-www-form-urlencoded
142+
143+
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
144+
# highlight-next-line
122145
&client_id=techcorp_support_app
123146
&scope=resource:read
124147
&subject_token=alx_7h32jf8sK3j2
@@ -140,19 +163,11 @@ grant_type=urn:ietf:params:oauth:grant-type:token-exchange
140163
141164
The `access_token` returned will be bound to the specified resource, ensuring it can only be used with TechCorp's customer data API.
142165

143-
**Note**: For traditional web applications, include `client_id` and `client_secret` in the header of the token request to prevent a 401 invalid_client error.
144-
145-
Here's a Node.js example:
146-
147-
```json
148-
Authorization: `Basic ${Buffer.from(`${client_id}:${client_secret}`, 'utf8').toString('base64')}`
149-
```
150-
151166
## Example usage \{#example-usage}
152167

153168
Here's how Sarah might use this in a Node.js support application:
154169
155-
```jsx
170+
```tsx
156171
interface ImpersonationResponse {
157172
subjectToken: string;
158173
expiresIn: number;
@@ -170,7 +185,9 @@ async function impersonateUser(
170185
userId: string,
171186
clientId: string,
172187
ticketId: string,
173-
resource: string
188+
resource: string,
189+
// highlight-next-line
190+
clientSecret?: string // Required for traditional web or machine-to-machine apps
174191
): Promise<string> {
175192
try {
176193
// Step 1 & 2: Request impersonation and get subject token
@@ -197,18 +214,34 @@ async function impersonateUser(
197214
const { subjectToken } = (await impersonationResponse.json()) as ImpersonationResponse;
198215
199216
// Step 3: Exchange subject token for access token
217+
// highlight-start
218+
// For traditional web or M2M apps, use Basic auth with client secret
219+
// For SPA or native apps, include client_id in the request body
220+
const headers: Record<string, string> = {
221+
'Content-Type': 'application/x-www-form-urlencoded',
222+
};
223+
200224
const tokenExchangeBody = new URLSearchParams({
201225
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
202-
client_id: clientId,
203226
scope: 'openid profile resource.read',
204227
subject_token: subjectToken,
205228
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
206229
resource: resource,
207230
});
208231
232+
if (clientSecret) {
233+
// Confidential client: use Basic auth
234+
headers['Authorization'] =
235+
`Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
236+
} else {
237+
// Public client: include client_id in body
238+
tokenExchangeBody.append('client_id', clientId);
239+
}
240+
// highlight-end
241+
209242
const tokenExchangeResponse = await fetch('https://techcorp.logto.app/oidc/token', {
210243
method: 'POST',
211-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
244+
headers,
212245
body: tokenExchangeBody,
213246
});
214247
@@ -227,20 +260,24 @@ async function impersonateUser(
227260
// Sarah uses this function to impersonate Alex
228261
async function performImpersonation(): Promise<void> {
229262
try {
263+
// highlight-start
264+
// For traditional web or M2M apps, pass the client secret
230265
const accessToken = await impersonateUser(
231266
'alex123',
232267
'techcorp_support_app',
233268
'TECH-1234',
234-
'https://api.techcorp.com/customer-data'
269+
'https://api.techcorp.com/customer-data',
270+
'your-client-secret' // Omit this for SPA or native apps
235271
);
272+
// highlight-end
236273
console.log('Impersonation access token for Alex:', accessToken);
237274
} catch (error) {
238275
console.error('Failed to perform impersonation:', error);
239276
}
240277
}
241278
242279
// Execute the impersonation
243-
void performImpersonation()
280+
void performImpersonation();
244281
```
245282
246283
:::note
@@ -257,12 +294,33 @@ When using the token exchange flow for impersonation, the issued access token ca
257294
258295
To include the `act` claim, Sarah's application needs to provide an `actor_token` in the token exchange request. This token should be a valid access token for Sarah with the `openid` scope. Here's how to include it in the token exchange request:
259296
297+
For traditional web applications or machine-to-machine applications:
298+
299+
```bash
300+
POST /oidc/token HTTP/1.1
301+
Host: techcorp.logto.app
302+
Content-Type: application/x-www-form-urlencoded
303+
# highlight-next-line
304+
Authorization: Basic <base64(client_id:client_secret)>
305+
306+
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
307+
&scope=resource:read
308+
&subject_token=alx_7h32jf8sK3j2
309+
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
310+
&actor_token=sarah_access_token
311+
&actor_token_type=urn:ietf:params:oauth:token-type:access_token
312+
&resource=https://api.techcorp.com/customer-data
313+
```
314+
315+
For SPA or native applications, include `client_id` in the request body instead:
316+
260317
```bash
261318
POST /oidc/token HTTP/1.1
262319
Host: techcorp.logto.app
263320
Content-Type: application/x-www-form-urlencoded
264321
265322
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
323+
# highlight-next-line
266324
&client_id=techcorp_support_app
267325
&scope=resource:read
268326
&subject_token=alx_7h32jf8sK3j2

docs/user-management/personal-access-token.mdx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
sidebar_position: 4
33
---
44

5+
import TokenExchangePrerequisites from '../developers/fragments/_token-exchange-prerequisites.mdx';
6+
57
# Personal access token
68

79
Personal access tokens (PATs) provide a secure way for users to grant [access token](https://auth.wiki/access-token) without using their credentials and interactive sign-in. This is useful for CI/CD, scripts, or applications that need to access resources programmatically.
@@ -34,6 +36,8 @@ If you're working with organizations, the access patterns and permissions are th
3436

3537
### Request \{#request}
3638

39+
<TokenExchangePrerequisites />
40+
3741
The application makes a [token exchange request](https://auth.wiki/authorization-code-flow#token-exchange-request) to the tenant's [token endpoint](/integrate-logto/application-data-structure#token-endpoint) with a special grant type using the HTTP POST method. The following parameters are included in the HTTP request entity-body using the `application/x-www-form-urlencoded` format.
3842

3943
1. `client_id`: REQUIRED. The client ID of the application.
@@ -55,36 +59,40 @@ If the token exchange request is successful, the tenant's token endpoint returns
5559

5660
### Example token exchange \{#example-token-exchange}
5761

58-
For traditional web applications with app secret:
62+
For traditional web applications or machine-to-machine applications with app secret, include the credentials in the `Authorization` header using HTTP Basic authentication:
5963

60-
```
64+
```bash
6165
POST /oidc/token HTTP/1.1
6266
Host: tenant.logto.app
6367
Content-Type: application/x-www-form-urlencoded
68+
# highlight-next-line
6469
Authorization: Basic <base64(app-id:app-secret)>
6570

66-
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
71+
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
6772
&resource=http://my-api.com
6873
&scope=read
6974
&subject_token=pat_W51arOqe7nynW75nWhvYogyc
70-
&subject_token_type=urn%3Alogto%3Atoken-type%3Apersonal_access_token
75+
&subject_token_type=urn:logto:token-type:personal_access_token
7176
```
7277

73-
For single-page or native applications without app secret:
78+
For single-page applications (SPA) or native applications without app secret, include `client_id` in the request body:
7479

75-
```
80+
```bash
7681
POST /oidc/token HTTP/1.1
7782
Host: tenant.logto.app
7883
Content-Type: application/x-www-form-urlencoded
7984

85+
# highlight-next-line
8086
client_id=your-app-id
81-
&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
87+
&grant_type=urn:ietf:params:oauth:grant-type:token-exchange
8288
&resource=http://my-api.com
8389
&scope=read
8490
&subject_token=pat_W51arOqe7nynW75nWhvYogyc
85-
&subject_token_type=urn%3Alogto%3Atoken-type%3Apersonal_access_token
91+
&subject_token_type=urn:logto:token-type:personal_access_token
8692
```
8793

94+
A successful response:
95+
8896
```
8997
HTTP/1.1 200 OK
9098
Content-Type: application/json

0 commit comments

Comments
 (0)