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 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
234 changes: 234 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,234 @@
---
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.

## Use the access token from the device authorization flow

The access token you receive from the 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 in the device authorization flow

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 with JWKS
+const jwt = require("jsonwebtoken");
+const jwksClient = require("jwks-rsa");
+
+const client = jwksClient({
+ jwksUri: "https://<your-subdomain>.kinde.com/.well-known/jwks"
+});
+
+function getKey(header, callback) {
+ client.getSigningKey(header.kid, (err, key) => {
+ const signingKey = key.publicKey || key.rsaPublicKey;
+ callback(null, signingKey);
+ });
+}
+
+function validateToken(token) {
+ return new Promise((resolve, reject) => {
+ jwt.verify(token, getKey, { algorithms: ["RS256"] }, (err, decoded) => {
+ if (err) {
+ resolve({ valid: false, error: err.message });
+ } else {
+ resolve({ valid: true, user: decoded });
+ }
+ });
+ });
+}
```

## Scope enforcement for device authorization

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 for device authorization

### 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 for device authorization

Handle common token-related errors:

```javascript
function handleTokenError(res, 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"});
}
}
Comment on lines +161 to +175
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

handleTokenError expects error.error, but jwt.verify throws typed errors

JsonWebTokenError, TokenExpiredError, etc., won’t have an error field. Map by error.name and still support structured errors from HTTP responses.

-function handleTokenError(res, 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"});
-  }
-}
+function handleTokenError(res, error) {
+  const code = error?.error || error?.name;
+  switch (code) {
+    case "invalid_token":
+    case "JsonWebTokenError":
+    case "NotBeforeError":
+      return res.status(401).json({ error: "Please re-authenticate" });
+    case "TokenExpiredError":
+      return res.status(401).json({ error: "Session expired. Please re-authenticate" });
+    case "insufficient_scope":
+      return res.status(403).json({ error: "Insufficient permissions" });
+    default:
+      return res.status(500).json({ error: "Authentication error" });
+  }
+}
📝 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
```javascript
function handleTokenError(res, 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"});
}
}
function handleTokenError(res, error) {
const code = error?.error || error?.name;
switch (code) {
case "invalid_token":
case "JsonWebTokenError":
case "NotBeforeError":
return res.status(401).json({ error: "Please re-authenticate" });
case "TokenExpiredError":
return res.status(401).json({ error: "Session expired. Please re-authenticate" });
case "insufficient_scope":
return res.status(403).json({ error: "Insufficient permissions" });
default:
return res.status(500).json({ error: "Authentication error" });
}
}
🤖 Prompt for AI Agents
In src/content/docs/authenticate/device-authorization-flow/api-calls.mdx around
lines 161 to 175, the handler currently inspects error.error but JWT runtime
errors use error.name (e.g., JsonWebTokenError, TokenExpiredError) and HTTP
responses may provide a structured error field; update the function to first
branch on error.name for known JWT errors (map TokenExpiredError -> 401 with
"Please re-authenticate", JsonWebTokenError -> 401 with generic auth error,
etc.), fall back to checking error.error for structured HTTP error payloads
(mapping "insufficient_scope" -> 403), and finally return a 500 for unknown
errors; ensure the response messages and status codes match the original intent
and preserve any original error details when appropriate.

```

## Security best practices for device authorization

### 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));
```
119 changes: 119 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,119 @@
---
page_id: ab3834cc-4645-4b7a-826a-c7f502eee3dd
title: About the device authorization flow
sidebar:
order: 2
relatedArticles:
- 2944d2bc-4e84-4918-b4f6-7406a7c26f98
- 8d8234a5-b064-47a1-a1e1-9b3f98a57f34
- 888b1546-8047-4609-af59-8cf859527aa0
- 28c6e830-8e82-4bf8-aab7-87ebafeb68e4
---

Kinde's device authorization flow adheres to `RFC 8628`, also known as the OAuth 2.0 Device Authorization Grant. It enables 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 the device authentication flow works

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 for the device authorization flow

### Device authorization endpoint

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

**Method**: `POST`

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

**Parameters**:

- `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": "",
"token_type": "bearer"
}
```

The scope field may be empty because granted scopes are carried in the access token’s scope claim.

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

## Device authorization flow 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 for device authorization

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

## Specifying an audience in a device authorization request

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 for a device authorization request

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