@keycardai/oauth 0.5.0 → 0.6.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 CHANGED
@@ -157,6 +157,40 @@ returns the issued client credentials. Throws `OAuthError` on RFC 6749 §5.2
157
157
  error responses, a plain `Error` on missing `registration_endpoint` or
158
158
  non-OAuth HTTP failures.
159
159
 
160
+ ### PKCE (RFC 7636)
161
+
162
+ ```typescript
163
+ import {
164
+ generateCodeVerifier,
165
+ generateCodeChallenge,
166
+ generatePkcePair,
167
+ exchangeAuthorizationCode,
168
+ authenticate,
169
+ } from "@keycardai/oauth/pkce";
170
+
171
+ // Generate primitives for a custom auth-code flow
172
+ const { codeVerifier, codeChallenge } = await generatePkcePair();
173
+
174
+ // Exchange code received at the redirect URI
175
+ const tokens = await exchangeAuthorizationCode(
176
+ "https://your-zone.keycard.cloud",
177
+ authorizationCode,
178
+ { codeVerifier, redirectUri: "https://app.example.com/callback", clientId: "my-client" },
179
+ );
180
+
181
+ // Or let authenticate() drive the full flow (Node.js only)
182
+ const tokens2 = await authenticate("https://your-zone.keycard.cloud", {
183
+ clientId: "my-client",
184
+ scopes: ["read", "write"],
185
+ });
186
+ ```
187
+
188
+ `generateCodeVerifier` and `generateCodeChallenge` use the global `crypto` API and
189
+ are runtime-agnostic (Node.js, Cloudflare Workers, browser). `authenticate()` drives
190
+ the full browser-launch and loopback-callback flow and **requires Node.js** — it uses
191
+ dynamic imports of `node:http` and `node:child_process` and will throw a runtime error
192
+ if called in a non-Node environment.
193
+
160
194
  ## API Overview
161
195
 
162
196
  ### JWKS Key Management
@@ -185,6 +219,17 @@ non-OAuth HTTP failures.
185
219
  | `buildSubstituteUserToken` | `@keycardai/oauth/jwt/substituteUser` | Builds the unsigned subject JWT for impersonation calls |
186
220
  | `registerClient` | `@keycardai/oauth/registration` | RFC 7591 dynamic client registration with auto-discovery |
187
221
 
222
+ ### PKCE (RFC 7636)
223
+
224
+ | Export | Import Path | Description |
225
+ |---|---|---|
226
+ | `generateCodeVerifier` | `@keycardai/oauth/pkce` | Generates a 43-char random code verifier (RFC 7636 §4.1) |
227
+ | `generateCodeChallenge` | `@keycardai/oauth/pkce` | Computes S256 or plain code challenge from a verifier (RFC 7636 §4.2) |
228
+ | `generatePkcePair` | `@keycardai/oauth/pkce` | Convenience: generates verifier + challenge in one call |
229
+ | `exchangeAuthorizationCode` | `@keycardai/oauth/pkce` | Exchanges an authorization code with code_verifier at the token endpoint |
230
+ | `authenticate` | `@keycardai/oauth/pkce` | Full browser-launch and loopback-callback flow. **Node.js only** |
231
+ | `Pkce` (type) | `@keycardai/oauth/pkce` | `{ codeVerifier, codeChallenge, codeChallengeMethod }` |
232
+
188
233
  ### Server-tier Primitives
189
234
 
190
235
  | Export | Import Path | Description |
