@keycardai/express 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/dist/cjs/bearerAuth.d.ts +76 -0
- package/dist/cjs/bearerAuth.d.ts.map +1 -0
- package/dist/cjs/bearerAuth.js +123 -0
- package/dist/cjs/bearerAuth.js.map +1 -0
- package/dist/cjs/grant.d.ts +46 -0
- package/dist/cjs/grant.d.ts.map +1 -0
- package/dist/cjs/grant.js +99 -0
- package/dist/cjs/grant.js.map +1 -0
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/middleware.d.ts +54 -0
- package/dist/cjs/middleware.d.ts.map +1 -0
- package/dist/cjs/middleware.js +49 -0
- package/dist/cjs/middleware.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/wellKnown.d.ts +48 -0
- package/dist/cjs/wellKnown.d.ts.map +1 -0
- package/dist/cjs/wellKnown.js +73 -0
- package/dist/cjs/wellKnown.js.map +1 -0
- package/dist/esm/bearerAuth.d.ts +76 -0
- package/dist/esm/bearerAuth.d.ts.map +1 -0
- package/dist/esm/bearerAuth.js +120 -0
- package/dist/esm/bearerAuth.js.map +1 -0
- package/dist/esm/grant.d.ts +46 -0
- package/dist/esm/grant.d.ts.map +1 -0
- package/dist/esm/grant.js +96 -0
- package/dist/esm/grant.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/middleware.d.ts +54 -0
- package/dist/esm/middleware.d.ts.map +1 -0
- package/dist/esm/middleware.js +46 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/wellKnown.d.ts +48 -0
- package/dist/esm/wellKnown.d.ts.map +1 -0
- package/dist/esm/wellKnown.js +70 -0
- package/dist/esm/wellKnown.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.keycardMetadataRouter = keycardMetadataRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
/**
|
|
6
|
+
* Returns an Express Router that serves the two OAuth discovery endpoints
|
|
7
|
+
* required by RFC 9728 and RFC 8414:
|
|
8
|
+
*
|
|
9
|
+
* - `GET /.well-known/oauth-protected-resource` (RFC 9728 §2)
|
|
10
|
+
* - `GET /.well-known/oauth-authorization-server` (RFC 8414 §3, proxied)
|
|
11
|
+
*
|
|
12
|
+
* Mount it at the application root:
|
|
13
|
+
* ```ts
|
|
14
|
+
* import express from "express";
|
|
15
|
+
* import { keycardMetadataRouter } from "@keycardai/express";
|
|
16
|
+
*
|
|
17
|
+
* const app = express();
|
|
18
|
+
* app.use(keycardMetadataRouter({ issuer: "https://zone.keycard.cloud" }));
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* These paths must remain publicly accessible (no bearer auth) per their
|
|
22
|
+
* respective specs. Per the security guidance in
|
|
23
|
+
* `@keycardai/starlette` and the feedback_specific_path_bypass rule:
|
|
24
|
+
* only bypass auth for these exact paths, never a broad `/.well-known/` prefix.
|
|
25
|
+
*/
|
|
26
|
+
function keycardMetadataRouter(options) {
|
|
27
|
+
const router = (0, express_1.Router)();
|
|
28
|
+
router.get("/.well-known/oauth-protected-resource", protectedResourceHandler(options));
|
|
29
|
+
router.get("/.well-known/oauth-authorization-server", authorizationServerHandler(options.issuer, options.asMetadataTimeoutMs ?? 10_000));
|
|
30
|
+
return router;
|
|
31
|
+
}
|
|
32
|
+
function protectedResourceHandler(options) {
|
|
33
|
+
return (req, res) => {
|
|
34
|
+
const resource = `${req.protocol}://${req.host}`;
|
|
35
|
+
const metadata = {
|
|
36
|
+
resource,
|
|
37
|
+
authorization_servers: [options.issuer],
|
|
38
|
+
};
|
|
39
|
+
if (options.resourceName)
|
|
40
|
+
metadata.resource_name = options.resourceName;
|
|
41
|
+
if (options.scopesSupported)
|
|
42
|
+
metadata.scopes_supported = [...options.scopesSupported];
|
|
43
|
+
if (options.resourceDocumentation)
|
|
44
|
+
metadata.resource_documentation = options.resourceDocumentation;
|
|
45
|
+
res.set("Access-Control-Allow-Origin", "*");
|
|
46
|
+
res.status(200).json(metadata);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function authorizationServerHandler(issuer, timeoutMs) {
|
|
50
|
+
return async (req, res, next) => {
|
|
51
|
+
try {
|
|
52
|
+
const upstream = await fetch(`${issuer}/.well-known/oauth-authorization-server`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
53
|
+
if (!upstream.ok) {
|
|
54
|
+
res.status(502).json({ error: "Failed to fetch AS metadata from issuer" });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const metadata = await upstream.json();
|
|
58
|
+
// Rewrite authorization_endpoint to include a `resource` param pointing
|
|
59
|
+
// at this server's origin so the AS knows which resource is being accessed.
|
|
60
|
+
if (typeof metadata.authorization_endpoint === "string") {
|
|
61
|
+
const authUrl = new URL(metadata.authorization_endpoint);
|
|
62
|
+
authUrl.searchParams.set("resource", `${req.protocol}://${req.host}`);
|
|
63
|
+
metadata.authorization_endpoint = authUrl.toString();
|
|
64
|
+
}
|
|
65
|
+
res.set("Access-Control-Allow-Origin", "*");
|
|
66
|
+
res.status(200).json(metadata);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
next(e);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=wellKnown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wellKnown.js","sourceRoot":"","sources":["../../src/wellKnown.ts"],"names":[],"mappings":";;AAiDA,sDAcC;AA/DD,qCAAiC;AA4BjC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAgB,qBAAqB,CAAC,OAA6B;IACjE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IAExB,MAAM,CAAC,GAAG,CACR,uCAAuC,EACvC,wBAAwB,CAAC,OAAO,CAAC,CAClC,CAAC;IAEF,MAAM,CAAC,GAAG,CACR,yCAAyC,EACzC,0BAA0B,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,mBAAmB,IAAI,MAAM,CAAC,CAClF,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,wBAAwB,CAAC,OAA6B;IAC7D,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAClB,MAAM,QAAQ,GAAG,GAAG,GAAG,CAAC,QAAQ,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QACjD,MAAM,QAAQ,GAA4B;YACxC,QAAQ;YACR,qBAAqB,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;SACxC,CAAC;QACF,IAAI,OAAO,CAAC,YAAY;YAAE,QAAQ,CAAC,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;QACxE,IAAI,OAAO,CAAC,eAAe;YAAE,QAAQ,CAAC,gBAAgB,GAAG,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;QACtF,IAAI,OAAO,CAAC,qBAAqB;YAAE,QAAQ,CAAC,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC;QAEnG,GAAG,CAAC,GAAG,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;QAC5C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjC,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,0BAA0B,CAAC,MAAc,EAAE,SAAiB;IACnE,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC9B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAC1B,GAAG,MAAM,yCAAyC,EAClD,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAC3C,CAAC;YACF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC,CAAC;gBAC3E,OAAO;YACT,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAA6B,CAAC;YAElE,wEAAwE;YACxE,4EAA4E;YAC5E,IAAI,OAAO,QAAQ,CAAC,sBAAsB,KAAK,QAAQ,EAAE,CAAC;gBACxD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC;gBACzD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,GAAG,CAAC,QAAQ,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;gBACtE,QAAQ,CAAC,sBAAsB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;YACvD,CAAC;YAED,GAAG,CAAC,GAAG,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;YAC5C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,CAAC,CAAC,CAAC;QACV,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Request, RequestHandler } from "express";
|
|
2
|
+
import { TokenVerifier } from "@keycardai/oauth/server/tokenVerifier";
|
|
3
|
+
import type { TokenVerifierOptions } from "@keycardai/oauth/server/tokenVerifier";
|
|
4
|
+
import type { AccessToken } from "@keycardai/oauth/server/accessToken";
|
|
5
|
+
/**
|
|
6
|
+
* Extends Express `Request` with the verified Keycard `AccessToken`.
|
|
7
|
+
*
|
|
8
|
+
* Cast inside handlers that run after `requireBearerAuth()`:
|
|
9
|
+
* ```ts
|
|
10
|
+
* app.get("/data", (req, res) => {
|
|
11
|
+
* const { auth } = req as AuthenticatedRequest;
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Alternatively, adopt Express module augmentation so `req.auth` is
|
|
16
|
+
* available without casting across your entire app:
|
|
17
|
+
* ```ts
|
|
18
|
+
* import type { AccessToken } from "@keycardai/oauth/server";
|
|
19
|
+
* declare global {
|
|
20
|
+
* namespace Express {
|
|
21
|
+
* interface Request {
|
|
22
|
+
* auth?: AccessToken;
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
* We ship the interface-extension form rather than augmenting the global
|
|
28
|
+
* namespace by default. Augmentation makes `req.auth` optional on every
|
|
29
|
+
* request including unauthenticated routes, which weakens the type
|
|
30
|
+
* contract. Use it when you prefer convenience over strictness.
|
|
31
|
+
* See: https://github.com/auth0/express-jwt/issues/311
|
|
32
|
+
*/
|
|
33
|
+
export interface AuthenticatedRequest extends Request {
|
|
34
|
+
auth: AccessToken;
|
|
35
|
+
}
|
|
36
|
+
export type BearerAuthOptions = {
|
|
37
|
+
verifier: TokenVerifier;
|
|
38
|
+
requiredScopes?: readonly string[];
|
|
39
|
+
} | {
|
|
40
|
+
/**
|
|
41
|
+
* Keycard zone URL, e.g. "https://zone-id.keycard.cloud".
|
|
42
|
+
* Either `zoneUrl` or `zoneId` is required (consistent with `grant()`).
|
|
43
|
+
*/
|
|
44
|
+
zoneUrl?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Keycard zone ID. Constructs the URL as `https://{zoneId}.keycard.cloud`.
|
|
47
|
+
* Either `zoneUrl` or `zoneId` is required (consistent with `grant()`).
|
|
48
|
+
*/
|
|
49
|
+
zoneId?: string;
|
|
50
|
+
audience?: string;
|
|
51
|
+
enableMultiZone?: boolean;
|
|
52
|
+
keyring?: TokenVerifierOptions["keyring"];
|
|
53
|
+
requiredScopes?: readonly string[];
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Express middleware that validates a Bearer token (RFC 6750) and sets
|
|
57
|
+
* `req.auth` to the verified `AccessToken`.
|
|
58
|
+
*
|
|
59
|
+
* On failure: responds with a `WWW-Authenticate` challenge containing the
|
|
60
|
+
* `resource_metadata` URL per RFC 9728 §3.
|
|
61
|
+
*
|
|
62
|
+
* Usage with a zone URL:
|
|
63
|
+
* ```ts
|
|
64
|
+
* app.use(requireBearerAuth({ zoneUrl: "https://zone.keycard.cloud" }));
|
|
65
|
+
* // or by zone ID
|
|
66
|
+
* app.use(requireBearerAuth({ zoneId: "zone-id" }));
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* Usage with a pre-built verifier (shared across routes):
|
|
70
|
+
* ```ts
|
|
71
|
+
* const verifier = new TokenVerifier({ issuer: "https://zone.keycard.cloud" });
|
|
72
|
+
* app.use(requireBearerAuth({ verifier }));
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function requireBearerAuth(options: BearerAuthOptions): RequestHandler;
|
|
76
|
+
//# sourceMappingURL=bearerAuth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bearerAuth.d.ts","sourceRoot":"","sources":["../../src/bearerAuth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA0B,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AACtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAClF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AASvE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,oBAAqB,SAAQ,OAAO;IACnD,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC/D;IACE;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC;IAC1C,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC,CAAC;AAEN;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAgG5E"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { TokenVerifier } from "@keycardai/oauth/server/tokenVerifier";
|
|
2
|
+
import { BadRequestError, UnauthorizedError, InvalidTokenError, InsufficientScopeError, OAuthError, } from "@keycardai/oauth/errors";
|
|
3
|
+
/**
|
|
4
|
+
* Express middleware that validates a Bearer token (RFC 6750) and sets
|
|
5
|
+
* `req.auth` to the verified `AccessToken`.
|
|
6
|
+
*
|
|
7
|
+
* On failure: responds with a `WWW-Authenticate` challenge containing the
|
|
8
|
+
* `resource_metadata` URL per RFC 9728 §3.
|
|
9
|
+
*
|
|
10
|
+
* Usage with a zone URL:
|
|
11
|
+
* ```ts
|
|
12
|
+
* app.use(requireBearerAuth({ zoneUrl: "https://zone.keycard.cloud" }));
|
|
13
|
+
* // or by zone ID
|
|
14
|
+
* app.use(requireBearerAuth({ zoneId: "zone-id" }));
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Usage with a pre-built verifier (shared across routes):
|
|
18
|
+
* ```ts
|
|
19
|
+
* const verifier = new TokenVerifier({ issuer: "https://zone.keycard.cloud" });
|
|
20
|
+
* app.use(requireBearerAuth({ verifier }));
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function requireBearerAuth(options) {
|
|
24
|
+
// Do not pass requiredScopes to TokenVerifier: it returns null on scope
|
|
25
|
+
// failure, which the middleware would interpret as a generic 401. The
|
|
26
|
+
// explicit scope check below produces the correct 403 InsufficientScopeError.
|
|
27
|
+
let verifier;
|
|
28
|
+
if ("verifier" in options) {
|
|
29
|
+
verifier = options.verifier;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const issuer = options.zoneUrl ?? buildIssuerFromZoneId(options.zoneId);
|
|
33
|
+
if (!issuer) {
|
|
34
|
+
throw new Error("requireBearerAuth: either `zoneUrl` or `zoneId` is required");
|
|
35
|
+
}
|
|
36
|
+
verifier = new TokenVerifier({
|
|
37
|
+
issuer,
|
|
38
|
+
audience: options.audience,
|
|
39
|
+
enableMultiZone: options.enableMultiZone,
|
|
40
|
+
keyring: options.keyring,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return async (req, res, next) => {
|
|
44
|
+
const resourceMetadataUrl = getResourceMetadataUrl(req);
|
|
45
|
+
try {
|
|
46
|
+
const authorization = req.headers.authorization;
|
|
47
|
+
if (!authorization) {
|
|
48
|
+
throw new UnauthorizedError("No credentials");
|
|
49
|
+
}
|
|
50
|
+
const [scheme, token] = authorization.split(" ");
|
|
51
|
+
if (!token) {
|
|
52
|
+
throw new BadRequestError("Malformed credentials");
|
|
53
|
+
}
|
|
54
|
+
if (scheme.toLowerCase() !== "bearer") {
|
|
55
|
+
throw new InvalidTokenError("Unsupported authentication scheme");
|
|
56
|
+
}
|
|
57
|
+
const accessToken = await verifier.verifyToken(token);
|
|
58
|
+
if (!accessToken) {
|
|
59
|
+
throw new InvalidTokenError("Token validation failed");
|
|
60
|
+
}
|
|
61
|
+
// Validate resource audience: a token scoped to a different resource
|
|
62
|
+
// server must not be accepted here. Compare origins so path and query
|
|
63
|
+
// string differences are ignored (mirrors Workers auth.ts:88-92).
|
|
64
|
+
if (accessToken.resource) {
|
|
65
|
+
const requestOrigin = `${req.protocol}://${req.host}`;
|
|
66
|
+
try {
|
|
67
|
+
const tokenOrigin = new URL(accessToken.resource).origin;
|
|
68
|
+
if (tokenOrigin !== requestOrigin) {
|
|
69
|
+
throw new InvalidTokenError("Token not intended for resource");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
if (e instanceof InvalidTokenError)
|
|
74
|
+
throw e;
|
|
75
|
+
// resource claim is not a URL; opaque audience, skip origin check
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if ("requiredScopes" in options &&
|
|
79
|
+
options.requiredScopes &&
|
|
80
|
+
options.requiredScopes.length > 0) {
|
|
81
|
+
const hasAllScopes = options.requiredScopes.every((scope) => accessToken.scopes.includes(scope));
|
|
82
|
+
if (!hasAllScopes) {
|
|
83
|
+
throw new InsufficientScopeError("Insufficient scope");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
req.auth = accessToken;
|
|
87
|
+
next();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof BadRequestError) {
|
|
91
|
+
res.status(400).end();
|
|
92
|
+
}
|
|
93
|
+
else if (error instanceof UnauthorizedError) {
|
|
94
|
+
res.set("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
|
|
95
|
+
res.status(401).end();
|
|
96
|
+
}
|
|
97
|
+
else if (error instanceof InsufficientScopeError) {
|
|
98
|
+
res.set("WWW-Authenticate", `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`);
|
|
99
|
+
res.status(403).end();
|
|
100
|
+
}
|
|
101
|
+
else if (error instanceof OAuthError || error instanceof InvalidTokenError) {
|
|
102
|
+
res.set("WWW-Authenticate", `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`);
|
|
103
|
+
res.status(401).end();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
next(error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function getResourceMetadataUrl(req) {
|
|
112
|
+
const origin = `${req.protocol}://${req.host}`;
|
|
113
|
+
return `${origin}/.well-known/oauth-protected-resource`;
|
|
114
|
+
}
|
|
115
|
+
function buildIssuerFromZoneId(zoneId) {
|
|
116
|
+
if (!zoneId)
|
|
117
|
+
return undefined;
|
|
118
|
+
return `https://${zoneId}.keycard.cloud`;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=bearerAuth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bearerAuth.js","sourceRoot":"","sources":["../../src/bearerAuth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RequestHandler } from "express";
|
|
2
|
+
import { AccessContext } from "@keycardai/oauth/server/accessContext";
|
|
3
|
+
import type { ApplicationCredential } from "@keycardai/oauth/credentials";
|
|
4
|
+
import type { AuthenticatedRequest } from "./bearerAuth.js";
|
|
5
|
+
export interface GrantedRequest extends AuthenticatedRequest {
|
|
6
|
+
accessContext: AccessContext;
|
|
7
|
+
}
|
|
8
|
+
export interface GrantOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Keycard zone URL, e.g. "https://zone-id.keycard.cloud".
|
|
11
|
+
* Either `zoneUrl` or `zoneId` is required.
|
|
12
|
+
*/
|
|
13
|
+
zoneUrl?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Keycard zone ID. Constructs the zone URL as
|
|
16
|
+
* `https://{zoneId}.keycard.cloud`.
|
|
17
|
+
*/
|
|
18
|
+
zoneId?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Application credential provider for authenticated token exchange.
|
|
21
|
+
* When omitted, the bearer token is exchanged without client auth.
|
|
22
|
+
*/
|
|
23
|
+
applicationCredential?: ApplicationCredential;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Express middleware factory for delegated token exchange (RFC 8693).
|
|
27
|
+
*
|
|
28
|
+
* Must run AFTER `requireBearerAuth()`. Reads the verified bearer token
|
|
29
|
+
* from `req.auth`, exchanges it for per-resource access tokens at the
|
|
30
|
+
* Keycard zone, and stores the results in `req.accessContext`.
|
|
31
|
+
*
|
|
32
|
+
* On success, `req.accessContext.access(resourceUrl)` returns the
|
|
33
|
+
* `TokenResponse` for that resource. On partial failure, some resources
|
|
34
|
+
* may have errors while others succeed.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* app.use(requireBearerAuth({ issuer: "https://zone.keycard.cloud" }));
|
|
38
|
+
* app.use(grant(["https://graph.microsoft.com"], { zoneUrl: "https://zone.keycard.cloud" }));
|
|
39
|
+
* app.get("/data", (req, res) => {
|
|
40
|
+
* const token = req.accessContext.access("https://graph.microsoft.com");
|
|
41
|
+
* // ...
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function grant(resources: string | readonly string[], options: GrantOptions): RequestHandler;
|
|
46
|
+
//# sourceMappingURL=grant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../src/grant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmC,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAG5D,MAAM,WAAW,cAAe,SAAQ,oBAAoB;IAC1D,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CAC/C;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,KAAK,CACnB,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACrC,OAAO,EAAE,YAAY,GACpB,cAAc,CA2EhB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { TokenExchangeClient } from "@keycardai/oauth/tokenExchange";
|
|
2
|
+
import { AccessContext } from "@keycardai/oauth/server/accessContext";
|
|
3
|
+
import { OAuthError, AuthProviderConfigurationError } from "@keycardai/oauth/errors";
|
|
4
|
+
/**
|
|
5
|
+
* Express middleware factory for delegated token exchange (RFC 8693).
|
|
6
|
+
*
|
|
7
|
+
* Must run AFTER `requireBearerAuth()`. Reads the verified bearer token
|
|
8
|
+
* from `req.auth`, exchanges it for per-resource access tokens at the
|
|
9
|
+
* Keycard zone, and stores the results in `req.accessContext`.
|
|
10
|
+
*
|
|
11
|
+
* On success, `req.accessContext.access(resourceUrl)` returns the
|
|
12
|
+
* `TokenResponse` for that resource. On partial failure, some resources
|
|
13
|
+
* may have errors while others succeed.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* app.use(requireBearerAuth({ issuer: "https://zone.keycard.cloud" }));
|
|
17
|
+
* app.use(grant(["https://graph.microsoft.com"], { zoneUrl: "https://zone.keycard.cloud" }));
|
|
18
|
+
* app.get("/data", (req, res) => {
|
|
19
|
+
* const token = req.accessContext.access("https://graph.microsoft.com");
|
|
20
|
+
* // ...
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function grant(resources, options) {
|
|
25
|
+
const zoneUrl = options.zoneUrl ?? buildZoneUrl(options.zoneId);
|
|
26
|
+
if (!zoneUrl) {
|
|
27
|
+
throw new AuthProviderConfigurationError("grant: either `zoneUrl` or `zoneId` is required");
|
|
28
|
+
}
|
|
29
|
+
return async (req, _res, next) => {
|
|
30
|
+
const authReq = req;
|
|
31
|
+
const subjectToken = authReq.auth?.token;
|
|
32
|
+
const accessCtx = new AccessContext();
|
|
33
|
+
if (!subjectToken) {
|
|
34
|
+
accessCtx.setError({
|
|
35
|
+
message: "No authentication token. Ensure requireBearerAuth() runs before grant().",
|
|
36
|
+
});
|
|
37
|
+
req.accessContext = accessCtx;
|
|
38
|
+
return next();
|
|
39
|
+
}
|
|
40
|
+
let client;
|
|
41
|
+
try {
|
|
42
|
+
const auth = options.applicationCredential?.getAuth();
|
|
43
|
+
client = new TokenExchangeClient(zoneUrl, auth ?? undefined);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
accessCtx.setError({
|
|
47
|
+
message: "Failed to initialize token exchange client.",
|
|
48
|
+
rawError: String(e),
|
|
49
|
+
});
|
|
50
|
+
req.accessContext = accessCtx;
|
|
51
|
+
return next();
|
|
52
|
+
}
|
|
53
|
+
const resourceList = Array.isArray(resources)
|
|
54
|
+
? resources
|
|
55
|
+
: [resources];
|
|
56
|
+
const tokens = {};
|
|
57
|
+
for (const resource of resourceList) {
|
|
58
|
+
try {
|
|
59
|
+
let exchangeRequest;
|
|
60
|
+
if (options.applicationCredential) {
|
|
61
|
+
exchangeRequest = await options.applicationCredential.prepareTokenExchangeRequest(subjectToken, resource);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
exchangeRequest = {
|
|
65
|
+
subjectToken,
|
|
66
|
+
resource,
|
|
67
|
+
subjectTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
tokens[resource] = await client.exchangeToken(exchangeRequest);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
const detail = {
|
|
74
|
+
message: `Token exchange failed for ${resource}`,
|
|
75
|
+
};
|
|
76
|
+
if (e instanceof OAuthError) {
|
|
77
|
+
detail.code = e.errorCode;
|
|
78
|
+
detail.description = e.message;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
detail.rawError = String(e);
|
|
82
|
+
}
|
|
83
|
+
accessCtx.setResourceError(resource, detail);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
accessCtx.setBulkTokens(tokens);
|
|
87
|
+
req.accessContext = accessCtx;
|
|
88
|
+
next();
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function buildZoneUrl(zoneId) {
|
|
92
|
+
if (!zoneId)
|
|
93
|
+
return undefined;
|
|
94
|
+
return `https://${zoneId}.keycard.cloud`;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=grant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant.js","sourceRoot":"","sources":["../../src/grant.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { requireBearerAuth } from "./bearerAuth.js";
|
|
2
|
+
export type { AuthenticatedRequest, BearerAuthOptions } from "./bearerAuth.js";
|
|
3
|
+
export { grant } from "./grant.js";
|
|
4
|
+
export type { GrantedRequest, GrantOptions } from "./grant.js";
|
|
5
|
+
export { keycardMetadataRouter } from "./wellKnown.js";
|
|
6
|
+
export type { KeycardRouterOptions } from "./wellKnown.js";
|
|
7
|
+
export { createKeycardMiddleware } from "./middleware.js";
|
|
8
|
+
export type { KeycardMiddlewareOptions, KeycardMiddleware } from "./middleware.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,YAAY,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RequestHandler } from "express";
|
|
2
|
+
import type { ApplicationCredential } from "@keycardai/oauth/credentials";
|
|
3
|
+
export interface KeycardMiddlewareOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Keycard zone URL, e.g. "https://zone-id.keycard.cloud".
|
|
6
|
+
* Either `zoneUrl` or `zoneId` is required.
|
|
7
|
+
*/
|
|
8
|
+
zoneUrl?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Keycard zone ID. Constructs the URL as `https://{zoneId}.keycard.cloud`.
|
|
11
|
+
*/
|
|
12
|
+
zoneId?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Application credential for token exchange in `grant()`.
|
|
15
|
+
* Typically a `ClientSecret` from `@keycardai/oauth/server`.
|
|
16
|
+
*/
|
|
17
|
+
applicationCredential?: ApplicationCredential;
|
|
18
|
+
}
|
|
19
|
+
export interface KeycardMiddleware {
|
|
20
|
+
/**
|
|
21
|
+
* Express middleware that validates a Bearer token and sets `req.auth`.
|
|
22
|
+
* Accepts optional `requiredScopes` to enforce at the middleware level.
|
|
23
|
+
*/
|
|
24
|
+
requireBearerAuth(options?: {
|
|
25
|
+
requiredScopes?: readonly string[];
|
|
26
|
+
}): RequestHandler;
|
|
27
|
+
/**
|
|
28
|
+
* Express middleware for delegated RFC 8693 token exchange.
|
|
29
|
+
* Sets `req.accessContext` with per-resource tokens.
|
|
30
|
+
*/
|
|
31
|
+
grant(resources: string | readonly string[], options?: {
|
|
32
|
+
applicationCredential?: ApplicationCredential;
|
|
33
|
+
}): RequestHandler;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates a pair of pre-configured Keycard middleware functions sharing a
|
|
37
|
+
* common zone URL. Eliminates the naming mismatch between `requireBearerAuth`
|
|
38
|
+
* (which takes `issuer`) and `grant` (which takes `zoneUrl`/`zoneId`) by
|
|
39
|
+
* accepting a single consistent config.
|
|
40
|
+
*
|
|
41
|
+
* Python equivalent: `AuthProvider(zone_url=..., application_credential=...)`
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* const keycard = createKeycardMiddleware({
|
|
45
|
+
* zoneUrl: "https://zone.keycard.cloud",
|
|
46
|
+
* applicationCredential: new ClientSecret("client-id", "client-secret"),
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* app.use(keycard.requireBearerAuth());
|
|
50
|
+
* app.use(keycard.grant(["https://graph.microsoft.com"]));
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function createKeycardMiddleware(options: KeycardMiddlewareOptions): KeycardMiddleware;
|
|
54
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CAC/C;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,iBAAiB,CAAC,OAAO,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAAG,cAAc,CAAC;IACpF;;;OAGG;IACH,KAAK,CACH,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACrC,OAAO,CAAC,EAAE;QACR,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;KAC/C,GACA,cAAc,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CAsB5F"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { requireBearerAuth } from "./bearerAuth.js";
|
|
2
|
+
import { grant } from "./grant.js";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a pair of pre-configured Keycard middleware functions sharing a
|
|
5
|
+
* common zone URL. Eliminates the naming mismatch between `requireBearerAuth`
|
|
6
|
+
* (which takes `issuer`) and `grant` (which takes `zoneUrl`/`zoneId`) by
|
|
7
|
+
* accepting a single consistent config.
|
|
8
|
+
*
|
|
9
|
+
* Python equivalent: `AuthProvider(zone_url=..., application_credential=...)`
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const keycard = createKeycardMiddleware({
|
|
13
|
+
* zoneUrl: "https://zone.keycard.cloud",
|
|
14
|
+
* applicationCredential: new ClientSecret("client-id", "client-secret"),
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* app.use(keycard.requireBearerAuth());
|
|
18
|
+
* app.use(keycard.grant(["https://graph.microsoft.com"]));
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function createKeycardMiddleware(options) {
|
|
22
|
+
const zoneUrl = options.zoneUrl ?? buildZoneUrl(options.zoneId);
|
|
23
|
+
if (!zoneUrl) {
|
|
24
|
+
throw new Error("createKeycardMiddleware: either `zoneUrl` or `zoneId` is required");
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
requireBearerAuth(localOptions) {
|
|
28
|
+
return requireBearerAuth({
|
|
29
|
+
zoneUrl,
|
|
30
|
+
requiredScopes: localOptions?.requiredScopes,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
grant(resources, localOptions) {
|
|
34
|
+
return grant(resources, {
|
|
35
|
+
zoneUrl,
|
|
36
|
+
applicationCredential: localOptions?.applicationCredential ?? options.applicationCredential,
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function buildZoneUrl(zoneId) {
|
|
42
|
+
if (!zoneId)
|
|
43
|
+
return undefined;
|
|
44
|
+
return `https://${zoneId}.keycard.cloud`;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAsCnC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAiC;IACvE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;IACvF,CAAC;IAED,OAAO;QACL,iBAAiB,CAAC,YAAqD;YACrE,OAAO,iBAAiB,CAAC;gBACvB,OAAO;gBACP,cAAc,EAAE,YAAY,EAAE,cAAc;aAC7C,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,SAAS,EAAE,YAAY;YAC3B,OAAO,KAAK,CAAC,SAAS,EAAE;gBACtB,OAAO;gBACP,qBAAqB,EACnB,YAAY,EAAE,qBAAqB,IAAI,OAAO,CAAC,qBAAqB;aACvE,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,MAAe;IACnC,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,OAAO,WAAW,MAAM,gBAAgB,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type": "module"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
export interface KeycardRouterOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Keycard issuer URL, e.g. "https://zone-id.keycard.cloud".
|
|
5
|
+
* Used to proxy AS metadata from the Keycard authorization server.
|
|
6
|
+
*/
|
|
7
|
+
issuer: string;
|
|
8
|
+
/**
|
|
9
|
+
* Human-readable resource name shown in AS metadata.
|
|
10
|
+
*/
|
|
11
|
+
resourceName?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Scopes this resource server supports.
|
|
14
|
+
*/
|
|
15
|
+
scopesSupported?: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* Link to documentation for this resource.
|
|
18
|
+
*/
|
|
19
|
+
resourceDocumentation?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Timeout in milliseconds for the upstream AS metadata fetch.
|
|
22
|
+
* Default: 10 000 ms.
|
|
23
|
+
*/
|
|
24
|
+
asMetadataTimeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Returns an Express Router that serves the two OAuth discovery endpoints
|
|
28
|
+
* required by RFC 9728 and RFC 8414:
|
|
29
|
+
*
|
|
30
|
+
* - `GET /.well-known/oauth-protected-resource` (RFC 9728 §2)
|
|
31
|
+
* - `GET /.well-known/oauth-authorization-server` (RFC 8414 §3, proxied)
|
|
32
|
+
*
|
|
33
|
+
* Mount it at the application root:
|
|
34
|
+
* ```ts
|
|
35
|
+
* import express from "express";
|
|
36
|
+
* import { keycardMetadataRouter } from "@keycardai/express";
|
|
37
|
+
*
|
|
38
|
+
* const app = express();
|
|
39
|
+
* app.use(keycardMetadataRouter({ issuer: "https://zone.keycard.cloud" }));
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* These paths must remain publicly accessible (no bearer auth) per their
|
|
43
|
+
* respective specs. Per the security guidance in
|
|
44
|
+
* `@keycardai/starlette` and the feedback_specific_path_bypass rule:
|
|
45
|
+
* only bypass auth for these exact paths, never a broad `/.well-known/` prefix.
|
|
46
|
+
*/
|
|
47
|
+
export declare function keycardMetadataRouter(options: KeycardRouterOptions): Router;
|
|
48
|
+
//# sourceMappingURL=wellKnown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wellKnown.d.ts","sourceRoot":"","sources":["../../src/wellKnown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;OAEG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CAc3E"}
|