Authdog

MCP SDK: Node.js

Last updated Nov 14, 2026
View as Markdown

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-sdk

To 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/sdk

Configure

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
  }
}