Skip to content

Commit 9b776f7

Browse files
committed
finish exercise 3 instructions
1 parent db1166c commit 9b776f7

File tree

7 files changed

+210
-0
lines changed

7 files changed

+210
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,75 @@
11
# Introspect
2+
3+
👨‍💼 Ok, so we've got the user's auth token, but what do we do with it? Our server needs to know:
4+
5+
- Is the token valid?
6+
- Is it still active?
7+
- Who is this person? (user ID)
8+
- What permissions do they have? (scopes)
9+
10+
So we need to "introspect" the token to get this information.
11+
12+
```ts
13+
// When a user makes a request, we need to resolve their token
14+
const authInfo = await resolveAuthInfo(request.headers.get('authorization'))
15+
if (!authInfo) {
16+
return handleUnauthorized(request)
17+
}
18+
19+
console.log(`User ${authInfo.extra.userId} from ${authInfo.clientId}`)
20+
console.log(`Has scopes: ${authInfo.scopes.join(', ')}`)
21+
```
22+
23+
Auth providers implement this slightly differently, but the core concept is the same: you send the token to the auth server, and it tells you everything you need to know about that token. Some auth providers store their token as a JWT which means the information is contained within the token itself and you don't need to perform a request to the auth server to get the information.
24+
25+
One way or another though, you need to determine whether the token is valid and active.
26+
27+
<callout-info>
28+
🔍 Token validation is essential for user-specific features. Without it, you
29+
can't personalize the experience or enforce proper permissions.
30+
</callout-info>
31+
32+
The introspection process involves making a POST request to the auth server's introspection endpoint with the token. The response contains the user ID (`sub`), client ID (`client_id`), and scopes (`scope`) that tell you exactly what this token represents.
33+
34+
```ts
35+
// Example introspection request
36+
const validateUrl = new URL('/oauth/introspection', 'https://auth.example.com')
37+
const resp = await fetch(validateUrl, {
38+
method: 'POST',
39+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
40+
body: new URLSearchParams({ token }),
41+
})
42+
43+
const result = await resp.json()
44+
// result.sub = user ID
45+
// result.client_id = app the user is using
46+
// result.scope = space-separated list of permissions
47+
```
48+
49+
<callout-info>
50+
🎯 When you're finished with this exercise, the behavior won't change
51+
visually. Consider adding a `console.log` to see the introspection results in
52+
action!
53+
</callout-info>
54+
55+
```mermaid
56+
sequenceDiagram
57+
MCP Client->>MCP Server: POST /mcp (Authorization: Bearer token)
58+
MCP Server->>Auth Server: POST /oauth/introspection (token)
59+
alt Token is active
60+
Auth Server-->>MCP Server: Returns user info (sub, client_id, scope)
61+
MCP Server-->>MCP Client: Processes request with user context
62+
else Token is invalid/expired
63+
Auth Server-->>MCP Server: Returns error or inactive status
64+
MCP Server-->>MCP Client: Returns unauthorized response
65+
end
66+
```
67+
68+
<callout-muted>
69+
📜 For more details on token introspection, see the [OAuth 2.0 Token
70+
Introspection RFC](https://datatracker.ietf.org/doc/html/rfc7662).
71+
</callout-muted>
72+
73+
The goal is to transform anonymous tokens into rich user context, enabling personalized experiences and proper permission enforcement.
74+
75+
Now, let's implement the `resolveAuthInfo` function to make this happen!
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# Introspect
2+
3+
👨‍💼 Super! You've successfully implemented token introspection, which transforms anonymous tokens into rich user context. Now our MCP server can identify who each user is and understand their permissions, enabling personalized experiences and proper access control.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,44 @@
11
# Invalid Token Error
2+
3+
👨‍💼 When clients provide an authentication token that turns out to be invalid or expired, they need clear feedback about what went wrong. Without proper error messaging, users might think the service is broken or get confused about why their request failed.
4+
5+
The current error response doesn't distinguish between "no token provided" and "invalid token provided." This makes it harder for clients to provide helpful guidance to clients about what they need to do next.
6+
7+
```
8+
// When no Authorization header is present:
9+
WWW-Authenticate: Bearer realm="EpicMe", resource_metadata=https://example.com/.well-known/oauth-protected-resource/mcp
10+
11+
// When Authorization header is present but token is invalid:
12+
WWW-Authenticate: Bearer realm="EpicMe", error="invalid_token", error_description="The access token is invalid or expired", resource_metadata=https://example.com/.well-known/oauth-protected-resource/mcp
13+
```
14+
15+
By adding the `error` and `error_description` parameters to the `WWW-Authenticate` header when an Authorization header is present, clients can provide more specific guidance to users. This helps clients know what they need to do to fix the problem.
16+
17+
<callout-info>
18+
🎯 The `error` parameter follows OAuth 2.0 standards and helps clients
19+
distinguish between different types of authentication failures.
20+
</callout-info>
21+
22+
```mermaid
23+
sequenceDiagram
24+
User->>MCP Client: Makes request
25+
MCP Client->>MCP Server: POST /mcp<br/>(Authorization: Bearer {invalid_token})
26+
MCP Server->>Auth Server: POST /oauth/introspection<br/>(invalid_token)
27+
Auth Server-->>MCP Server: Returns error or inactive status
28+
MCP Server-->>MCP Client: 401 with error="invalid_token"<br/>and error_description
29+
MCP Client->>User: Shows "Token expired,<br/>please log in again"
30+
```
31+
32+
<callout-warning>
33+
Only include error parameters when an Authorization header is present. Users
34+
without tokens should get a generic unauthorized response.
35+
</callout-warning>
36+
37+
<callout-muted>
38+
📜 For more details on OAuth 2.0 error handling, see the [OAuth 2.0 Bearer
39+
Token Usage RFC](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1).
40+
</callout-muted>
41+
42+
The goal is to make authentication errors more actionable for users, helping them understand exactly what they need to do to fix the problem.
43+
44+
Now, let's enhance the error handling to provide better feedback when tokens are invalid!
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# Invalid Token Error
2+
3+
👨‍💼 Excellent! We've enhanced our error handling to provide clear, actionable feedback when clients provide invalid tokens. Now users get specific guidance about token expiration instead of generic error messages, making the authentication experience much more user-friendly.
4+
5+
This improvement means clients can distinguish between "no token provided" and "invalid token provided," enabling them to show appropriate messages like "Please log in again" when tokens expire.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,84 @@
11
# Token Active
2+
3+
👨‍💼 When we introspect an auth token, the introspection endpoint will return whether the token is currently active (because tokens can be revoked or expired) and we need to check for that in our resolution callback.
4+
5+
The solution is to always verify that a token is active before trusting any of its claims. OAuth 2.0 introspection responses include an `active` property that tells us exactly this.
6+
7+
It's pretty simple:
8+
9+
```ts
10+
const data = await introspectionResopnse.json()
11+
if (!data.active) {
12+
// Token is expired, revoked, or invalid
13+
return null
14+
}
15+
```
16+
17+
## Discriminated Unions
18+
19+
🦉 As a bonus in this exercise step you can make the types nicer if you use discriminated unions. When working with responses that can have different shapes based on a property value, TypeScript's discriminated unions are incredibly useful. They let you create types that change based on a specific field, giving you type safety and better IntelliSense.
20+
21+
Here's a simple example of how discriminated unions work:
22+
23+
```ts
24+
// A discriminated union based on the "status" property
25+
type ApiResponse =
26+
| { status: 'success'; data: Array<string> }
27+
| { status: 'error'; message: string }
28+
29+
// TypeScript knows the shape based on the status
30+
function handleResponse(response: ApiResponse) {
31+
if (response.status === 'success') {
32+
// TypeScript knows response.data exists here
33+
console.log(response.data.length)
34+
} else {
35+
// TypeScript knows response.message exists here
36+
console.log(response.message)
37+
}
38+
}
39+
```
40+
41+
Here's how you represent that with zod:
42+
43+
```ts
44+
const introspectResponseSchema = z.discriminatedUnion('status', [
45+
z.object({
46+
status: z.literal('success'),
47+
data: z.array(z.string()),
48+
}),
49+
z.object({
50+
status: z.literal('error'),
51+
message: z.string(),
52+
}),
53+
])
54+
```
55+
56+
In our case, we'll use a discriminated union based on the `active` property to handle both active and inactive token responses safely.
57+
58+
<callout-warning>
59+
🎯 When a token is inactive, don't process any of its claims. Return null
60+
immediately to prevent unauthorized access.
61+
</callout-warning>
62+
63+
```mermaid
64+
sequenceDiagram
65+
MCP Client->>MCP Server: POST /mcp (Authorization: Bearer token)
66+
MCP Server->>Auth Server: POST /oauth/introspection (token)
67+
alt Token is active
68+
Auth Server-->>MCP Server: { active: true, sub: "user123", ... }
69+
MCP Server-->>MCP Client: Processes request with user context
70+
else Token is inactive
71+
Auth Server-->>MCP Server: { active: false }
72+
MCP Server-->>MCP Client: Returns null (no auth info)
73+
end
74+
```
75+
76+
<callout-muted>
77+
📜 For more details on OAuth 2.0 token introspection and the `active`
78+
property, see the [OAuth 2.0 Token Introspection
79+
RFC](https://datatracker.ietf.org/doc/html/rfc7662).
80+
</callout-muted>
81+
82+
The goal is to ensure that only active, valid tokens can access our MCP server resources, maintaining security and providing a reliable user experience.
83+
84+
Now, let's implement proper token validation by checking the `active` property!
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# Token Active
2+
3+
👨‍💼 Great job. Really checking the `active` property was just a simple change, but making it typesafe with discriminated unions was a nice bonus!
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# Auth Info
2+
3+
Great work on this. You now know how to go from an auth token to user-specific information that will help you identify the user and their permissions in your server. Now on to getting that information into your MCP tools!

0 commit comments

Comments
 (0)