AuthdogVerifier from @authdog/mcp-sdk verifies the bearer token an MCP client presents to your Node.js MCP server. It checks the token locally against your environment's JWKS, or calls GET {baseUrl}/v1/userinfo, and returns an AuthInfo (subject, scopes, expiry, claims). Source is `node/` in authdog/mcp-sdk. The package is ESM, uses axios, and declares node >=16.
This is the MCP token verifier. The name @authdog/mcp-sdk also belongs to a separate private runtime package inside the Authdog platform monorepo (trust store, AuthZEN, telemetry). That package is not on npm. See MCP SDK for how the two relate.
Install
npm install @authdog/mcp-sdk
# or
deno add npm:@authdog/mcp-sdkTo use the MCP adapter (@authdog/mcp-sdk/mcp), also install the MCP TypeScript SDK, version 1.32 or newer. It is an optional peer dependency.
npm install @modelcontextprotocol/sdkConfigure
import { AuthdogVerifier } from "@authdog/mcp-sdk"
const verifier = new AuthdogVerifier({
baseUrl: "https://api.authdog.com",
issuer: "https://api.authdog.com/oidc/<environmentId>",
audience: "https://mcp.example.com",
})| Option | Type | Default | Behavior |
|---|---|---|---|
baseUrl |
string |
required | Authdog API base URL. Trailing slashes are stripped. A missing or blank value throws TypeError. |
timeout |
number |
10000 |
Request timeout in milliseconds. A falsy value (including 0) falls back to 10000. |
issuer |
string |
none | Environment OIDC issuer. Setting it turns on local JWT verification. |
audience |
string |
none | The token's aud must contain it. Set it in production so a token minted for another resource is refused. |
mode |
"auto" | "jwt" | "userinfo" |
"auto" |
auto verifies JWT-shaped tokens locally when issuer is set and sends other tokens to userinfo. jwt is local only. userinfo always calls the API. |
jwksUri |
string |
discovered | Skip discovery and use this JWKS URL. |
algorithms |
string[] |
RS256, ES256, EdDSA | none and HMAC algorithms are never accepted. |
clockSkewSeconds |
number |
60 |
Tolerance for exp and nbf. |
Each request sends Content-Type: application/json and a User-Agent of authdog-mcp-node-sdk.
Verify a token
import { AuthdogVerifier, AuthenticationError, APIError } from "@authdog/mcp-sdk"
const authInfo = await verifier.verifyToken("the-access-token")
console.log(authInfo.subject, authInfo.scopes, authInfo.expiresAt)verifyToken(accessToken: string, signal?: AbortSignal): Promise<AuthInfo> sends the token to the right verifier. Pass an AbortSignal to cancel the in-flight request, for example when the incoming MCP request is cancelled. An empty or whitespace-only token throws AuthenticationError before any request is made.
AuthInfo:
| Field | Type | Value |
|---|---|---|
token |
string |
The token you passed in |
subject |
string |
The caller: a user id, or the machine subject Authdog creates for an agent or M2M client. |
username |
string |
user.userName when verified through userinfo |
email |
string | null |
First user.emails[].value, or null |
scopes |
string[] |
From the scope or scp claim when verified locally. [] through userinfo. |
expiresAt |
Date | null |
From the token's exp when verified locally. Through userinfo, now plus session.remainingSeconds when that is greater than 0. Otherwise null. |
user |
UserInfoResponse["user"] | null |
Full user object through userinfo. null when verified locally. |
claims |
Record<string, unknown> |
Verified JWT claims, local verification only |
Set issuer for agent traffic. Local verification needs no API call per request and reads scopes from the token.
Local JWT verification
With issuer set, the SDK fetches the issuer's OIDC discovery document and JWKS once, caches the keys, and checks signature, iss, aud, exp and nbf. A JWT that fails is rejected with AuthenticationError and is never retried against userinfo. If discovery or the JWKS cannot be reached and no key is cached, verification fails with APIError (fail closed).
| Option | Default | Behavior |
|---|---|---|
jwksCacheSeconds |
300 |
How long keys are cached. A failed refresh keeps using the cached keys. |
jwksCooldownSeconds |
30 |
An unknown kid triggers at most one refresh per cooldown, so a rotated key is picked up quickly. |
maxRetries, retryBaseDelayMs |
0, 100 |
Opt-in bounded, jittered retry for timeouts, 502, 503 and 504. A 401 is never retried. |
userinfoCacheSeconds |
0 (off) |
Cache successful userinfo results, never past the token's own expiry. |
introspection |
off | { endpoint?, clientId, clientSecret }. RFC 7662 introspection for opaque tokens, using a confidential client of your environment. |
Scopes, challenges, and resource metadata
import {
requireScopes,
challengeForError,
buildProtectedResourceMetadata,
} from "@authdog/mcp-sdk"
const auth = await verifier.verifyToken(token)
requireScopes(auth, ["tools:call"]) // throws InsufficientScopeError
// On failure, answer with the right status and WWW-Authenticate header:
const { status, header } = challengeForError(
error,
"https://mcp.example.com/.well-known/oauth-protected-resource",
)
// Serve this at /.well-known/oauth-protected-resource:
const metadata = buildProtectedResourceMetadata({
resource: "https://mcp.example.com",
authorizationServers: ["https://api.authdog.com/oidc/<environmentId>"],
scopesSupported: ["tools:call"],
})| Failure | Status |
|---|---|
| Missing or rejected token | 401 with a Bearer challenge |
| Insufficient scope | 403 with error="insufficient_scope" |
Verification could not be completed (APIError) |
503 with no Bearer challenge, because the client did nothing wrong |
buildChallenge builds the header on its own.
Wire into an MCP server
The MCP TypeScript SDK protects HTTP transports with requireBearerAuth, which delegates to a token verifier. createMcpTokenVerifier adapts AuthdogVerifier to that interface:
import express from "express"
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js"
import { AuthdogVerifier } from "@authdog/mcp-sdk"
import { createMcpTokenVerifier } from "@authdog/mcp-sdk/mcp"
const authdog = new AuthdogVerifier({
baseUrl: "https://api.authdog.com",
issuer: "https://api.authdog.com/oidc/<environmentId>",
audience: "https://mcp.example.com",
})
const app = express()
app.use(
"/mcp",
requireBearerAuth({
verifier: createMcpTokenVerifier(authdog, {
resource: "https://mcp.example.com",
}),
requiredScopes: ["tools:call"],
resourceMetadataUrl:
"https://mcp.example.com/.well-known/oauth-protected-resource",
expectedResource: "https://mcp.example.com",
}),
)The adapter translates errors into the MCP SDK's own types, so requireBearerAuth answers correctly: a rejected token is a 401 with a WWW-Authenticate challenge, an insufficient scope is a 403, and a verification that could not be completed is a 500, because the MCP middleware has no 503. requireBearerAuth also rejects tokens that carry no expiry.
Errors
All three classes are exported and extend Error. name is set to the class name.
| Class | Thrown when |
|---|---|
AuthdogError |
Base class for the others |
AuthenticationError |
Empty or blank token, or HTTP 401 (Unauthorized - invalid or expired token) |
APIError |
Timeout (Request timed out: ...), other non-2xx (HTTP error <status>: <body>), network failure (Request failed: ...), or a 2xx response with no user (Invalid response: missing user in userinfo payload) |
try {
await verifier.verifyToken(token)
} catch (error) {
if (error instanceof AuthenticationError) {
// reject with 401
} else if (error instanceof APIError) {
// upstream or transport failure
} else {
throw error
}
}Related
- MCP SDK: language overview and the name collision
- MCP authentication: how MCP clients obtain tokens
- Python and Go verifiers