Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/developers/fragments/_token-exchange-prerequisites.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:::info[Prerequisites]
Before using the token exchange grant, you need to enable it for your application:

1. Go to <CloudLink to="/applications">Console > Applications</CloudLink> and select your application.
2. In the application settings, find the "Token exchange" section.
3. Enable the "Allow token exchange" toggle.

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.
:::
86 changes: 72 additions & 14 deletions docs/developers/user-impersonation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ sidebar_label: User impersonation
sidebar_position: 3
---

import TokenExchangePrerequisites from './fragments/_token-exchange-prerequisites.mdx';

# User impersonation

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.
Expand Down Expand Up @@ -109,16 +111,37 @@ TechCorp's server should then return this subject token to Sarah's application.

### Step 3: Exchanging the subject token for an access token \{#step-3-exchanging-the-subject-token-for-an-access-token}

<TokenExchangePrerequisites />

Now, Sarah's application exchanges this subject token for an access token representing Alex, specifying the resource where the token will be used.

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

For traditional web applications or machine-to-machine applications with app secret, include the credentials in the `Authorization` header:

```bash
POST /oidc/token HTTP/1.1
Host: techcorp.logto.app
Content-Type: application/x-www-form-urlencoded
# highlight-next-line
Authorization: Basic <base64(client_id:client_secret)>

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&scope=resource:read
&subject_token=alx_7h32jf8sK3j2
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&resource=https://api.techcorp.com/customer-data
```

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

```bash
POST /oidc/token HTTP/1.1
Host: techcorp.logto.app
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
# highlight-next-line
&client_id=techcorp_support_app
&scope=resource:read
&subject_token=alx_7h32jf8sK3j2
Expand All @@ -140,19 +163,11 @@ grant_type=urn:ietf:params:oauth:grant-type:token-exchange

The `access_token` returned will be bound to the specified resource, ensuring it can only be used with TechCorp's customer data API.

**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.

Here's a Node.js example:

```json
Authorization: `Basic ${Buffer.from(`${client_id}:${client_secret}`, 'utf8').toString('base64')}`
```

## Example usage \{#example-usage}

Here's how Sarah might use this in a Node.js support application:

```jsx
```tsx
interface ImpersonationResponse {
subjectToken: string;
expiresIn: number;
Expand All @@ -170,7 +185,9 @@ async function impersonateUser(
userId: string,
clientId: string,
ticketId: string,
resource: string
resource: string,
// highlight-next-line
clientSecret?: string // Required for traditional web or machine-to-machine apps
): Promise<string> {
try {
// Step 1 & 2: Request impersonation and get subject token
Expand All @@ -197,18 +214,34 @@ async function impersonateUser(
const { subjectToken } = (await impersonationResponse.json()) as ImpersonationResponse;

// Step 3: Exchange subject token for access token
// highlight-start
// For traditional web or M2M apps, use Basic auth with client secret
// For SPA or native apps, include client_id in the request body
const headers: Record<string, string> = {
'Content-Type': 'application/x-www-form-urlencoded',
};

const tokenExchangeBody = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
client_id: clientId,
scope: 'openid profile resource.read',
subject_token: subjectToken,
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
resource: resource,
});

if (clientSecret) {
// Confidential client: use Basic auth
headers['Authorization'] =
`Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
} else {
// Public client: include client_id in body
tokenExchangeBody.append('client_id', clientId);
}
// highlight-end

const tokenExchangeResponse = await fetch('https://techcorp.logto.app/oidc/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers,
body: tokenExchangeBody,
});

Expand All @@ -227,20 +260,24 @@ async function impersonateUser(
// Sarah uses this function to impersonate Alex
async function performImpersonation(): Promise<void> {
try {
// highlight-start
// For traditional web or M2M apps, pass the client secret
const accessToken = await impersonateUser(
'alex123',
'techcorp_support_app',
'TECH-1234',
'https://api.techcorp.com/customer-data'
'https://api.techcorp.com/customer-data',
'your-client-secret' // Omit this for SPA or native apps
);
// highlight-end
console.log('Impersonation access token for Alex:', accessToken);
} catch (error) {
console.error('Failed to perform impersonation:', error);
}
}

// Execute the impersonation
void performImpersonation()
void performImpersonation();
```

:::note
Expand All @@ -257,12 +294,33 @@ When using the token exchange flow for impersonation, the issued access token ca

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:

For traditional web applications or machine-to-machine applications:

```bash
POST /oidc/token HTTP/1.1
Host: techcorp.logto.app
Content-Type: application/x-www-form-urlencoded
# highlight-next-line
Authorization: Basic <base64(client_id:client_secret)>

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&scope=resource:read
&subject_token=alx_7h32jf8sK3j2
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=sarah_access_token
&actor_token_type=urn:ietf:params:oauth:token-type:access_token
&resource=https://api.techcorp.com/customer-data
```

For SPA or native applications, include `client_id` in the request body instead:

```bash
POST /oidc/token HTTP/1.1
Host: techcorp.logto.app
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
# highlight-next-line
&client_id=techcorp_support_app
&scope=resource:read
&subject_token=alx_7h32jf8sK3j2
Expand Down
24 changes: 16 additions & 8 deletions docs/user-management/personal-access-token.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
sidebar_position: 4
---

import TokenExchangePrerequisites from '../developers/fragments/_token-exchange-prerequisites.mdx';

# Personal access token

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.
Expand Down Expand Up @@ -34,6 +36,8 @@ If you're working with organizations, the access patterns and permissions are th

### Request \{#request}

<TokenExchangePrerequisites />

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.

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

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

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

```
```bash
POST /oidc/token HTTP/1.1
Host: tenant.logto.app
Content-Type: application/x-www-form-urlencoded
# highlight-next-line
Authorization: Basic <base64(app-id:app-secret)>

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&resource=http://my-api.com
&scope=read
&subject_token=pat_W51arOqe7nynW75nWhvYogyc
&subject_token_type=urn%3Alogto%3Atoken-type%3Apersonal_access_token
&subject_token_type=urn:logto:token-type:personal_access_token
```

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

```
```bash
POST /oidc/token HTTP/1.1
Host: tenant.logto.app
Content-Type: application/x-www-form-urlencoded

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

A successful response:

```
HTTP/1.1 200 OK
Content-Type: application/json
Expand Down
Loading