@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
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @keycardai/express
|
|
2
|
+
|
|
3
|
+
> **Preview.** This SDK has not reached parity with the Keycard Python SDK. APIs may change between minor versions.
|
|
4
|
+
|
|
5
|
+
Keycard auth middleware for Express. Wraps Express's standard middleware idioms for protecting HTTP APIs with Keycard: bearer token validation (RFC 6750), delegated token exchange (RFC 8693), and OAuth discovery routes (RFC 9728 + RFC 8414).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @keycardai/express express
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Protect routes with `requireBearerAuth`
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import express from "express";
|
|
19
|
+
import { requireBearerAuth } from "@keycardai/express";
|
|
20
|
+
|
|
21
|
+
const app = express();
|
|
22
|
+
|
|
23
|
+
app.use(requireBearerAuth({ issuer: "https://your-zone.keycard.cloud" }));
|
|
24
|
+
|
|
25
|
+
app.get("/api/data", (req, res) => {
|
|
26
|
+
// req.auth is AccessToken: { token, clientId, scopes, ... }
|
|
27
|
+
res.json({ clientId: req.auth.clientId });
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Delegate tokens with `grant`
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { requireBearerAuth, grant } from "@keycardai/express";
|
|
35
|
+
import { ClientSecret } from "@keycardai/oauth/server";
|
|
36
|
+
|
|
37
|
+
const credential = new ClientSecret("your-client-id", "your-client-secret");
|
|
38
|
+
|
|
39
|
+
app.use(requireBearerAuth({ issuer: "https://your-zone.keycard.cloud" }));
|
|
40
|
+
app.use(grant(["https://graph.microsoft.com"], {
|
|
41
|
+
zoneUrl: "https://your-zone.keycard.cloud",
|
|
42
|
+
applicationCredential: credential,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
app.get("/api/email", async (req, res) => {
|
|
46
|
+
const token = req.accessContext.access("https://graph.microsoft.com");
|
|
47
|
+
// use token.accessToken to call Graph API
|
|
48
|
+
res.json({ ok: true });
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Add OAuth discovery routes
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { keycardMetadataRouter } from "@keycardai/express";
|
|
56
|
+
|
|
57
|
+
app.use(keycardMetadataRouter({ issuer: "https://your-zone.keycard.cloud" }));
|
|
58
|
+
// Serves:
|
|
59
|
+
// GET /.well-known/oauth-protected-resource (RFC 9728)
|
|
60
|
+
// GET /.well-known/oauth-authorization-server (RFC 8414, proxied)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
| Export | Description |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `requireBearerAuth(options)` | Middleware factory that validates a Bearer token and sets `req.auth: AccessToken`. Returns 401 with RFC 6750 `WWW-Authenticate` challenge on failure. |
|
|
68
|
+
| `grant(resources, options)` | Middleware factory that exchanges the bearer token for per-resource access tokens and sets `req.accessContext: AccessContext`. Must run after `requireBearerAuth`. |
|
|
69
|
+
| `keycardMetadataRouter(options)` | Returns an Express Router with `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server` routes. |
|
|
70
|
+
| `AuthenticatedRequest` | `Request` extended with `auth: AccessToken`. |
|
|
71
|
+
| `GrantedRequest` | `Request` extended with `auth: AccessToken` and `accessContext: AccessContext`. |
|
|
72
|
+
|
|
73
|
+
## Related Packages
|
|
74
|
+
|
|
75
|
+
- [`@keycardai/oauth`](../oauth/) — Framework-free OAuth primitives this package builds on
|
|
76
|
+
- [`@keycardai/mcp`](../mcp/) — MCP-specific OAuth integration
|
|
77
|
+
- [Keycard TypeScript SDK](../../README.md) — Root documentation
|
|
@@ -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,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireBearerAuth = requireBearerAuth;
|
|
4
|
+
const tokenVerifier_1 = require("@keycardai/oauth/server/tokenVerifier");
|
|
5
|
+
const errors_1 = require("@keycardai/oauth/errors");
|
|
6
|
+
/**
|
|
7
|
+
* Express middleware that validates a Bearer token (RFC 6750) and sets
|
|
8
|
+
* `req.auth` to the verified `AccessToken`.
|
|
9
|
+
*
|
|
10
|
+
* On failure: responds with a `WWW-Authenticate` challenge containing the
|
|
11
|
+
* `resource_metadata` URL per RFC 9728 §3.
|
|
12
|
+
*
|
|
13
|
+
* Usage with a zone URL:
|
|
14
|
+
* ```ts
|
|
15
|
+
* app.use(requireBearerAuth({ zoneUrl: "https://zone.keycard.cloud" }));
|
|
16
|
+
* // or by zone ID
|
|
17
|
+
* app.use(requireBearerAuth({ zoneId: "zone-id" }));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Usage with a pre-built verifier (shared across routes):
|
|
21
|
+
* ```ts
|
|
22
|
+
* const verifier = new TokenVerifier({ issuer: "https://zone.keycard.cloud" });
|
|
23
|
+
* app.use(requireBearerAuth({ verifier }));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function requireBearerAuth(options) {
|
|
27
|
+
// Do not pass requiredScopes to TokenVerifier: it returns null on scope
|
|
28
|
+
// failure, which the middleware would interpret as a generic 401. The
|
|
29
|
+
// explicit scope check below produces the correct 403 InsufficientScopeError.
|
|
30
|
+
let verifier;
|
|
31
|
+
if ("verifier" in options) {
|
|
32
|
+
verifier = options.verifier;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const issuer = options.zoneUrl ?? buildIssuerFromZoneId(options.zoneId);
|
|
36
|
+
if (!issuer) {
|
|
37
|
+
throw new Error("requireBearerAuth: either `zoneUrl` or `zoneId` is required");
|
|
38
|
+
}
|
|
39
|
+
verifier = new tokenVerifier_1.TokenVerifier({
|
|
40
|
+
issuer,
|
|
41
|
+
audience: options.audience,
|
|
42
|
+
enableMultiZone: options.enableMultiZone,
|
|
43
|
+
keyring: options.keyring,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return async (req, res, next) => {
|
|
47
|
+
const resourceMetadataUrl = getResourceMetadataUrl(req);
|
|
48
|
+
try {
|
|
49
|
+
const authorization = req.headers.authorization;
|
|
50
|
+
if (!authorization) {
|
|
51
|
+
throw new errors_1.UnauthorizedError("No credentials");
|
|
52
|
+
}
|
|
53
|
+
const [scheme, token] = authorization.split(" ");
|
|
54
|
+
if (!token) {
|
|
55
|
+
throw new errors_1.BadRequestError("Malformed credentials");
|
|
56
|
+
}
|
|
57
|
+
if (scheme.toLowerCase() !== "bearer") {
|
|
58
|
+
throw new errors_1.InvalidTokenError("Unsupported authentication scheme");
|
|
59
|
+
}
|
|
60
|
+
const accessToken = await verifier.verifyToken(token);
|
|
61
|
+
if (!accessToken) {
|
|
62
|
+
throw new errors_1.InvalidTokenError("Token validation failed");
|
|
63
|
+
}
|
|
64
|
+
// Validate resource audience: a token scoped to a different resource
|
|
65
|
+
// server must not be accepted here. Compare origins so path and query
|
|
66
|
+
// string differences are ignored (mirrors Workers auth.ts:88-92).
|
|
67
|
+
if (accessToken.resource) {
|
|
68
|
+
const requestOrigin = `${req.protocol}://${req.host}`;
|
|
69
|
+
try {
|
|
70
|
+
const tokenOrigin = new URL(accessToken.resource).origin;
|
|
71
|
+
if (tokenOrigin !== requestOrigin) {
|
|
72
|
+
throw new errors_1.InvalidTokenError("Token not intended for resource");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
if (e instanceof errors_1.InvalidTokenError)
|
|
77
|
+
throw e;
|
|
78
|
+
// resource claim is not a URL; opaque audience, skip origin check
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if ("requiredScopes" in options &&
|
|
82
|
+
options.requiredScopes &&
|
|
83
|
+
options.requiredScopes.length > 0) {
|
|
84
|
+
const hasAllScopes = options.requiredScopes.every((scope) => accessToken.scopes.includes(scope));
|
|
85
|
+
if (!hasAllScopes) {
|
|
86
|
+
throw new errors_1.InsufficientScopeError("Insufficient scope");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
req.auth = accessToken;
|
|
90
|
+
next();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof errors_1.BadRequestError) {
|
|
94
|
+
res.status(400).end();
|
|
95
|
+
}
|
|
96
|
+
else if (error instanceof errors_1.UnauthorizedError) {
|
|
97
|
+
res.set("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
|
|
98
|
+
res.status(401).end();
|
|
99
|
+
}
|
|
100
|
+
else if (error instanceof errors_1.InsufficientScopeError) {
|
|
101
|
+
res.set("WWW-Authenticate", `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`);
|
|
102
|
+
res.status(403).end();
|
|
103
|
+
}
|
|
104
|
+
else if (error instanceof errors_1.OAuthError || error instanceof errors_1.InvalidTokenError) {
|
|
105
|
+
res.set("WWW-Authenticate", `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`);
|
|
106
|
+
res.status(401).end();
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
next(error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function getResourceMetadataUrl(req) {
|
|
115
|
+
const origin = `${req.protocol}://${req.host}`;
|
|
116
|
+
return `${origin}/.well-known/oauth-protected-resource`;
|
|
117
|
+
}
|
|
118
|
+
function buildIssuerFromZoneId(zoneId) {
|
|
119
|
+
if (!zoneId)
|
|
120
|
+
return undefined;
|
|
121
|
+
return `https://${zoneId}.keycard.cloud`;
|
|
122
|
+
}
|
|
123
|
+
//# 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,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.grant = grant;
|
|
4
|
+
const tokenExchange_1 = require("@keycardai/oauth/tokenExchange");
|
|
5
|
+
const accessContext_1 = require("@keycardai/oauth/server/accessContext");
|
|
6
|
+
const errors_1 = require("@keycardai/oauth/errors");
|
|
7
|
+
/**
|
|
8
|
+
* Express middleware factory for delegated token exchange (RFC 8693).
|
|
9
|
+
*
|
|
10
|
+
* Must run AFTER `requireBearerAuth()`. Reads the verified bearer token
|
|
11
|
+
* from `req.auth`, exchanges it for per-resource access tokens at the
|
|
12
|
+
* Keycard zone, and stores the results in `req.accessContext`.
|
|
13
|
+
*
|
|
14
|
+
* On success, `req.accessContext.access(resourceUrl)` returns the
|
|
15
|
+
* `TokenResponse` for that resource. On partial failure, some resources
|
|
16
|
+
* may have errors while others succeed.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* app.use(requireBearerAuth({ issuer: "https://zone.keycard.cloud" }));
|
|
20
|
+
* app.use(grant(["https://graph.microsoft.com"], { zoneUrl: "https://zone.keycard.cloud" }));
|
|
21
|
+
* app.get("/data", (req, res) => {
|
|
22
|
+
* const token = req.accessContext.access("https://graph.microsoft.com");
|
|
23
|
+
* // ...
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
function grant(resources, options) {
|
|
28
|
+
const zoneUrl = options.zoneUrl ?? buildZoneUrl(options.zoneId);
|
|
29
|
+
if (!zoneUrl) {
|
|
30
|
+
throw new errors_1.AuthProviderConfigurationError("grant: either `zoneUrl` or `zoneId` is required");
|
|
31
|
+
}
|
|
32
|
+
return async (req, _res, next) => {
|
|
33
|
+
const authReq = req;
|
|
34
|
+
const subjectToken = authReq.auth?.token;
|
|
35
|
+
const accessCtx = new accessContext_1.AccessContext();
|
|
36
|
+
if (!subjectToken) {
|
|
37
|
+
accessCtx.setError({
|
|
38
|
+
message: "No authentication token. Ensure requireBearerAuth() runs before grant().",
|
|
39
|
+
});
|
|
40
|
+
req.accessContext = accessCtx;
|
|
41
|
+
return next();
|
|
42
|
+
}
|
|
43
|
+
let client;
|
|
44
|
+
try {
|
|
45
|
+
const auth = options.applicationCredential?.getAuth();
|
|
46
|
+
client = new tokenExchange_1.TokenExchangeClient(zoneUrl, auth ?? undefined);
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
accessCtx.setError({
|
|
50
|
+
message: "Failed to initialize token exchange client.",
|
|
51
|
+
rawError: String(e),
|
|
52
|
+
});
|
|
53
|
+
req.accessContext = accessCtx;
|
|
54
|
+
return next();
|
|
55
|
+
}
|
|
56
|
+
const resourceList = Array.isArray(resources)
|
|
57
|
+
? resources
|
|
58
|
+
: [resources];
|
|
59
|
+
const tokens = {};
|
|
60
|
+
for (const resource of resourceList) {
|
|
61
|
+
try {
|
|
62
|
+
let exchangeRequest;
|
|
63
|
+
if (options.applicationCredential) {
|
|
64
|
+
exchangeRequest = await options.applicationCredential.prepareTokenExchangeRequest(subjectToken, resource);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
exchangeRequest = {
|
|
68
|
+
subjectToken,
|
|
69
|
+
resource,
|
|
70
|
+
subjectTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
tokens[resource] = await client.exchangeToken(exchangeRequest);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
const detail = {
|
|
77
|
+
message: `Token exchange failed for ${resource}`,
|
|
78
|
+
};
|
|
79
|
+
if (e instanceof errors_1.OAuthError) {
|
|
80
|
+
detail.code = e.errorCode;
|
|
81
|
+
detail.description = e.message;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
detail.rawError = String(e);
|
|
85
|
+
}
|
|
86
|
+
accessCtx.setResourceError(resource, detail);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
accessCtx.setBulkTokens(tokens);
|
|
90
|
+
req.accessContext = accessCtx;
|
|
91
|
+
next();
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function buildZoneUrl(zoneId) {
|
|
95
|
+
if (!zoneId)
|
|
96
|
+
return undefined;
|
|
97
|
+
return `https://${zoneId}.keycard.cloud`;
|
|
98
|
+
}
|
|
99
|
+
//# 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,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createKeycardMiddleware = exports.keycardMetadataRouter = exports.grant = exports.requireBearerAuth = void 0;
|
|
4
|
+
var bearerAuth_js_1 = require("./bearerAuth.js");
|
|
5
|
+
Object.defineProperty(exports, "requireBearerAuth", { enumerable: true, get: function () { return bearerAuth_js_1.requireBearerAuth; } });
|
|
6
|
+
var grant_js_1 = require("./grant.js");
|
|
7
|
+
Object.defineProperty(exports, "grant", { enumerable: true, get: function () { return grant_js_1.grant; } });
|
|
8
|
+
var wellKnown_js_1 = require("./wellKnown.js");
|
|
9
|
+
Object.defineProperty(exports, "keycardMetadataRouter", { enumerable: true, get: function () { return wellKnown_js_1.keycardMetadataRouter; } });
|
|
10
|
+
var middleware_js_1 = require("./middleware.js");
|
|
11
|
+
Object.defineProperty(exports, "createKeycardMiddleware", { enumerable: true, get: function () { return middleware_js_1.createKeycardMiddleware; } });
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,iDAAoD;AAA3C,kHAAA,iBAAiB,OAAA;AAE1B,uCAAmC;AAA1B,iGAAA,KAAK,OAAA;AAEd,+CAAuD;AAA9C,qHAAA,qBAAqB,OAAA;AAE9B,iDAA0D;AAAjD,wHAAA,uBAAuB,OAAA"}
|
|
@@ -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,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createKeycardMiddleware = createKeycardMiddleware;
|
|
4
|
+
const bearerAuth_js_1 = require("./bearerAuth.js");
|
|
5
|
+
const grant_js_1 = require("./grant.js");
|
|
6
|
+
/**
|
|
7
|
+
* Creates a pair of pre-configured Keycard middleware functions sharing a
|
|
8
|
+
* common zone URL. Eliminates the naming mismatch between `requireBearerAuth`
|
|
9
|
+
* (which takes `issuer`) and `grant` (which takes `zoneUrl`/`zoneId`) by
|
|
10
|
+
* accepting a single consistent config.
|
|
11
|
+
*
|
|
12
|
+
* Python equivalent: `AuthProvider(zone_url=..., application_credential=...)`
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const keycard = createKeycardMiddleware({
|
|
16
|
+
* zoneUrl: "https://zone.keycard.cloud",
|
|
17
|
+
* applicationCredential: new ClientSecret("client-id", "client-secret"),
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* app.use(keycard.requireBearerAuth());
|
|
21
|
+
* app.use(keycard.grant(["https://graph.microsoft.com"]));
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function createKeycardMiddleware(options) {
|
|
25
|
+
const zoneUrl = options.zoneUrl ?? buildZoneUrl(options.zoneId);
|
|
26
|
+
if (!zoneUrl) {
|
|
27
|
+
throw new Error("createKeycardMiddleware: either `zoneUrl` or `zoneId` is required");
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
requireBearerAuth(localOptions) {
|
|
31
|
+
return (0, bearerAuth_js_1.requireBearerAuth)({
|
|
32
|
+
zoneUrl,
|
|
33
|
+
requiredScopes: localOptions?.requiredScopes,
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
grant(resources, localOptions) {
|
|
37
|
+
return (0, grant_js_1.grant)(resources, {
|
|
38
|
+
zoneUrl,
|
|
39
|
+
applicationCredential: localOptions?.applicationCredential ?? options.applicationCredential,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function buildZoneUrl(zoneId) {
|
|
45
|
+
if (!zoneId)
|
|
46
|
+
return undefined;
|
|
47
|
+
return `https://${zoneId}.keycard.cloud`;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":";;AA0DA,0DAsBC;AA/ED,mDAAoD;AACpD,yCAAmC;AAsCnC;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,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,IAAA,iCAAiB,EAAC;gBACvB,OAAO;gBACP,cAAc,EAAE,YAAY,EAAE,cAAc;aAC7C,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,SAAS,EAAE,YAAY;YAC3B,OAAO,IAAA,gBAAK,EAAC,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": "commonjs"}
|
|
@@ -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"}
|