@@ -0,0 +1,64 @@
1
+ import type { TokenResponse } from "./tokenExchange.js";
2
+ export interface Pkce {
3
+ codeVerifier: string;
4
+ codeChallenge: string;
5
+ codeChallengeMethod: "S256" | "plain";
6
+ }
7
+ /**
8
+ * Generate a cryptographically random PKCE code verifier (RFC 7636 §4.1).
9
+ *
10
+ * Returns a 43-character base64url string (32 random bytes). Runtime-agnostic:
11
+ * uses the global `crypto.getRandomValues` which is available in Node 19+,
12
+ * Cloudflare Workers, and browsers.
13
+ */
14
+ export declare function generateCodeVerifier(): string;
15
+ /**
16
+ * Derive a PKCE code challenge from a code verifier (RFC 7636 §4.2).
17
+ *
18
+ * S256 (default): `BASE64URL(SHA-256(ASCII(code_verifier)))`
19
+ * plain: returns the verifier unchanged (not recommended; use only when
20
+ * the AS does not support S256).
21
+ */
22
+ export declare function generateCodeChallenge(verifier: string, method?: "S256" | "plain"): Promise<string>;
23
+ /**
24
+ * Generate a PKCE pair (verifier + challenge) in one call.
25
+ */
26
+ export declare function generatePkcePair(method?: "S256" | "plain"): Promise<Pkce>;
27
+ export interface ExchangeAuthorizationCodeOptions {
28
+ codeVerifier: string;
29
+ redirectUri: string;
30
+ clientId?: string;
31
+ clientSecret?: string;
32
+ signal?: AbortSignal;
33
+ }
34
+ /**
35
+ * Exchange an authorization code for tokens (RFC 6749 §4.1.3 + RFC 7636).
36
+ *
37
+ * Discovers `token_endpoint` from the AS metadata, then POSTs
38
+ * `grant_type=authorization_code` with the code verifier.
39
+ */
40
+ export declare function exchangeAuthorizationCode(issuerUrl: string, code: string, options: ExchangeAuthorizationCodeOptions): Promise<TokenResponse>;
41
+ export interface AuthenticateOptions {
42
+ clientId: string;
43
+ /** Default: "http://localhost:{port}/callback" */
44
+ redirectUri?: string;
45
+ /** Default: 8080 */
46
+ port?: number;
47
+ scopes?: readonly string[];
48
+ clientSecret?: string;
49
+ /** Default: 60_000 ms */
50
+ timeoutMs?: number;
51
+ }
52
+ /**
53
+ * Full authorization-code-with-PKCE flow for local/CLI contexts.
54
+ *
55
+ * Generates a PKCE pair, builds the authorization URL, opens the user's
56
+ * browser, starts a local loopback HTTP server to receive the redirect,
57
+ * and exchanges the authorization code for tokens.
58
+ *
59
+ * **Requires Node.js.** Uses `node:http` and `node:child_process` via
60
+ * dynamic import. Importing this module is safe in any runtime; only
61
+ * *calling* `authenticate()` requires Node.js.
62
+ */
63
+ export declare function authenticate(issuerUrl: string, options: AuthenticateOptions): Promise<TokenResponse>;
64
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/pkce.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,MAAM,WAAW,IAAI;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAI7C;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAM,GAAG,OAAgB,GAChC,OAAO,CAAC,MAAM,CAAC,CASjB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,MAAM,GAAG,OAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvF;AAMD,MAAM,WAAW,gCAAgC;IAC/C,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,aAAa,CAAC,CAyExB;AAMD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAkCxB"}
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.generateCodeVerifier = generateCodeVerifier;
40
+ exports.generateCodeChallenge = generateCodeChallenge;
41
+ exports.generatePkcePair = generatePkcePair;
42
+ exports.exchangeAuthorizationCode = exchangeAuthorizationCode;
43
+ exports.authenticate = authenticate;
44
+ const base64url_js_1 = __importDefault(require("./base64url.js"));
45
+ const discovery_js_1 = require("./discovery.js");
46
+ const errors_js_1 = require("./errors.js");
47
+ /**
48
+ * Generate a cryptographically random PKCE code verifier (RFC 7636 §4.1).
49
+ *
50
+ * Returns a 43-character base64url string (32 random bytes). Runtime-agnostic:
51
+ * uses the global `crypto.getRandomValues` which is available in Node 19+,
52
+ * Cloudflare Workers, and browsers.
53
+ */
54
+ function generateCodeVerifier() {
55
+ const bytes = new Uint8Array(32);
56
+ crypto.getRandomValues(bytes);
57
+ return base64url_js_1.default.encode(bytes.buffer);
58
+ }
59
+ /**
60
+ * Derive a PKCE code challenge from a code verifier (RFC 7636 §4.2).
61
+ *
62
+ * S256 (default): `BASE64URL(SHA-256(ASCII(code_verifier)))`
63
+ * plain: returns the verifier unchanged (not recommended; use only when
64
+ * the AS does not support S256).
65
+ */
66
+ async function generateCodeChallenge(verifier, method = "S256") {
67
+ if (method === "plain") {
68
+ return verifier;
69
+ }
70
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
71
+ return base64url_js_1.default.encode(digest);
72
+ }
73
+ /**
74
+ * Generate a PKCE pair (verifier + challenge) in one call.
75
+ */
76
+ async function generatePkcePair(method = "S256") {
77
+ const codeVerifier = generateCodeVerifier();
78
+ const codeChallenge = await generateCodeChallenge(codeVerifier, method);
79
+ return { codeVerifier, codeChallenge, codeChallengeMethod: method };
80
+ }
81
+ /**
82
+ * Exchange an authorization code for tokens (RFC 6749 §4.1.3 + RFC 7636).
83
+ *
84
+ * Discovers `token_endpoint` from the AS metadata, then POSTs
85
+ * `grant_type=authorization_code` with the code verifier.
86
+ */
87
+ async function exchangeAuthorizationCode(issuerUrl, code, options) {
88
+ const metadata = await (0, discovery_js_1.fetchAuthorizationServerMetadata)(issuerUrl, {
89
+ signal: options.signal,
90
+ });
91
+ if (!metadata.token_endpoint) {
92
+ throw new Error(`Authorization server "${issuerUrl}" does not advertise a token_endpoint`);
93
+ }
94
+ const params = new URLSearchParams();
95
+ params.set("grant_type", "authorization_code");
96
+ params.set("code", code);
97
+ params.set("code_verifier", options.codeVerifier);
98
+ params.set("redirect_uri", options.redirectUri);
99
+ if (options.clientId)
100
+ params.set("client_id", options.clientId);
101
+ const headers = {
102
+ "Content-Type": "application/x-www-form-urlencoded",
103
+ };
104
+ if (options.clientId && options.clientSecret) {
105
+ headers["Authorization"] = `Basic ${btoa(`${options.clientId}:${options.clientSecret}`)}`;
106
+ params.delete("client_id");
107
+ }
108
+ const response = await fetch(metadata.token_endpoint, {
109
+ method: "POST",
110
+ headers,
111
+ body: params.toString(),
112
+ signal: options.signal,
113
+ });
114
+ if (!response.ok) {
115
+ let errorBody = null;
116
+ try {
117
+ const json = await response.json();
118
+ if (json && typeof json === "object" && !Array.isArray(json)) {
119
+ errorBody = json;
120
+ }
121
+ }
122
+ catch {
123
+ // non-JSON error body — fall through to generic error
124
+ }
125
+ if (errorBody && typeof errorBody.error === "string") {
126
+ const description = typeof errorBody.error_description === "string"
127
+ ? errorBody.error_description
128
+ : errorBody.error;
129
+ const errorUri = typeof errorBody.error_uri === "string" ? errorBody.error_uri : undefined;
130
+ throw new errors_js_1.OAuthError(errorBody.error, description, errorUri);
131
+ }
132
+ throw new Error(`Authorization code exchange failed (HTTP ${response.status})`);
133
+ }
134
+ const json = await response.json();
135
+ if (!json || typeof json !== "object" || Array.isArray(json)) {
136
+ throw new Error("Token endpoint response is not a valid JSON object");
137
+ }
138
+ const body = json;
139
+ const accessToken = body.access_token;
140
+ if (typeof accessToken !== "string" || !accessToken) {
141
+ throw new Error("Token endpoint response missing access_token");
142
+ }
143
+ const tokenResponse = {
144
+ accessToken,
145
+ tokenType: typeof body.token_type === "string" ? body.token_type : "bearer",
146
+ };
147
+ if (typeof body.expires_in === "number")
148
+ tokenResponse.expiresIn = body.expires_in;
149
+ if (typeof body.refresh_token === "string")
150
+ tokenResponse.refreshToken = body.refresh_token;
151
+ if (typeof body.scope === "string") {
152
+ tokenResponse.scope = body.scope.split(" ").filter(Boolean);
153
+ }
154
+ return tokenResponse;
155
+ }
156
+ /**
157
+ * Full authorization-code-with-PKCE flow for local/CLI contexts.
158
+ *
159
+ * Generates a PKCE pair, builds the authorization URL, opens the user's
160
+ * browser, starts a local loopback HTTP server to receive the redirect,
161
+ * and exchanges the authorization code for tokens.
162
+ *
163
+ * **Requires Node.js.** Uses `node:http` and `node:child_process` via
164
+ * dynamic import. Importing this module is safe in any runtime; only
165
+ * *calling* `authenticate()` requires Node.js.
166
+ */
167
+ async function authenticate(issuerUrl, options) {
168
+ const port = options.port ?? 8080;
169
+ const redirectUri = options.redirectUri ?? `http://localhost:${port}/callback`;
170
+ const timeoutMs = options.timeoutMs ?? 60_000;
171
+ const { codeVerifier, codeChallenge } = await generatePkcePair("S256");
172
+ const metadata = await (0, discovery_js_1.fetchAuthorizationServerMetadata)(issuerUrl);
173
+ if (!metadata.authorization_endpoint) {
174
+ throw new Error(`Authorization server "${issuerUrl}" does not advertise an authorization_endpoint`);
175
+ }
176
+ const authUrl = new URL(metadata.authorization_endpoint);
177
+ authUrl.searchParams.set("response_type", "code");
178
+ authUrl.searchParams.set("client_id", options.clientId);
179
+ authUrl.searchParams.set("redirect_uri", redirectUri);
180
+ authUrl.searchParams.set("code_challenge", codeChallenge);
181
+ authUrl.searchParams.set("code_challenge_method", "S256");
182
+ if (options.scopes && options.scopes.length > 0) {
183
+ authUrl.searchParams.set("scope", options.scopes.join(" "));
184
+ }
185
+ await openBrowser(authUrl.toString());
186
+ const code = await waitForCode(port, redirectUri, timeoutMs);
187
+ return exchangeAuthorizationCode(issuerUrl, code, {
188
+ codeVerifier,
189
+ redirectUri,
190
+ clientId: options.clientId,
191
+ clientSecret: options.clientSecret,
192
+ });
193
+ }
194
+ async function openBrowser(url) {
195
+ const { execFile } = await Promise.resolve().then(() => __importStar(require("node:child_process")));
196
+ if (process.platform === "darwin") {
197
+ execFile("open", [url]);
198
+ }
199
+ else if (process.platform === "win32") {
200
+ // `start` is a cmd.exe built-in, not a standalone executable.
201
+ execFile("cmd", ["/c", "start", "", url]);
202
+ }
203
+ else {
204
+ execFile("xdg-open", [url]);
205
+ }
206
+ }
207
+ async function waitForCode(port, redirectUri, timeoutMs) {
208
+ // Import before entering the Promise constructor to avoid the async-executor
209
+ // anti-pattern: if the dynamic import throws, the rejection propagates through
210
+ // this async function rather than escaping an async Promise constructor.
211
+ const { createServer } = await Promise.resolve().then(() => __importStar(require("node:http")));
212
+ return new Promise((resolve, reject) => {
213
+ const timer = setTimeout(() => {
214
+ server.close();
215
+ reject(new Error(`PKCE authentication timed out after ${timeoutMs}ms`));
216
+ }, timeoutMs);
217
+ const server = createServer((req, res) => {
218
+ try {
219
+ const reqUrl = new URL(req.url ?? "/", redirectUri);
220
+ const code = reqUrl.searchParams.get("code");
221
+ const error = reqUrl.searchParams.get("error");
222
+ res.writeHead(200, { "Content-Type": "text/html" });
223
+ res.end("<html><body><p>Authentication complete. You can close this tab.</p></body></html>");
224
+ server.close();
225
+ clearTimeout(timer);
226
+ if (error) {
227
+ reject(new errors_js_1.OAuthError(error, reqUrl.searchParams.get("error_description") ?? error));
228
+ }
229
+ else if (code) {
230
+ resolve(code);
231
+ }
232
+ else {
233
+ reject(new Error("No authorization code in redirect"));
234
+ }
235
+ }
236
+ catch (e) {
237
+ server.close();
238
+ clearTimeout(timer);
239
+ reject(e);
240
+ }
241
+ });
242
+ server.listen(port, "localhost");
243
+ server.on("error", (err) => {
244
+ clearTimeout(timer);
245
+ reject(new Error(`Failed to start loopback server on port ${port}: ${err.message}`));
246
+ });
247
+ });
248
+ }
249
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/pkce.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ import type { TokenResponse } from "./tokenExchange.js";
2
+ export interface Pkce {
3
+ codeVerifier: string;
4
+ codeChallenge: string;
5
+ codeChallengeMethod: "S256" | "plain";
6
+ }
7
+ /**
8
+ * Generate a cryptographically random PKCE code verifier (RFC 7636 §4.1).
9
+ *
10
+ * Returns a 43-character base64url string (32 random bytes). Runtime-agnostic:
11
+ * uses the global `crypto.getRandomValues` which is available in Node 19+,
12
+ * Cloudflare Workers, and browsers.
13
+ */
14
+ export declare function generateCodeVerifier(): string;
15
+ /**
16
+ * Derive a PKCE code challenge from a code verifier (RFC 7636 §4.2).
17
+ *
18
+ * S256 (default): `BASE64URL(SHA-256(ASCII(code_verifier)))`
19
+ * plain: returns the verifier unchanged (not recommended; use only when
20
+ * the AS does not support S256).
21
+ */
22
+ export declare function generateCodeChallenge(verifier: string, method?: "S256" | "plain"): Promise<string>;
23
+ /**
24
+ * Generate a PKCE pair (verifier + challenge) in one call.
25
+ */
26
+ export declare function generatePkcePair(method?: "S256" | "plain"): Promise<Pkce>;
27
+ export interface ExchangeAuthorizationCodeOptions {
28
+ codeVerifier: string;
29
+ redirectUri: string;
30
+ clientId?: string;
31
+ clientSecret?: string;
32
+ signal?: AbortSignal;
33
+ }
34
+ /**
35
+ * Exchange an authorization code for tokens (RFC 6749 §4.1.3 + RFC 7636).
36
+ *
37
+ * Discovers `token_endpoint` from the AS metadata, then POSTs
38
+ * `grant_type=authorization_code` with the code verifier.
39
+ */
40
+ export declare function exchangeAuthorizationCode(issuerUrl: string, code: string, options: ExchangeAuthorizationCodeOptions): Promise<TokenResponse>;
41
+ export interface AuthenticateOptions {
42
+ clientId: string;
43
+ /** Default: "http://localhost:{port}/callback" */
44
+ redirectUri?: string;
45
+ /** Default: 8080 */
46
+ port?: number;
47
+ scopes?: readonly string[];
48
+ clientSecret?: string;
49
+ /** Default: 60_000 ms */
50
+ timeoutMs?: number;
51
+ }
52
+ /**
53
+ * Full authorization-code-with-PKCE flow for local/CLI contexts.
54
+ *
55
+ * Generates a PKCE pair, builds the authorization URL, opens the user's
56
+ * browser, starts a local loopback HTTP server to receive the redirect,
57
+ * and exchanges the authorization code for tokens.
58
+ *
59
+ * **Requires Node.js.** Uses `node:http` and `node:child_process` via
60
+ * dynamic import. Importing this module is safe in any runtime; only
61
+ * *calling* `authenticate()` requires Node.js.
62
+ */
63
+ export declare function authenticate(issuerUrl: string, options: AuthenticateOptions): Promise<TokenResponse>;
64
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/pkce.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,MAAM,WAAW,IAAI;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAI7C;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAM,GAAG,OAAgB,GAChC,OAAO,CAAC,MAAM,CAAC,CASjB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,MAAM,GAAG,OAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvF;AAMD,MAAM,WAAW,gCAAgC;IAC/C,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,aAAa,CAAC,CAyExB;AAMD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAkCxB"}
@@ -0,0 +1,206 @@
1
+ import base64url from "./base64url.js";
2
+ import { fetchAuthorizationServerMetadata } from "./discovery.js";
3
+ import { OAuthError } from "./errors.js";
4
+ /**
5
+ * Generate a cryptographically random PKCE code verifier (RFC 7636 §4.1).
6
+ *
7
+ * Returns a 43-character base64url string (32 random bytes). Runtime-agnostic:
8
+ * uses the global `crypto.getRandomValues` which is available in Node 19+,
9
+ * Cloudflare Workers, and browsers.
10
+ */
11
+ export function generateCodeVerifier() {
12
+ const bytes = new Uint8Array(32);
13
+ crypto.getRandomValues(bytes);
14
+ return base64url.encode(bytes.buffer);
15
+ }
16
+ /**
17
+ * Derive a PKCE code challenge from a code verifier (RFC 7636 §4.2).
18
+ *
19
+ * S256 (default): `BASE64URL(SHA-256(ASCII(code_verifier)))`
20
+ * plain: returns the verifier unchanged (not recommended; use only when
21
+ * the AS does not support S256).
22
+ */
23
+ export async function generateCodeChallenge(verifier, method = "S256") {
24
+ if (method === "plain") {
25
+ return verifier;
26
+ }
27
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
28
+ return base64url.encode(digest);
29
+ }
30
+ /**
31
+ * Generate a PKCE pair (verifier + challenge) in one call.
32
+ */
33
+ export async function generatePkcePair(method = "S256") {
34
+ const codeVerifier = generateCodeVerifier();
35
+ const codeChallenge = await generateCodeChallenge(codeVerifier, method);
36
+ return { codeVerifier, codeChallenge, codeChallengeMethod: method };
37
+ }
38
+ /**
39
+ * Exchange an authorization code for tokens (RFC 6749 §4.1.3 + RFC 7636).
40
+ *
41
+ * Discovers `token_endpoint` from the AS metadata, then POSTs
42
+ * `grant_type=authorization_code` with the code verifier.
43
+ */
44
+ export async function exchangeAuthorizationCode(issuerUrl, code, options) {
45
+ const metadata = await fetchAuthorizationServerMetadata(issuerUrl, {
46
+ signal: options.signal,
47
+ });
48
+ if (!metadata.token_endpoint) {
49
+ throw new Error(`Authorization server "${issuerUrl}" does not advertise a token_endpoint`);
50
+ }
51
+ const params = new URLSearchParams();
52
+ params.set("grant_type", "authorization_code");
53
+ params.set("code", code);
54
+ params.set("code_verifier", options.codeVerifier);
55
+ params.set("redirect_uri", options.redirectUri);
56
+ if (options.clientId)
57
+ params.set("client_id", options.clientId);
58
+ const headers = {
59
+ "Content-Type": "application/x-www-form-urlencoded",
60
+ };
61
+ if (options.clientId && options.clientSecret) {
62
+ headers["Authorization"] = `Basic ${btoa(`${options.clientId}:${options.clientSecret}`)}`;
63
+ params.delete("client_id");
64
+ }
65
+ const response = await fetch(metadata.token_endpoint, {
66
+ method: "POST",
67
+ headers,
68
+ body: params.toString(),
69
+ signal: options.signal,
70
+ });
71
+ if (!response.ok) {
72
+ let errorBody = null;
73
+ try {
74
+ const json = await response.json();
75
+ if (json && typeof json === "object" && !Array.isArray(json)) {
76
+ errorBody = json;
77
+ }
78
+ }
79
+ catch {
80
+ // non-JSON error body — fall through to generic error
81
+ }
82
+ if (errorBody && typeof errorBody.error === "string") {
83
+ const description = typeof errorBody.error_description === "string"
84
+ ? errorBody.error_description
85
+ : errorBody.error;
86
+ const errorUri = typeof errorBody.error_uri === "string" ? errorBody.error_uri : undefined;
87
+ throw new OAuthError(errorBody.error, description, errorUri);
88
+ }
89
+ throw new Error(`Authorization code exchange failed (HTTP ${response.status})`);
90
+ }
91
+ const json = await response.json();
92
+ if (!json || typeof json !== "object" || Array.isArray(json)) {
93
+ throw new Error("Token endpoint response is not a valid JSON object");
94
+ }
95
+ const body = json;
96
+ const accessToken = body.access_token;
97
+ if (typeof accessToken !== "string" || !accessToken) {
98
+ throw new Error("Token endpoint response missing access_token");
99
+ }
100
+ const tokenResponse = {
101
+ accessToken,
102
+ tokenType: typeof body.token_type === "string" ? body.token_type : "bearer",
103
+ };
104
+ if (typeof body.expires_in === "number")
105
+ tokenResponse.expiresIn = body.expires_in;
106
+ if (typeof body.refresh_token === "string")
107
+ tokenResponse.refreshToken = body.refresh_token;
108
+ if (typeof body.scope === "string") {
109
+ tokenResponse.scope = body.scope.split(" ").filter(Boolean);
110
+ }
111
+ return tokenResponse;
112
+ }
113
+ /**
114
+ * Full authorization-code-with-PKCE flow for local/CLI contexts.
115
+ *
116
+ * Generates a PKCE pair, builds the authorization URL, opens the user's
117
+ * browser, starts a local loopback HTTP server to receive the redirect,
118
+ * and exchanges the authorization code for tokens.
119
+ *
120
+ * **Requires Node.js.** Uses `node:http` and `node:child_process` via
121
+ * dynamic import. Importing this module is safe in any runtime; only
122
+ * *calling* `authenticate()` requires Node.js.
123
+ */
124
+ export async function authenticate(issuerUrl, options) {
125
+ const port = options.port ?? 8080;
126
+ const redirectUri = options.redirectUri ?? `http://localhost:${port}/callback`;
127
+ const timeoutMs = options.timeoutMs ?? 60_000;
128
+ const { codeVerifier, codeChallenge } = await generatePkcePair("S256");
129
+ const metadata = await fetchAuthorizationServerMetadata(issuerUrl);
130
+ if (!metadata.authorization_endpoint) {
131
+ throw new Error(`Authorization server "${issuerUrl}" does not advertise an authorization_endpoint`);
132
+ }
133
+ const authUrl = new URL(metadata.authorization_endpoint);
134
+ authUrl.searchParams.set("response_type", "code");
135
+ authUrl.searchParams.set("client_id", options.clientId);
136
+ authUrl.searchParams.set("redirect_uri", redirectUri);
137
+ authUrl.searchParams.set("code_challenge", codeChallenge);
138
+ authUrl.searchParams.set("code_challenge_method", "S256");
139
+ if (options.scopes && options.scopes.length > 0) {
140
+ authUrl.searchParams.set("scope", options.scopes.join(" "));
141
+ }
142
+ await openBrowser(authUrl.toString());
143
+ const code = await waitForCode(port, redirectUri, timeoutMs);
144
+ return exchangeAuthorizationCode(issuerUrl, code, {
145
+ codeVerifier,
146
+ redirectUri,
147
+ clientId: options.clientId,
148
+ clientSecret: options.clientSecret,
149
+ });
150
+ }
151
+ async function openBrowser(url) {
152
+ const { execFile } = await import("node:child_process");
153
+ if (process.platform === "darwin") {
154
+ execFile("open", [url]);
155
+ }
156
+ else if (process.platform === "win32") {
157
+ // `start` is a cmd.exe built-in, not a standalone executable.
158
+ execFile("cmd", ["/c", "start", "", url]);
159
+ }
160
+ else {
161
+ execFile("xdg-open", [url]);
162
+ }
163
+ }
164
+ async function waitForCode(port, redirectUri, timeoutMs) {
165
+ // Import before entering the Promise constructor to avoid the async-executor
166
+ // anti-pattern: if the dynamic import throws, the rejection propagates through
167
+ // this async function rather than escaping an async Promise constructor.
168
+ const { createServer } = await import("node:http");
169
+ return new Promise((resolve, reject) => {
170
+ const timer = setTimeout(() => {
171
+ server.close();
172
+ reject(new Error(`PKCE authentication timed out after ${timeoutMs}ms`));
173
+ }, timeoutMs);
174
+ const server = createServer((req, res) => {
175
+ try {
176
+ const reqUrl = new URL(req.url ?? "/", redirectUri);
177
+ const code = reqUrl.searchParams.get("code");
178
+ const error = reqUrl.searchParams.get("error");
179
+ res.writeHead(200, { "Content-Type": "text/html" });
180
+ res.end("<html><body><p>Authentication complete. You can close this tab.</p></body></html>");
181
+ server.close();
182
+ clearTimeout(timer);
183
+ if (error) {
184
+ reject(new OAuthError(error, reqUrl.searchParams.get("error_description") ?? error));
185
+ }
186
+ else if (code) {
187
+ resolve(code);
188
+ }
189
+ else {
190
+ reject(new Error("No authorization code in redirect"));
191
+ }
192
+ }
193
+ catch (e) {
194
+ server.close();
195
+ clearTimeout(timer);
196
+ reject(e);
197
+ }
198
+ });
199
+ server.listen(port, "localhost");
200
+ server.on("error", (err) => {
201
+ clearTimeout(timer);
202
+ reject(new Error(`Failed to start loopback server on port ${port}: ${err.message}`));
203
+ });
204
+ });
205
+ }
206
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/pkce.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keycardai/oauth",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "[Preview] OAuth 2.0 primitives for Keycard: JWKS keyring, JWT signing/verification, server-tier token verifier, AccessContext, ClientSecret credentials, and impersonation via RFC 8693 token exchange",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -89,6 +89,11 @@
89
89
  "import": "./dist/esm/server/clientSecret.js",
90
90
  "require": "./dist/cjs/server/clientSecret.js",
91
91
  "types": "./dist/esm/server/clientSecret.d.ts"
92
+ },
93
+ "./pkce": {
94
+ "import": "./dist/esm/pkce.js",
95
+ "require": "./dist/cjs/pkce.js",
96
+ "types": "./dist/esm/pkce.d.ts"
92
97
  }
93
98
  },
94
99
  "files": [
@@ -112,6 +117,7 @@
112
117
  },
113
118
  "devDependencies": {
114
119
  "@jest/globals": "^30.0.4",
120
+ "@types/node": "^25.6.0",
115
121
  "jest": "^30.0.4",
116
122
  "ts-jest": "^29.4.0",
117
123
  "typescript": "^5.8.3"