Skip to content

Device authorization flow #515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
225 changes: 225 additions & 0 deletions src/content/docs/authenticate/device-authorization-flow/api-calls.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
---
page_id: 8d8234a5-b064-47a1-a1e1-9b3f98a57f34
title: Call your API using device authorization flow
sidebar:
order: 3
relatedArticles:
- 2944d2bc-4e84-4918-b4f6-7406a7c26f98
- 888b1546-8047-4609-af59-8cf859527aa0
- de937e16-8094-4aad-ada9-e6a37d74f508
- 1cbd91d2-c0b3-45b3-b038-319de1b2c794
---

Once you've received an access token from the Device Authorization Flow, you can use it to call your protected APIs. This guide shows you how to validate tokens, handle scopes, and make authenticated API requests.

## Using the access token

The access token you receive from Device Authorization Flow is a standard OAuth 2.0 Bearer token. Include it in the `Authorization` header of your API requests:

```bash
curl -X GET https://your-api.com/protected-resource \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

## Token validation

Before processing API requests, validate the access token to ensure it's valid and hasn't expired:

### Validate with Kinde's userinfo endpoint

```bash
curl -X GET https://<your-subdomain>.kinde.com/oauth2/v2/user_profile \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

**Success response**:

```json
{
"sub": "kp_c3143a4b50ad43c88e541d9077681782",
"provided_id": "some_external_id",
"name": "John Snow",
"given_name": "John",
"family_name": "Snow",
"updated_at": 1612345678,
"email": "[email protected]",
"email_verified": true,
"picture": "https://example.com/john_snow.jpg",
"preferred_username": "john_snow",
"id": "kp_c3143a4b50ad43c88e541d9077681782"
}
```

**Error response** (invalid token):

```json
{
"error": "invalid_token",
"error_description": "The access token is invalid or expired"
}
```

### Validate with your own API

You can also validate tokens in your own API by verifying the JWT signature and claims:

```javascript
// Node.js example using jsonwebtoken
const jwt = require("jsonwebtoken");

function validateToken(token) {
try {
const decoded = jwt.verify(token, "YOUR_JWT_SECRET");
return {
valid: true,
user: decoded
};
} catch (error) {
return {
valid: false,
error: error.message
};
}
}
```

## Scope enforcement

Access tokens include scopes that determine what resources the user can access. Check the required scopes before processing requests:

```javascript
// Example: Check if user has required scope
function hasRequiredScope(token, requiredScope) {
const decoded = jwt.decode(token);
const tokenScopes = decoded.scope.split(" ");
return tokenScopes.includes(requiredScope);
}

// Usage
if (!hasRequiredScope(accessToken, "read:users")) {
return res.status(403).json({error: "Insufficient scope"});
}
```

## Common API patterns

### Protected resource endpoint

