@tdacorp/identity-client 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/LICENSE +21 -0
- package/README.md +299 -0
- package/dist/chunk-KLXEPP4S.js +80 -0
- package/dist/chunk-VJSMXIZB.js +320 -0
- package/dist/index.cjs +357 -0
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +28 -0
- package/dist/next.cjs +461 -0
- package/dist/next.d.cts +85 -0
- package/dist/next.d.ts +85 -0
- package/dist/next.js +129 -0
- package/dist/sealed.cjs +101 -0
- package/dist/sealed.d.cts +95 -0
- package/dist/sealed.d.ts +95 -0
- package/dist/sealed.js +10 -0
- package/dist/verify-BiGhwaIz.d.cts +306 -0
- package/dist/verify-BiGhwaIz.d.ts +306 -0
- package/package.json +80 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createRemoteJWKSet } from 'jose';
|
|
2
|
+
export { A as AuthorizationCodeExchangeOptions, C as CLOCK_SKEW_TOLERANCE_SECONDS, a as ClientCredentialsGrantOptions, I as IdTokenClaims, R as RefreshTokensOptions, T as TokenRefreshOutcome, b as TokenRequestError, c as TokenSet, d as TokenVerificationError, e as TokenVerificationErrorCode, V as VerificationResult, f as VerifiedAccessToken, g as VerifyTokenOptions, h as clientCredentialsGrant, i as exchangeAuthorizationCode, r as refreshTokens, v as verifyAccessToken, j as verifyIdToken } from './verify-BiGhwaIz.cjs';
|
|
3
|
+
import '@tdacorp/identity-authz';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The subset of OpenID Provider Metadata / RFC 8414 Authorization Server
|
|
7
|
+
* Metadata this package actually reads -- every field below is one the
|
|
8
|
+
* identity server this package targets always sets. An index signature
|
|
9
|
+
* carries the rest through unread rather than dropping it.
|
|
10
|
+
*/
|
|
11
|
+
interface DiscoveryDocument {
|
|
12
|
+
issuer: string;
|
|
13
|
+
authorization_endpoint: string;
|
|
14
|
+
token_endpoint: string;
|
|
15
|
+
jwks_uri: string;
|
|
16
|
+
userinfo_endpoint?: string;
|
|
17
|
+
end_session_endpoint?: string;
|
|
18
|
+
revocation_endpoint?: string;
|
|
19
|
+
introspection_endpoint?: string;
|
|
20
|
+
registration_endpoint?: string;
|
|
21
|
+
scopes_supported?: string[];
|
|
22
|
+
response_types_supported?: string[];
|
|
23
|
+
grant_types_supported?: string[];
|
|
24
|
+
code_challenge_methods_supported?: string[];
|
|
25
|
+
token_endpoint_auth_methods_supported?: string[];
|
|
26
|
+
subject_types_supported?: string[];
|
|
27
|
+
id_token_signing_alg_values_supported?: string[];
|
|
28
|
+
claims_supported?: string[];
|
|
29
|
+
acr_values_supported?: string[];
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
type RemoteJwks = ReturnType<typeof createRemoteJWKSet>;
|
|
33
|
+
/**
|
|
34
|
+
* Fetches and caches an issuer's OpenID Provider Metadata.
|
|
35
|
+
*
|
|
36
|
+
* Cache: 600s TTL, up to 100 issuers, LRU-evicted. A cache hit also counts as
|
|
37
|
+
* a use for LRU purposes (`touchLru`), so a repeatedly-queried issuer is the
|
|
38
|
+
* last one evicted under pressure.
|
|
39
|
+
*
|
|
40
|
+
* In-flight requests are deduplicated per issuer: see `discoveryPending`'s
|
|
41
|
+
* own docblock for why the pending promise is stored before this function
|
|
42
|
+
* awaits it, and why that ordering is what prevents concurrent cold-start
|
|
43
|
+
* callers from each firing their own request.
|
|
44
|
+
*/
|
|
45
|
+
declare function fetchDiscovery(issuerUrl: string): Promise<DiscoveryDocument>;
|
|
46
|
+
/**
|
|
47
|
+
* Returns a cached `createRemoteJWKSet` instance for `jwksUri`, creating one
|
|
48
|
+
* on first use. See `jwksCache`'s own docblock for why the JWKS response
|
|
49
|
+
* itself is not cached a second time here.
|
|
50
|
+
*/
|
|
51
|
+
declare function getRemoteJwksForUri(jwksUri: string): RemoteJwks;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates a random RFC 7636 PKCE code verifier.
|
|
55
|
+
*
|
|
56
|
+
* @returns A base64url string of 43 characters (32 random bytes), the
|
|
57
|
+
* shortest length RFC 7636 §4.1 allows (43-128). Pass it to
|
|
58
|
+
* `generateCodeChallenge` to derive the matching `code_challenge`, and
|
|
59
|
+
* send it unchanged as `code_verifier` when exchanging the authorization
|
|
60
|
+
* code.
|
|
61
|
+
*/
|
|
62
|
+
declare function generateCodeVerifier(): string;
|
|
63
|
+
/**
|
|
64
|
+
* Derives the RFC 7636 S256 `code_challenge` for a verifier.
|
|
65
|
+
*
|
|
66
|
+
* @param verifier - The code verifier from `generateCodeVerifier`.
|
|
67
|
+
* @returns `base64url(SHA-256(verifier))`, sent as `code_challenge` on the
|
|
68
|
+
* authorization request (alongside `code_challenge_method=S256`).
|
|
69
|
+
*/
|
|
70
|
+
declare function generateCodeChallenge(verifier: string): Promise<string>;
|
|
71
|
+
/**
|
|
72
|
+
* Generates a random CSRF `state` value for the authorization request.
|
|
73
|
+
*
|
|
74
|
+
* @returns A base64url string of 43 characters (32 random bytes). Store it
|
|
75
|
+
* alongside the PKCE verifier and compare it against the `state` the
|
|
76
|
+
* callback receives back before trusting the request.
|
|
77
|
+
*/
|
|
78
|
+
declare function generateState(): string;
|
|
79
|
+
|
|
80
|
+
export { type DiscoveryDocument, fetchDiscovery, generateCodeChallenge, generateCodeVerifier, generateState, getRemoteJwksForUri };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createRemoteJWKSet } from 'jose';
|
|
2
|
+
export { A as AuthorizationCodeExchangeOptions, C as CLOCK_SKEW_TOLERANCE_SECONDS, a as ClientCredentialsGrantOptions, I as IdTokenClaims, R as RefreshTokensOptions, T as TokenRefreshOutcome, b as TokenRequestError, c as TokenSet, d as TokenVerificationError, e as TokenVerificationErrorCode, V as VerificationResult, f as VerifiedAccessToken, g as VerifyTokenOptions, h as clientCredentialsGrant, i as exchangeAuthorizationCode, r as refreshTokens, v as verifyAccessToken, j as verifyIdToken } from './verify-BiGhwaIz.js';
|
|
3
|
+
import '@tdacorp/identity-authz';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The subset of OpenID Provider Metadata / RFC 8414 Authorization Server
|
|
7
|
+
* Metadata this package actually reads -- every field below is one the
|
|
8
|
+
* identity server this package targets always sets. An index signature
|
|
9
|
+
* carries the rest through unread rather than dropping it.
|
|
10
|
+
*/
|
|
11
|
+
interface DiscoveryDocument {
|
|
12
|
+
issuer: string;
|
|
13
|
+
authorization_endpoint: string;
|
|
14
|
+
token_endpoint: string;
|
|
15
|
+
jwks_uri: string;
|
|
16
|
+
userinfo_endpoint?: string;
|
|
17
|
+
end_session_endpoint?: string;
|
|
18
|
+
revocation_endpoint?: string;
|
|
19
|
+
introspection_endpoint?: string;
|
|
20
|
+
registration_endpoint?: string;
|
|
21
|
+
scopes_supported?: string[];
|
|
22
|
+
response_types_supported?: string[];
|
|
23
|
+
grant_types_supported?: string[];
|
|
24
|
+
code_challenge_methods_supported?: string[];
|
|
25
|
+
token_endpoint_auth_methods_supported?: string[];
|
|
26
|
+
subject_types_supported?: string[];
|
|
27
|
+
id_token_signing_alg_values_supported?: string[];
|
|
28
|
+
claims_supported?: string[];
|
|
29
|
+
acr_values_supported?: string[];
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
type RemoteJwks = ReturnType<typeof createRemoteJWKSet>;
|
|
33
|
+
/**
|
|
34
|
+
* Fetches and caches an issuer's OpenID Provider Metadata.
|
|
35
|
+
*
|
|
36
|
+
* Cache: 600s TTL, up to 100 issuers, LRU-evicted. A cache hit also counts as
|
|
37
|
+
* a use for LRU purposes (`touchLru`), so a repeatedly-queried issuer is the
|
|
38
|
+
* last one evicted under pressure.
|
|
39
|
+
*
|
|
40
|
+
* In-flight requests are deduplicated per issuer: see `discoveryPending`'s
|
|
41
|
+
* own docblock for why the pending promise is stored before this function
|
|
42
|
+
* awaits it, and why that ordering is what prevents concurrent cold-start
|
|
43
|
+
* callers from each firing their own request.
|
|
44
|
+
*/
|
|
45
|
+
declare function fetchDiscovery(issuerUrl: string): Promise<DiscoveryDocument>;
|
|
46
|
+
/**
|
|
47
|
+
* Returns a cached `createRemoteJWKSet` instance for `jwksUri`, creating one
|
|
48
|
+
* on first use. See `jwksCache`'s own docblock for why the JWKS response
|
|
49
|
+
* itself is not cached a second time here.
|
|
50
|
+
*/
|
|
51
|
+
declare function getRemoteJwksForUri(jwksUri: string): RemoteJwks;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates a random RFC 7636 PKCE code verifier.
|
|
55
|
+
*
|
|
56
|
+
* @returns A base64url string of 43 characters (32 random bytes), the
|
|
57
|
+
* shortest length RFC 7636 §4.1 allows (43-128). Pass it to
|
|
58
|
+
* `generateCodeChallenge` to derive the matching `code_challenge`, and
|
|
59
|
+
* send it unchanged as `code_verifier` when exchanging the authorization
|
|
60
|
+
* code.
|
|
61
|
+
*/
|
|
62
|
+
declare function generateCodeVerifier(): string;
|
|
63
|
+
/**
|
|
64
|
+
* Derives the RFC 7636 S256 `code_challenge` for a verifier.
|
|
65
|
+
*
|
|
66
|
+
* @param verifier - The code verifier from `generateCodeVerifier`.
|
|
67
|
+
* @returns `base64url(SHA-256(verifier))`, sent as `code_challenge` on the
|
|
68
|
+
* authorization request (alongside `code_challenge_method=S256`).
|
|
69
|
+
*/
|
|
70
|
+
declare function generateCodeChallenge(verifier: string): Promise<string>;
|
|
71
|
+
/**
|
|
72
|
+
* Generates a random CSRF `state` value for the authorization request.
|
|
73
|
+
*
|
|
74
|
+
* @returns A base64url string of 43 characters (32 random bytes). Store it
|
|
75
|
+
* alongside the PKCE verifier and compare it against the `state` the
|
|
76
|
+
* callback receives back before trusting the request.
|
|
77
|
+
*/
|
|
78
|
+
declare function generateState(): string;
|
|
79
|
+
|
|
80
|
+
export { type DiscoveryDocument, fetchDiscovery, generateCodeChallenge, generateCodeVerifier, generateState, getRemoteJwksForUri };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLOCK_SKEW_TOLERANCE_SECONDS,
|
|
3
|
+
TokenRequestError,
|
|
4
|
+
clientCredentialsGrant,
|
|
5
|
+
exchangeAuthorizationCode,
|
|
6
|
+
fetchDiscovery,
|
|
7
|
+
generateCodeChallenge,
|
|
8
|
+
generateCodeVerifier,
|
|
9
|
+
generateState,
|
|
10
|
+
getRemoteJwksForUri,
|
|
11
|
+
refreshTokens,
|
|
12
|
+
verifyAccessToken,
|
|
13
|
+
verifyIdToken
|
|
14
|
+
} from "./chunk-VJSMXIZB.js";
|
|
15
|
+
export {
|
|
16
|
+
CLOCK_SKEW_TOLERANCE_SECONDS,
|
|
17
|
+
TokenRequestError,
|
|
18
|
+
clientCredentialsGrant,
|
|
19
|
+
exchangeAuthorizationCode,
|
|
20
|
+
fetchDiscovery,
|
|
21
|
+
generateCodeChallenge,
|
|
22
|
+
generateCodeVerifier,
|
|
23
|
+
generateState,
|
|
24
|
+
getRemoteJwksForUri,
|
|
25
|
+
refreshTokens,
|
|
26
|
+
verifyAccessToken,
|
|
27
|
+
verifyIdToken
|
|
28
|
+
};
|
package/dist/next.cjs
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/next.ts
|
|
21
|
+
var next_exports = {};
|
|
22
|
+
__export(next_exports, {
|
|
23
|
+
createCallbackRoute: () => createCallbackRoute,
|
|
24
|
+
createLoginRoute: () => createLoginRoute
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(next_exports);
|
|
27
|
+
var import_server = require("next/server");
|
|
28
|
+
|
|
29
|
+
// src/discovery.ts
|
|
30
|
+
var import_jose = require("jose");
|
|
31
|
+
var DISCOVERY_CACHE_TTL_MS = 6e5;
|
|
32
|
+
var CACHE_MAX_ENTRIES = 100;
|
|
33
|
+
var discoveryCache = /* @__PURE__ */ new Map();
|
|
34
|
+
var discoveryPending = /* @__PURE__ */ new Map();
|
|
35
|
+
var jwksCache = /* @__PURE__ */ new Map();
|
|
36
|
+
function touchLru(map, key) {
|
|
37
|
+
const value = map.get(key);
|
|
38
|
+
if (value === void 0) return;
|
|
39
|
+
map.delete(key);
|
|
40
|
+
map.set(key, value);
|
|
41
|
+
}
|
|
42
|
+
function setWithLruEviction(map, key, value, maxEntries) {
|
|
43
|
+
map.delete(key);
|
|
44
|
+
map.set(key, value);
|
|
45
|
+
if (map.size > maxEntries) {
|
|
46
|
+
const oldestKey = map.keys().next().value;
|
|
47
|
+
if (oldestKey !== void 0) map.delete(oldestKey);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function discoveryUrl(issuerUrl) {
|
|
51
|
+
return `${issuerUrl.replace(/\/+$/, "")}/.well-known/openid-configuration`;
|
|
52
|
+
}
|
|
53
|
+
function isLoopbackHost(hostname) {
|
|
54
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1";
|
|
55
|
+
}
|
|
56
|
+
function assertHttps(url, label) {
|
|
57
|
+
const parsed = new URL(url);
|
|
58
|
+
if (parsed.protocol !== "https:" && !isLoopbackHost(parsed.hostname)) {
|
|
59
|
+
throw new Error(`Discovery: ${label} "${url}" must be https (loopback hosts are the only http exception)`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function validateDiscoveryDocument(document, issuerUrl) {
|
|
63
|
+
assertHttps(issuerUrl, "issuer");
|
|
64
|
+
assertHttps(document.jwks_uri, "jwks_uri");
|
|
65
|
+
assertHttps(document.token_endpoint, "token_endpoint");
|
|
66
|
+
if (document.issuer !== issuerUrl) {
|
|
67
|
+
throw new Error(`Discovery: document issuer "${document.issuer}" does not match the configured issuer "${issuerUrl}"`);
|
|
68
|
+
}
|
|
69
|
+
const issuerOrigin = new URL(issuerUrl).origin;
|
|
70
|
+
if (new URL(document.jwks_uri).origin !== issuerOrigin) {
|
|
71
|
+
throw new Error(`Discovery: jwks_uri "${document.jwks_uri}" is not same-origin as issuer "${issuerUrl}"`);
|
|
72
|
+
}
|
|
73
|
+
if (new URL(document.token_endpoint).origin !== issuerOrigin) {
|
|
74
|
+
throw new Error(`Discovery: token_endpoint "${document.token_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function fetchDiscovery(issuerUrl) {
|
|
78
|
+
const cached = discoveryCache.get(issuerUrl);
|
|
79
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
80
|
+
touchLru(discoveryCache, issuerUrl);
|
|
81
|
+
return cached.document;
|
|
82
|
+
}
|
|
83
|
+
const pending = discoveryPending.get(issuerUrl);
|
|
84
|
+
if (pending) return pending;
|
|
85
|
+
const promise = fetch(discoveryUrl(issuerUrl), { headers: { Accept: "application/json" } }).then(
|
|
86
|
+
async (response) => {
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(`Discovery fetch for ${issuerUrl} failed: HTTP ${response.status}`);
|
|
89
|
+
}
|
|
90
|
+
const document = await response.json();
|
|
91
|
+
validateDiscoveryDocument(document, issuerUrl);
|
|
92
|
+
return document;
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
discoveryPending.set(issuerUrl, promise);
|
|
96
|
+
try {
|
|
97
|
+
const document = await promise;
|
|
98
|
+
discoveryPending.delete(issuerUrl);
|
|
99
|
+
setWithLruEviction(discoveryCache, issuerUrl, { document, expiresAt: Date.now() + DISCOVERY_CACHE_TTL_MS }, CACHE_MAX_ENTRIES);
|
|
100
|
+
return document;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
discoveryPending.delete(issuerUrl);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function getRemoteJwksForUri(jwksUri) {
|
|
107
|
+
const cached = jwksCache.get(jwksUri);
|
|
108
|
+
if (cached) {
|
|
109
|
+
touchLru(jwksCache, jwksUri);
|
|
110
|
+
return cached;
|
|
111
|
+
}
|
|
112
|
+
const jwks = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
|
|
113
|
+
setWithLruEviction(jwksCache, jwksUri, jwks, CACHE_MAX_ENTRIES);
|
|
114
|
+
return jwks;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/pkce.ts
|
|
118
|
+
var CODE_VERIFIER_BYTE_LENGTH = 32;
|
|
119
|
+
var STATE_BYTE_LENGTH = 32;
|
|
120
|
+
function base64UrlEncode(bytes) {
|
|
121
|
+
let binary = "";
|
|
122
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
123
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
124
|
+
}
|
|
125
|
+
function randomBase64Url(byteLength) {
|
|
126
|
+
const bytes = new Uint8Array(byteLength);
|
|
127
|
+
crypto.getRandomValues(bytes);
|
|
128
|
+
return base64UrlEncode(bytes);
|
|
129
|
+
}
|
|
130
|
+
function generateCodeVerifier() {
|
|
131
|
+
return randomBase64Url(CODE_VERIFIER_BYTE_LENGTH);
|
|
132
|
+
}
|
|
133
|
+
async function generateCodeChallenge(verifier) {
|
|
134
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
135
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
136
|
+
}
|
|
137
|
+
function generateState() {
|
|
138
|
+
return randomBase64Url(STATE_BYTE_LENGTH);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/token.ts
|
|
142
|
+
var TokenRequestError = class extends Error {
|
|
143
|
+
status;
|
|
144
|
+
error;
|
|
145
|
+
errorDescription;
|
|
146
|
+
constructor(details) {
|
|
147
|
+
super(
|
|
148
|
+
details.errorDescription ?? details.error ?? `Token request failed${details.status ? ` with HTTP ${details.status}` : ""}`
|
|
149
|
+
);
|
|
150
|
+
this.name = "TokenRequestError";
|
|
151
|
+
this.status = details.status;
|
|
152
|
+
this.error = details.error;
|
|
153
|
+
this.errorDescription = details.errorDescription;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function toTokenSet(body) {
|
|
157
|
+
return {
|
|
158
|
+
accessToken: body.access_token,
|
|
159
|
+
tokenType: body.token_type ?? "Bearer",
|
|
160
|
+
refreshToken: body.refresh_token,
|
|
161
|
+
idToken: body.id_token,
|
|
162
|
+
expiresIn: body.expires_in,
|
|
163
|
+
scope: body.scope
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function postTokenRequest(tokenEndpoint, clientId, clientSecret, params) {
|
|
167
|
+
const body = new URLSearchParams(params);
|
|
168
|
+
const headers = {
|
|
169
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
170
|
+
Accept: "application/json"
|
|
171
|
+
};
|
|
172
|
+
if (clientSecret) {
|
|
173
|
+
headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
|
|
174
|
+
} else {
|
|
175
|
+
body.set("client_id", clientId);
|
|
176
|
+
}
|
|
177
|
+
return fetch(tokenEndpoint, { method: "POST", headers, body: body.toString() });
|
|
178
|
+
}
|
|
179
|
+
async function readTokenResponseBody(response) {
|
|
180
|
+
return await response.json().catch(() => null);
|
|
181
|
+
}
|
|
182
|
+
async function exchangeAuthorizationCode(options) {
|
|
183
|
+
const discovery = await fetchDiscovery(options.issuer);
|
|
184
|
+
const response = await postTokenRequest(discovery.token_endpoint, options.clientId, options.clientSecret, {
|
|
185
|
+
grant_type: "authorization_code",
|
|
186
|
+
code: options.code,
|
|
187
|
+
redirect_uri: options.redirectUri,
|
|
188
|
+
code_verifier: options.codeVerifier
|
|
189
|
+
});
|
|
190
|
+
const body = await readTokenResponseBody(response);
|
|
191
|
+
if (!response.ok || !body || body.error || typeof body.access_token !== "string") {
|
|
192
|
+
throw new TokenRequestError({ status: response.status, error: body?.error, errorDescription: body?.error_description });
|
|
193
|
+
}
|
|
194
|
+
return toTokenSet(body);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/verify.ts
|
|
198
|
+
var import_jose2 = require("jose");
|
|
199
|
+
var CLOCK_SKEW_TOLERANCE_SECONDS = 60;
|
|
200
|
+
function joseErrorCode(error) {
|
|
201
|
+
return error instanceof import_jose2.errors.JOSEError ? error.code : void 0;
|
|
202
|
+
}
|
|
203
|
+
function classifyJoseError(error) {
|
|
204
|
+
const code = joseErrorCode(error);
|
|
205
|
+
const message = error instanceof Error ? error.message : "Token verification failed";
|
|
206
|
+
switch (code) {
|
|
207
|
+
case "ERR_JWT_EXPIRED":
|
|
208
|
+
return { code: "expired", message };
|
|
209
|
+
case "ERR_JWT_CLAIM_VALIDATION_FAILED": {
|
|
210
|
+
const claim = error instanceof import_jose2.errors.JWTClaimValidationFailed ? error.claim : void 0;
|
|
211
|
+
if (claim === "aud") return { code: "invalid-audience", message };
|
|
212
|
+
if (claim === "iss") return { code: "invalid-issuer", message };
|
|
213
|
+
return { code: "malformed-claims", message };
|
|
214
|
+
}
|
|
215
|
+
case "ERR_JWS_SIGNATURE_VERIFICATION_FAILED":
|
|
216
|
+
case "ERR_JWS_INVALID":
|
|
217
|
+
case "ERR_JWKS_NO_MATCHING_KEY":
|
|
218
|
+
case "ERR_JWKS_MULTIPLE_MATCHING_KEYS":
|
|
219
|
+
return { code: "invalid-signature", message };
|
|
220
|
+
case "ERR_JWKS_TIMEOUT":
|
|
221
|
+
return { code: "jwks-unavailable", message };
|
|
222
|
+
case "ERR_JWT_INVALID":
|
|
223
|
+
default:
|
|
224
|
+
return { code: "malformed-token", message };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function checkAuthorizedParty(payload, authorizedParties) {
|
|
228
|
+
const party = typeof payload.azp === "string" ? payload.azp : typeof payload.aud === "string" ? payload.aud : void 0;
|
|
229
|
+
if (party === void 0) {
|
|
230
|
+
return { code: "unauthorized-party", message: "Token has no azp claim and no single-string aud to check against authorizedParties" };
|
|
231
|
+
}
|
|
232
|
+
if (!authorizedParties.includes(party)) {
|
|
233
|
+
return { code: "unauthorized-party", message: `Token's authorized party "${party}" is not in the configured allowlist` };
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
function requireConfig(options) {
|
|
238
|
+
if (!options.issuer) throw new Error("verify: options.issuer is required");
|
|
239
|
+
if (!options.audience) throw new Error("verify: options.audience is required");
|
|
240
|
+
}
|
|
241
|
+
async function verifyAgainstIssuer(token, options) {
|
|
242
|
+
requireConfig(options);
|
|
243
|
+
let jwksUri;
|
|
244
|
+
try {
|
|
245
|
+
jwksUri = (await fetchDiscovery(options.issuer)).jwks_uri;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return { errors: [{ code: "jwks-unavailable", message: error instanceof Error ? error.message : "Discovery fetch failed" }] };
|
|
248
|
+
}
|
|
249
|
+
const jwks = getRemoteJwksForUri(jwksUri);
|
|
250
|
+
try {
|
|
251
|
+
const { payload } = await (0, import_jose2.jwtVerify)(token, jwks, {
|
|
252
|
+
issuer: options.issuer,
|
|
253
|
+
audience: options.audience,
|
|
254
|
+
algorithms: ["EdDSA"],
|
|
255
|
+
clockTolerance: CLOCK_SKEW_TOLERANCE_SECONDS
|
|
256
|
+
});
|
|
257
|
+
if (options.authorizedParties) {
|
|
258
|
+
const authorizedPartyError = checkAuthorizedParty(payload, options.authorizedParties);
|
|
259
|
+
if (authorizedPartyError) return { errors: [authorizedPartyError] };
|
|
260
|
+
}
|
|
261
|
+
return { payload };
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return { errors: [classifyJoseError(error)] };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function verifyIdToken(idToken, options) {
|
|
267
|
+
const result = await verifyAgainstIssuer(idToken, options);
|
|
268
|
+
if ("errors" in result) return { success: false, errors: result.errors };
|
|
269
|
+
const { payload } = result;
|
|
270
|
+
if (typeof payload.sub !== "string") {
|
|
271
|
+
return { success: false, errors: [{ code: "malformed-claims", message: "id_token has no sub claim" }] };
|
|
272
|
+
}
|
|
273
|
+
return { success: true, data: payload };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/sealed.ts
|
|
277
|
+
var import_jose3 = require("jose");
|
|
278
|
+
async function deriveContentEncryptionKey(secret, purpose) {
|
|
279
|
+
const encoder = new TextEncoder();
|
|
280
|
+
const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(secret), "HKDF", false, ["deriveBits"]);
|
|
281
|
+
const bits = await crypto.subtle.deriveBits(
|
|
282
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: encoder.encode(purpose) },
|
|
283
|
+
keyMaterial,
|
|
284
|
+
256
|
|
285
|
+
);
|
|
286
|
+
return new Uint8Array(bits);
|
|
287
|
+
}
|
|
288
|
+
function resolveSealingKey(secret) {
|
|
289
|
+
if (typeof secret === "string") return { secretValue: secret };
|
|
290
|
+
const keyNumbers = Object.keys(secret).map(Number);
|
|
291
|
+
if (keyNumbers.length === 0) throw new Error("seal: secret key-rotation map is empty");
|
|
292
|
+
const highest = Math.max(...keyNumbers);
|
|
293
|
+
return { kid: String(highest), secretValue: secret[highest] };
|
|
294
|
+
}
|
|
295
|
+
var MAX_SEALED_BYTES = 4096;
|
|
296
|
+
async function seal(payload, options) {
|
|
297
|
+
const { kid, secretValue } = resolveSealingKey(options.secret);
|
|
298
|
+
const cek = await deriveContentEncryptionKey(secretValue, options.purpose);
|
|
299
|
+
const header = {
|
|
300
|
+
alg: "dir",
|
|
301
|
+
enc: "A256GCM",
|
|
302
|
+
...kid !== void 0 ? { kid } : {}
|
|
303
|
+
};
|
|
304
|
+
const sealed = await new import_jose3.EncryptJWT(payload).setProtectedHeader(header).setIssuedAt().setExpirationTime(`${options.ttlSeconds}s`).encrypt(cek);
|
|
305
|
+
const byteLength = new TextEncoder().encode(sealed).length;
|
|
306
|
+
if (byteLength > MAX_SEALED_BYTES) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`seal: sealed payload is ${byteLength} bytes, over the ${MAX_SEALED_BYTES}-byte limit (MAX_SEALED_BYTES). Shrink the payload -- do not chunk it across cookies; see MAX_SEALED_BYTES's own docblock for why.`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return sealed;
|
|
312
|
+
}
|
|
313
|
+
function resolveUnsealingKey(secret, kid) {
|
|
314
|
+
if (typeof secret === "string") return { secretValue: secret };
|
|
315
|
+
if (kid === void 0 || !(Number(kid) in secret)) return { reason: "unknown-key" };
|
|
316
|
+
return { secretValue: secret[Number(kid)] };
|
|
317
|
+
}
|
|
318
|
+
function classifyUnsealError(error) {
|
|
319
|
+
const code = error instanceof import_jose3.errors.JOSEError ? error.code : void 0;
|
|
320
|
+
return code === "ERR_JWT_EXPIRED" ? "expired" : "invalid";
|
|
321
|
+
}
|
|
322
|
+
async function unseal(sealedValue, options) {
|
|
323
|
+
let kid;
|
|
324
|
+
try {
|
|
325
|
+
kid = (0, import_jose3.decodeProtectedHeader)(sealedValue).kid;
|
|
326
|
+
} catch {
|
|
327
|
+
options.onError?.("invalid");
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const resolved = resolveUnsealingKey(options.secret, kid);
|
|
331
|
+
if ("reason" in resolved) {
|
|
332
|
+
options.onError?.(resolved.reason);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const cek = await deriveContentEncryptionKey(resolved.secretValue, options.purpose);
|
|
337
|
+
const { payload } = await (0, import_jose3.jwtDecrypt)(sealedValue, cek);
|
|
338
|
+
return payload;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const reason = classifyUnsealError(error);
|
|
341
|
+
options.onError?.(reason);
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/next.ts
|
|
347
|
+
var TRANSACTION_TTL_SECONDS = 600;
|
|
348
|
+
var DEFAULT_TRANSACTION_COOKIE = "identity_oauth_txn";
|
|
349
|
+
var TRANSACTION_PURPOSE = "identity-client:oauth-transaction";
|
|
350
|
+
function transactionCookieOptions(maxAge) {
|
|
351
|
+
return {
|
|
352
|
+
httpOnly: true,
|
|
353
|
+
secure: process.env.NODE_ENV === "production",
|
|
354
|
+
sameSite: "lax",
|
|
355
|
+
path: "/",
|
|
356
|
+
maxAge
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function createLoginRoute(config) {
|
|
360
|
+
return async function loginRoute(request) {
|
|
361
|
+
const state = generateState();
|
|
362
|
+
const codeVerifier = generateCodeVerifier();
|
|
363
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
364
|
+
const returnTo = config.getReturnTo?.(request) ?? request.nextUrl.searchParams.get("returnTo") ?? void 0;
|
|
365
|
+
const transaction = { state, codeVerifier, returnTo };
|
|
366
|
+
const sealedTransaction = await seal(transaction, {
|
|
367
|
+
secret: config.cookieSecret,
|
|
368
|
+
ttlSeconds: TRANSACTION_TTL_SECONDS,
|
|
369
|
+
purpose: TRANSACTION_PURPOSE
|
|
370
|
+
});
|
|
371
|
+
const discovery = await fetchDiscovery(config.issuer);
|
|
372
|
+
const authorizeUrl = new URL(discovery.authorization_endpoint);
|
|
373
|
+
authorizeUrl.searchParams.set("client_id", config.clientId);
|
|
374
|
+
authorizeUrl.searchParams.set("redirect_uri", config.redirectUri);
|
|
375
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
376
|
+
authorizeUrl.searchParams.set("scope", config.scope ?? "openid profile email roles");
|
|
377
|
+
authorizeUrl.searchParams.set("state", state);
|
|
378
|
+
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
379
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
380
|
+
const response = import_server.NextResponse.redirect(authorizeUrl);
|
|
381
|
+
response.cookies.set(
|
|
382
|
+
config.transactionCookieName ?? DEFAULT_TRANSACTION_COOKIE,
|
|
383
|
+
sealedTransaction,
|
|
384
|
+
transactionCookieOptions(TRANSACTION_TTL_SECONDS)
|
|
385
|
+
);
|
|
386
|
+
return response;
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function defaultOnError(error) {
|
|
390
|
+
return import_server.NextResponse.json({ error: error.code, error_description: error.message }, { status: 400 });
|
|
391
|
+
}
|
|
392
|
+
function createCallbackRoute(config) {
|
|
393
|
+
return async function callbackRoute(request) {
|
|
394
|
+
const handleError = config.onError ?? defaultOnError;
|
|
395
|
+
const cookieName = config.transactionCookieName ?? DEFAULT_TRANSACTION_COOKIE;
|
|
396
|
+
const params = request.nextUrl.searchParams;
|
|
397
|
+
const providerError = params.get("error");
|
|
398
|
+
if (providerError) {
|
|
399
|
+
return handleError({ code: "provider_error", message: params.get("error_description") ?? providerError });
|
|
400
|
+
}
|
|
401
|
+
const code = params.get("code");
|
|
402
|
+
const state = params.get("state");
|
|
403
|
+
if (!code) return handleError({ code: "missing_code", message: "Callback request has no code parameter" });
|
|
404
|
+
if (!state) return handleError({ code: "missing_state", message: "Callback request has no state parameter" });
|
|
405
|
+
const sealedTransaction = request.cookies.get(cookieName)?.value;
|
|
406
|
+
if (!sealedTransaction) {
|
|
407
|
+
return handleError({
|
|
408
|
+
code: "missing_transaction",
|
|
409
|
+
message: `No ${cookieName} cookie was presented; it may have expired, or this is a replayed callback`
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const transaction = await unseal(sealedTransaction, {
|
|
413
|
+
secret: config.cookieSecret,
|
|
414
|
+
purpose: TRANSACTION_PURPOSE
|
|
415
|
+
});
|
|
416
|
+
if (!transaction) {
|
|
417
|
+
return handleError({ code: "missing_transaction", message: "The transaction cookie failed to decrypt, or has expired" });
|
|
418
|
+
}
|
|
419
|
+
if (transaction.state !== state) {
|
|
420
|
+
return handleError({ code: "state_mismatch", message: "The state parameter does not match the value stored at login" });
|
|
421
|
+
}
|
|
422
|
+
let tokens;
|
|
423
|
+
try {
|
|
424
|
+
tokens = await exchangeAuthorizationCode({
|
|
425
|
+
issuer: config.issuer,
|
|
426
|
+
clientId: config.clientId,
|
|
427
|
+
clientSecret: config.clientSecret,
|
|
428
|
+
code,
|
|
429
|
+
redirectUri: config.redirectUri,
|
|
430
|
+
codeVerifier: transaction.codeVerifier
|
|
431
|
+
});
|
|
432
|
+
} catch (error) {
|
|
433
|
+
return handleError({
|
|
434
|
+
code: "token_exchange_failed",
|
|
435
|
+
message: error instanceof Error ? error.message : "Token exchange failed"
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (!tokens.idToken) {
|
|
439
|
+
return handleError({ code: "token_verification_failed", message: "Token response carried no id_token to verify" });
|
|
440
|
+
}
|
|
441
|
+
const verification = await verifyIdToken(tokens.idToken, {
|
|
442
|
+
issuer: config.issuer,
|
|
443
|
+
audience: config.audience ?? config.clientId,
|
|
444
|
+
authorizedParties: config.authorizedParties
|
|
445
|
+
});
|
|
446
|
+
if (!verification.success) {
|
|
447
|
+
return handleError({
|
|
448
|
+
code: "token_verification_failed",
|
|
449
|
+
message: verification.errors.map((e) => `${e.code}: ${e.message}`).join("; ")
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const response = await config.onSuccess(tokens, verification.data, { returnTo: transaction.returnTo });
|
|
453
|
+
response.cookies.delete(cookieName);
|
|
454
|
+
return response;
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
458
|
+
0 && (module.exports = {
|
|
459
|
+
createCallbackRoute,
|
|
460
|
+
createLoginRoute
|
|
461
|
+
});
|