Skip to content

Commit 6338d14

Browse files
committed
feat: add auth
1 parent e8b808f commit 6338d14

File tree

14 files changed

+1363
-445
lines changed

14 files changed

+1363
-445
lines changed

README.md

Lines changed: 116 additions & 250 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 481 additions & 58 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mcp-framework",
3-
"version": "0.1.21-beta.9",
3+
"version": "0.1.21-beta.12",
44
"description": "Framework for building Model Context Protocol (MCP) servers in Typescript",
55
"type": "module",
66
"author": "Alex Andru <alex@andru.codes>",
@@ -40,13 +40,16 @@
4040
"commander": "^12.1.0",
4141
"execa": "^9.5.2",
4242
"find-up": "^7.0.0",
43+
"jsonwebtoken": "^9.0.2",
4344
"prompts": "^2.4.2",
4445
"typescript": "^5.3.3",
4546
"zod": "^3.23.8"
4647
},
4748
"devDependencies": {
4849
"@modelcontextprotocol/sdk": "^0.6.0",
50+
"@types/content-type": "^1.1.8",
4951
"@types/jest": "^29.5.12",
52+
"@types/jsonwebtoken": "^9.0.8",
5053
"@types/node": "^20.11.24",
5154
"jest": "^29.7.0",
5255
"ts-jest": "^29.1.2"

src/auth/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export * from "./types.js";
2+
export * from "./providers/jwt.js";
3+
export * from "./providers/apikey.js";
4+
5+
export type { AuthProvider, AuthConfig, AuthResult } from "./types.js";
6+
export type { JWTConfig } from "./providers/jwt.js";
7+
export type { APIKeyConfig } from "./providers/apikey.js";

src/auth/providers/apikey.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { IncomingMessage } from "node:http";
2+
import { logger } from "../../core/Logger.js";
3+
import { AuthProvider, AuthResult, DEFAULT_AUTH_ERROR } from "../types.js";
4+
5+
/**
6+
* Configuration options for API key authentication
7+
*/
8+
export interface APIKeyConfig {
9+
/**
10+
* Valid API keys
11+
*/
12+
keys: string[];
13+
14+
/**
15+
* Name of the header containing the API key
16+
* @default "X-API-Key"
17+
*/
18+
headerName?: string;
19+
}
20+
21+
/**
22+
* API key-based authentication provider
23+
*/
24+
export class APIKeyAuthProvider implements AuthProvider {
25+
private config: Required<APIKeyConfig>;
26+
27+
constructor(config: APIKeyConfig) {
28+
this.config = {
29+
headerName: "X-API-Key",
30+
...config
31+
};
32+
33+
if (!this.config.keys?.length) {
34+
throw new Error("At least one API key is required");
35+
}
36+
}
37+
38+
/**
39+
* Get the number of configured API keys
40+
*/
41+
getKeyCount(): number {
42+
return this.config.keys.length;
43+
}
44+
45+
/**
46+
* Get the configured header name
47+
*/
48+
getHeaderName(): string {
49+
return this.config.headerName;
50+
}
51+
52+
async authenticate(req: IncomingMessage): Promise<boolean | AuthResult> {
53+
logger.debug(`API Key auth attempt from ${req.socket.remoteAddress}`);
54+
55+
logger.debug(`All request headers: ${JSON.stringify(req.headers, null, 2)}`);
56+
57+
const headerVariations = [
58+
this.config.headerName,
59+
this.config.headerName.toLowerCase(),
60+
this.config.headerName.toUpperCase(),
61+
'x-api-key',
62+
'X-API-KEY',
63+
'X-Api-Key'
64+
];
65+
66+
logger.debug(`Looking for header variations: ${headerVariations.join(', ')}`);
67+
68+
let apiKey: string | undefined;
69+
let matchedHeader: string | undefined;
70+
71+
for (const [key, value] of Object.entries(req.headers)) {
72+
const lowerKey = key.toLowerCase();
73+
if (headerVariations.some(h => h.toLowerCase() === lowerKey)) {
74+
apiKey = Array.isArray(value) ? value[0] : value;
75+
matchedHeader = key;
76+
break;
77+
}
78+
}
79+
80+
if (!apiKey) {
81+
logger.debug(`API Key header missing. Checked variations: ${headerVariations.join(', ')}`);
82+
logger.debug(`Available headers: ${Object.keys(req.headers).join(', ')}`);
83+
return false;
84+
}
85+
86+
logger.debug(`Found API key in header: ${matchedHeader}`);
87+
logger.debug(`Comparing provided key: ${apiKey.substring(0, 3)}... with ${this.config.keys.length} configured keys`);
88+
89+
for (const validKey of this.config.keys) {
90+
logger.debug(`Comparing with key: ${validKey.substring(0, 3)}...`);
91+
if (apiKey === validKey) {
92+
logger.debug(`API Key authentication successful - matched key starting with ${validKey.substring(0, 3)}...`);
93+
return true;
94+
}
95+
}
96+
97+
logger.debug(`Invalid API Key provided: ${apiKey.substring(0, 3)}...`);
98+
logger.debug(`Expected one of: ${this.config.keys.map(k => k.substring(0, 3) + '...').join(', ')}`);
99+
return false;
100+
}
101+
102+
getAuthError() {
103+
return {
104+
...DEFAULT_AUTH_ERROR,
105+
message: "Invalid API key"
106+
};
107+
}
108+
}

src/auth/providers/jwt.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { IncomingMessage } from "node:http";
2+
import jwt, { Algorithm } from "jsonwebtoken";
3+
import { AuthProvider, AuthResult, DEFAULT_AUTH_ERROR } from "../types.js";
4+
5+
/**
6+
* Configuration options for JWT authentication
7+
*/
8+
export interface JWTConfig {
9+
/**
10+
* Secret key for verifying JWT tokens
11+
*/
12+
secret: string;
13+
14+
/**
15+
* Allowed JWT algorithms
16+
* @default ["HS256"]
17+
*/
18+
algorithms?: Algorithm[];
19+
20+
/**
21+
* Name of the header containing the JWT token
22+
* @default "Authorization"
23+
*/
24+
headerName?: string;
25+
26+
/**
27+
* Whether to require "Bearer" prefix in Authorization header
28+
* @default true
29+
*/
30+
requireBearer?: boolean;
31+
}
32+
33+
/**
34+
* JWT-based authentication provider
35+
*/
36+
export class JWTAuthProvider implements AuthProvider {
37+
private config: Required<JWTConfig>;
38+
39+
constructor(config: JWTConfig) {
40+
this.config = {
41+
algorithms: ["HS256"],
42+
headerName: "Authorization",
43+
requireBearer: true,
44+
...config
45+
};
46+
47+
if (!this.config.secret) {
48+
throw new Error("JWT secret is required");
49+
}
50+
}
51+
52+
async authenticate(req: IncomingMessage): Promise<boolean | AuthResult> {
53+
const authHeader = req.headers[this.config.headerName.toLowerCase()];
54+
55+
if (!authHeader || typeof authHeader !== "string") {
56+
return false;
57+
}
58+
59+
let token = authHeader;
60+
if (this.config.requireBearer) {
61+
if (!authHeader.startsWith("Bearer ")) {
62+
return false;
63+
}
64+
token = authHeader.split(" ")[1];
65+
}
66+
67+
try {
68+
const decoded = jwt.verify(token, this.config.secret, {
69+
algorithms: this.config.algorithms
70+
});
71+
72+
return {
73+
data: typeof decoded === "object" ? decoded : { sub: decoded }
74+
};
75+
} catch (err) {
76+
return false;
77+
}
78+
}
79+
80+
getAuthError() {
81+
return {
82+
...DEFAULT_AUTH_ERROR,
83+
message: "Invalid or expired JWT token"
84+
};
85+
}
86+
}

src/auth/types.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { IncomingMessage } from "node:http";
2+
3+
/**
4+
* Result of successful authentication
5+
*/
6+
export interface AuthResult {
7+
/**
8+
* User or token data from authentication
9+
*/
10+
data?: Record<string, any>;
11+
}
12+
13+
/**
14+
* Base interface for authentication providers
15+
*/
16+
export interface AuthProvider {
17+
/**
18+
* Authenticate an incoming request
19+
* @param req The incoming HTTP request
20+
* @returns Promise resolving to boolean or AuthResult
21+
*/
22+
authenticate(req: IncomingMessage): Promise<boolean | AuthResult>;
23+
24+
/**
25+
* Get error details for failed authentication
26+
*/
27+
getAuthError?(): { status: number; message: string };
28+
}
29+
30+
/**
31+
* Authentication configuration for transport
32+
*/
33+
export interface AuthConfig {
34+
/**
35+
* Authentication provider implementation
36+
*/
37+
provider: AuthProvider;
38+
39+
/**
40+
* Per-endpoint authentication configuration
41+
*/
42+
endpoints?: {
43+
/**
44+
* Whether to authenticate SSE connection endpoint
45+
* @default false
46+
*/
47+
sse?: boolean;
48+
49+
/**
50+
* Whether to authenticate message endpoint
51+
* @default true
52+
*/
53+
messages?: boolean;
54+
};
55+
}
56+
57+
/**
58+
* Default authentication error
59+
*/
60+
export const DEFAULT_AUTH_ERROR = {
61+
status: 401,
62+
message: "Unauthorized"
63+
};

0 commit comments

Comments
 (0)