```javascript
// Express.js example
app.get("/api/protected-resource", authenticateToken, (req, res) => {
// req.user contains the decoded token payload
res.json({
message: "Access granted",
user: req.user
});
});

function authenticateToken(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];

if (!token) {
return res.status(401).json({error: "Access token required"});
}

// Validate token with Kinde
fetch("https://<your-subdomain>.kinde.com/oauth2/v2/user_profile", {
headers: {
Authorization: `Bearer ${token}`
}
})
.then((response) => {
if (!response.ok) {
throw new Error("Invalid token");
}
return response.json();
})
.then((user) => {
req.user = user;
next();
})
.catch((error) => {
return res.status(401).json({error: "Invalid token"});
});
}
Comment on lines +127 to +154
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prefer local JWT validation in middleware; avoid per-request network hop to user_profile

Round-tripping to Kinde for every request adds latency and a failure mode. Since a valid JWT already proves authenticity, validate locally and attach claims to req.user. If you keep the fetch approach, add timeout/error handling and note that user_profile does not carry scopes.

-function authenticateToken(req, res, next) {
-  const authHeader = req.headers["authorization"];
-  const token = authHeader && authHeader.split(" ")[1];
-
-  if (!token) {
-    return res.status(401).json({error: "Access token required"});
-  }
-
-  // Validate token with Kinde
-  fetch("https://<your-subdomain>.kinde.com/oauth2/v2/user_profile", {
-    headers: {
-      Authorization: `Bearer ${token}`
-    }
-  })
-    .then((response) => {
-      if (!response.ok) {
-        throw new Error("Invalid token");
-      }
-      return response.json();
-    })
-    .then((user) => {
-      req.user = user;
-      next();
-    })
-    .catch((error) => {
-      return res.status(401).json({error: "Invalid token"});
-    });
-}
+function authenticateToken(req, res, next) {
+  const authHeader = req.headers["authorization"];
+  const token = authHeader && authHeader.split(" ")[1];
+  if (!token) return res.status(401).json({ error: "Access token required" });
+
+  validateToken(token).then((result) => {
+    if (!result.valid) return res.status(401).json({ error: result.error || "Invalid token" });
+    req.user = result.user; // decoded claims
+    next();
+  }).catch(() => res.status(401).json({ error: "Invalid token" }));
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function authenticateToken(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
return res.status(401).json({error: "Access token required"});
}
// Validate token with Kinde
fetch("https://<your-subdomain>.kinde.com/oauth2/v2/user_profile", {
headers: {
Authorization: `Bearer ${token}`
}
})
.then((response) => {
if (!response.ok) {
throw new Error("Invalid token");
}
return response.json();
})
.then((user) => {
req.user = user;
next();
})
.catch((error) => {
return res.status(401).json({error: "Invalid token"});
});
}
function authenticateToken(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (!token) return res.status(401).json({ error: "Access token required" });
validateToken(token)
.then((result) => {
if (!result.valid) return res.status(401).json({ error: result.error || "Invalid token" });
req.user = result.user; // decoded claims
next();
})
.catch(() => res.status(401).json({ error: "Invalid token" }));
}
🤖 Prompt for AI Agents
In src/content/docs/authenticate/device-authorization-flow/api-calls.mdx around
lines 127 to 154, the middleware currently fetches /user_profile on every
request; instead implement local JWT validation: extract the Bearer token,
download/refresh the provider JWKS (cache keys and respect cache-control),
verify token signature, issuer, audience, and expiry, then set req.user to the
decoded claims and call next(); if validation fails return 401. If you choose to
keep the remote fetch approach, add a request timeout, proper error handling for
non-200 responses and network errors, and a comment that user_profile does not
include scopes so you must rely on token claims for authorization.

```

### Error handling

Handle common token-related errors:

```javascript
function handleTokenError(error) {
switch (error.error) {
case "invalid_token":
// Token is invalid or expired
return res.status(401).json({error: "Please re-authenticate"});

case "insufficient_scope":
// Token doesn't have required permissions
return res.status(403).json({error: "Insufficient permissions"});

default:
return res.status(500).json({error: "Authentication error"});
}
}
```

## Security best practices

### Token storage

- **Never store tokens in localStorage**: Use secure HTTP-only cookies or memory storage
- **Validate tokens server-side**: Always validate tokens on your backend, not just the client

### Rate limiting

Implement rate limiting for token validation requests:

```javascript
const rateLimit = require("express-rate-limit");

const tokenValidationLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: "Too many token validation requests"
});

app.use("/api/protected-resource", tokenValidationLimiter);
```

### Logging and monitoring

Log authentication events for security monitoring:

```javascript
function logAuthEvent(token, action, success) {
console.log({
timestamp: new Date().toISOString(),
action: action,
success: success,
userId: token.user_id,
scopes: token.scope
});
}
```

## Testing your API

Test your protected endpoints with the access token:

