mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
feat(mcp): add an OAuth 2.1 authorization-server bridge for remote connectors
Let a remote MCP client (Claude.ai, ChatGPT) self-register and connect to the `/mcp` endpoint over OAuth 2.1 + PKCE, bridged onto the existing `hlk_` Bearer model — no parallel identity store. - Discovery: RFC 9728 Protected Resource Metadata and RFC 8414 Authorization Server Metadata at `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server` (added to the proxy allowlist). - `/api/mcp/oauth/authorize` — consent gate backed by the existing session; a code is only issued on an authenticated POST decision, never on a GET. - `/api/mcp/oauth/token` — authorization-code and refresh-token grants with mandatory PKCE S256, single-use codes, and rotating refresh tokens. The access token is an ordinary short-lived `hlk_` token scoped `health:read`. - `/api/mcp/oauth/register` — RFC 7591 DCR fallback; CIMD (SEP-991) is the preferred path and resolves client metadata through `safeFetch`. - Audience binding (RFC 8707): every token is bound to the canonical `/mcp` resource URI; a mismatched `resource` is rejected at authorize and token. - The grant artifacts (code, refresh, DCR client id) are HMAC-signed, self-describing, and domain-separated; the central log redactors scrub them plus the PKCE verifier.
This commit is contained in:
1 parent
03d41bc5c0
commit
7ec3bf0dad
18 files changed
+2186
-3
No files matched your search
@@ -23,6 +23,12 @@ export interface IssueTokenOptions {
|
||||
permissions?: string[];
|
||||
/** Lifetime in days. */
|
||||
expiresInDays?: number;
|
||||
/**
|
||||
* Lifetime in minutes. When set it WINS over `expiresInDays` — used for the
|
||||
* short-lived OAuth access tokens the MCP bridge mints (the refresh token
|
||||
* carries continuity). Ignored when `expiresInDays` alone is supplied.
|
||||
*/
|
||||
expiresInMinutes?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,9 +41,10 @@ export async function issueApiToken(
|
||||
): Promise<IssuedToken> {
|
||||
const rawToken = `hlk_${randomBytes(32).toString("hex")}`;
|
||||
const tokenHashValue = hashToken(rawToken);
|
||||
const expiresAt = new Date(
|
||||
Date.now() + (opts.expiresInDays ?? 90) * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
const expiresAt =
|
||||
opts.expiresInMinutes !== undefined
|
||||
? new Date(Date.now() + opts.expiresInMinutes * 60 * 1000)
|
||||
: new Date(Date.now() + (opts.expiresInDays ?? 90) * 24 * 60 * 60 * 1000);
|
||||
|
||||
const created = await prisma.apiToken.create({
|
||||
data: {
|
||||
|
||||
Reference in new issue
Block a user