```bash
# Test with curl
curl -X GET https://your-api.com/protected-resource \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Test with JavaScript
fetch('https://your-api.com/protected-resource', {
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
}
})
.then(response => response.json())
.then(data => console.log(data));
```
120 changes: 120 additions & 0 deletions src/content/docs/authenticate/device-authorization-flow/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
page_id: ab3834cc-4645-4b7a-826a-c7f502eee3dd
title: Overview
sidebar:
order: 2
relatedArticles:
- 2944d2bc-4e84-4918-b4f6-7406a7c26f98
- 8d8234a5-b064-47a1-a1e1-9b3f98a57f34
- 888b1546-8047-4609-af59-8cf859527aa0
- 28c6e830-8e82-4bf8-aab7-87ebafeb68e4
---

Device Authorization Flow (RFC 8628) enables OAuth 2.0 authorization for devices with limited input capabilities, such as smart TVs, gaming consoles, or IoT devices. Users authenticate on a secondary device (like a phone or computer) while the primary device receives the access token.

## How it works

The Device Authorization Flow follows these steps:

1. **Device requests authorization**: The device requests a device code and user code from Kinde
2. **User authenticates**: The user visits a verification URI on another device and enters the user code
3. **Device polls for token**: The device polls the token endpoint until authorization is complete
4. **Access granted**: The device receives an access token and can call protected APIs

## Endpoints

### Device Authorization Endpoint

**URL**: `https://<your-subdomain>.kinde.com/oauth2/device/auth`

**Method**: `POST`

**Content-Type**: `application/x-www-form-urlencoded`

**Parameters**:

- `grant_type`: `urn:ietf:params:oauth:grant-type:device_code`
- `client_id` (optional): Your application's client ID - can be omitted if you have set an application as the default for device flows
- `audience` (optional): The audience to use for the request

**Response**:

```json
{
"device_code": "kinde_dc_device_code_here",
"user_code": "CSLDFDUU",
"verification_uri": "https://<your-subdomain>.kinde.com/device",
"verification_uri_complete": "https://<your-subdomain>.kinde.com/device?user_code=CSLDFDUU",
"expires_in": 600,
"interval": 5,
"qr_code": "data:image/png;base64,..."
}
```

### Token Endpoint

**URL**: `https://<your-subdomain>.kinde.com/oauth2/token`

**Method**: `POST`

**Content-Type**: `application/x-www-form-urlencoded`

**Parameters**:

- `grant_type`: `urn:ietf:params:oauth:grant-type:device_code`
- `client_id`: Your application's client ID
- `device_code`: The device code received from the authorization endpoint

**Success response**:

```json
{
"access_token": "eyJ...",
"expires_in": 86400,
"scope": "", // granted scopes are in the access token
"token_type": "bearer"
}
```

**Example error response**:

```json
{
"error": "authorization_pending",
"error_description": "The user has not yet completed the authorization"
}
```

## Polling behavior

The device must poll the token endpoint at regular intervals until the user completes authentication:

- **Initial interval**: Use the `interval` value from the device authorization response (typically 5 seconds)
- **Slow down**: If you receive a `slow_down` error, increase the polling interval by 5 seconds
- **Maximum time**: Stop polling after the `expires_in` time (typically 30 minutes)

## Error codes

| Error Code | Description | Action |
| ----------------------- | ------------------------------------ | ------------------------------ |
| `authorization_pending` | User hasn't completed authentication | Continue polling |
| `slow_down` | Polling too frequently | Increase interval by 5 seconds |
| `access_denied` | User denied the authorization | Stop polling |
| `expired_token` | Device code has expired | Request a new device code |
| `server_error` | Misconfigured device code | Request a new device code |

## Security considerations

- **User code format**: User codes are formatted as `XXXXXXXX` for easy entry
- **Verification URI**: Users should verify they're on the correct domain
- **Token expiration**: Access tokens expire after 1 hour by default

## Audience

If an `audience` is specified in the request, the access token will include the audience in the `aud` claim. Kinde supports requesting multiple audiences.

The API must be authorized for the Device Authorization application.

## Scopes and permissions

If an audience is specified in the request, any scopes which are belong to that audience that are granted to the user by their role will also be granted to the device. The list of scopes will be displayed on the consent screen. If the user consents, the scopes will be included in the `scope` claim of the access token.
Loading