@fluixi/oauth2 1.0.0-alpha.1
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/dist/api-guard.cjs +185 -0
- package/dist/api-guard.d.ts +94 -0
- package/dist/api-guard.d.ts.map +1 -0
- package/dist/api-guard.js +127 -0
- package/dist/api-guard.mjs +141 -0
- package/dist/browser-FDUOEUM2.mjs +3544 -0
- package/dist/browser-ONMHX3Z3.mjs +3545 -0
- package/dist/browser.cjs +735 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +448 -0
- package/dist/browser.mjs +704 -0
- package/dist/callback.cjs +125 -0
- package/dist/callback.d.ts +113 -0
- package/dist/callback.d.ts.map +1 -0
- package/dist/callback.js +183 -0
- package/dist/callback.mjs +106 -0
- package/dist/chunk-25WE6UWI.mjs +10 -0
- package/dist/chunk-4NSOGPLM.mjs +15 -0
- package/dist/chunk-7P6ASYW6.mjs +9 -0
- package/dist/chunk-OGSFNTKA.mjs +13 -0
- package/dist/client-auth.cjs +115 -0
- package/dist/client-auth.d.ts +43 -0
- package/dist/client-auth.d.ts.map +1 -0
- package/dist/client-auth.js +112 -0
- package/dist/client-auth.mjs +84 -0
- package/dist/discovery.cjs +86 -0
- package/dist/discovery.d.ts +15 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +51 -0
- package/dist/discovery.mjs +63 -0
- package/dist/dpop-LVPWWADA.mjs +80 -0
- package/dist/dpop-NS7NKOWM.mjs +79 -0
- package/dist/dpop-store.cjs +152 -0
- package/dist/dpop-store.d.ts +37 -0
- package/dist/dpop-store.d.ts.map +1 -0
- package/dist/dpop-store.js +119 -0
- package/dist/dpop-store.mjs +121 -0
- package/dist/dpop.cjs +117 -0
- package/dist/dpop.d.ts +72 -0
- package/dist/dpop.d.ts.map +1 -0
- package/dist/dpop.js +114 -0
- package/dist/dpop.mjs +86 -0
- package/dist/env-config.cjs +366 -0
- package/dist/env-config.d.ts +103 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +184 -0
- package/dist/env-config.mjs +343 -0
- package/dist/env-credentials.cjs +82 -0
- package/dist/env-credentials.d.ts +68 -0
- package/dist/env-credentials.d.ts.map +1 -0
- package/dist/env-credentials.js +76 -0
- package/dist/env-credentials.mjs +51 -0
- package/dist/index.cjs +940 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.mjs +909 -0
- package/dist/pkce.cjs +87 -0
- package/dist/pkce.d.ts +58 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +112 -0
- package/dist/pkce.mjs +66 -0
- package/dist/presets.cjs +240 -0
- package/dist/presets.d.ts +140 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +182 -0
- package/dist/presets.mjs +217 -0
- package/dist/return-to.cjs +46 -0
- package/dist/return-to.d.ts +49 -0
- package/dist/return-to.d.ts.map +1 -0
- package/dist/return-to.js +71 -0
- package/dist/return-to.mjs +25 -0
- package/dist/roles.cjs +56 -0
- package/dist/roles.d.ts +35 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +37 -0
- package/dist/roles.mjs +35 -0
- package/dist/server-client.cjs +134 -0
- package/dist/server-client.d.ts +12 -0
- package/dist/server-client.d.ts.map +1 -0
- package/dist/server-client.js +107 -0
- package/dist/server-client.mjs +111 -0
- package/dist/server.cjs +470 -0
- package/dist/server.d.ts +89 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +360 -0
- package/dist/server.mjs +441 -0
- package/dist/service-client.cjs +244 -0
- package/dist/service-client.d.ts +89 -0
- package/dist/service-client.d.ts.map +1 -0
- package/dist/service-client.js +112 -0
- package/dist/service-client.mjs +213 -0
- package/dist/service.cjs +903 -0
- package/dist/service.d.ts +96 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +129 -0
- package/dist/service.mjs +874 -0
- package/dist/tokens.cjs +29 -0
- package/dist/tokens.d.ts +13 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +21 -0
- package/dist/tokens.mjs +8 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/types.cjs +33 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.mjs +12 -0
- package/dist/verify.cjs +724 -0
- package/dist/verify.d.ts +205 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +371 -0
- package/dist/verify.mjs +590 -0
- package/package.json +121 -0
package/dist/pkce.cjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
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/pkce.ts
|
|
21
|
+
var pkce_exports = {};
|
|
22
|
+
__export(pkce_exports, {
|
|
23
|
+
claimsOf: () => claimsOf,
|
|
24
|
+
createChallenge: () => createChallenge,
|
|
25
|
+
createPkcePair: () => createPkcePair,
|
|
26
|
+
createStateValue: () => createStateValue,
|
|
27
|
+
createVerifier: () => createVerifier,
|
|
28
|
+
expiryFrom: () => expiryFrom,
|
|
29
|
+
expiryFromJwt: () => expiryFromJwt,
|
|
30
|
+
timingSafeEqual: () => timingSafeEqual
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(pkce_exports);
|
|
33
|
+
function base64url(bytes) {
|
|
34
|
+
let binary = "";
|
|
35
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
36
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
37
|
+
}
|
|
38
|
+
function randomBytes(length) {
|
|
39
|
+
const bytes = new Uint8Array(length);
|
|
40
|
+
crypto.getRandomValues(bytes);
|
|
41
|
+
return bytes;
|
|
42
|
+
}
|
|
43
|
+
function createVerifier() {
|
|
44
|
+
return base64url(randomBytes(32));
|
|
45
|
+
}
|
|
46
|
+
function createStateValue() {
|
|
47
|
+
return base64url(randomBytes(16));
|
|
48
|
+
}
|
|
49
|
+
async function createChallenge(verifier) {
|
|
50
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
51
|
+
return base64url(new Uint8Array(digest));
|
|
52
|
+
}
|
|
53
|
+
async function createPkcePair() {
|
|
54
|
+
const verifier = createVerifier();
|
|
55
|
+
return { verifier, challenge: await createChallenge(verifier) };
|
|
56
|
+
}
|
|
57
|
+
function timingSafeEqual(a, b) {
|
|
58
|
+
if (a.length !== b.length) return false;
|
|
59
|
+
let diff = 0;
|
|
60
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
61
|
+
return diff === 0;
|
|
62
|
+
}
|
|
63
|
+
function expiryFrom(expiresIn, now = Date.now()) {
|
|
64
|
+
const seconds = typeof expiresIn === "number" ? expiresIn : Number(expiresIn);
|
|
65
|
+
return Number.isFinite(seconds) && seconds > 0 ? now + seconds * 1e3 : void 0;
|
|
66
|
+
}
|
|
67
|
+
function claimsOf(token) {
|
|
68
|
+
const parts = token.split(".");
|
|
69
|
+
if (parts.length < 2) return null;
|
|
70
|
+
try {
|
|
71
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
72
|
+
return JSON.parse(atob(payload.padEnd(Math.ceil(payload.length / 4) * 4, "=")));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function expiryFromJwt(token) {
|
|
78
|
+
const parts = token.split(".");
|
|
79
|
+
if (parts.length < 2) return null;
|
|
80
|
+
try {
|
|
81
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
82
|
+
const json = JSON.parse(atob(payload.padEnd(Math.ceil(payload.length / 4) * 4, "=")));
|
|
83
|
+
return typeof json.exp === "number" ? json.exp * 1e3 : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/pkce.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PKCE, and the random values the flow needs.
|
|
3
|
+
*
|
|
4
|
+
* A browser client cannot keep a secret, so the authorization code alone is not enough
|
|
5
|
+
* to prove the request came from the app that started it. PKCE closes that: the client
|
|
6
|
+
* invents a `code_verifier`, sends only its hash on the way out, and presents the
|
|
7
|
+
* original when exchanging the code. An attacker who intercepts the code has neither.
|
|
8
|
+
*
|
|
9
|
+
* `state` is the other half, and a different problem: it proves the callback belongs to
|
|
10
|
+
* a flow this client started, which is what stops a forged callback.
|
|
11
|
+
*
|
|
12
|
+
* Everything here goes through the Web Crypto API, which every browser with custom
|
|
13
|
+
* elements has, and which Node has had since 18.
|
|
14
|
+
*/
|
|
15
|
+
/** A `code_verifier`: 43 to 128 characters from the unreserved set. */
|
|
16
|
+
export declare function createVerifier(): string;
|
|
17
|
+
/** A `state` or `nonce`. Same shape, different job. */
|
|
18
|
+
export declare function createStateValue(): string;
|
|
19
|
+
/**
|
|
20
|
+
* The `code_challenge` for a verifier, as S256.
|
|
21
|
+
*
|
|
22
|
+
* The `plain` method is in the spec and is worth nothing: it sends the verifier itself.
|
|
23
|
+
* This package does not offer it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createChallenge(verifier: string): Promise<string>;
|
|
26
|
+
/** A verifier with its challenge, which is how a caller always wants them. */
|
|
27
|
+
export declare function createPkcePair(): Promise<{
|
|
28
|
+
verifier: string;
|
|
29
|
+
challenge: string;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Compare two `state` values without leaking where they differ.
|
|
33
|
+
*
|
|
34
|
+
* The timing of a `===` on strings depends on the first differing character. That is a
|
|
35
|
+
* weak signal here, since state is single-use and short-lived, but comparing it properly
|
|
36
|
+
* costs nothing.
|
|
37
|
+
*/
|
|
38
|
+
export declare function timingSafeEqual(a: string, b: string): boolean;
|
|
39
|
+
/** Epoch milliseconds from an `expires_in` in seconds. */
|
|
40
|
+
export declare function expiryFrom(expiresIn: unknown, now?: number): number | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* A JWT's payload, without verifying the signature.
|
|
43
|
+
*
|
|
44
|
+
* Legitimate for a token this client fetched itself: the code exchange is a direct TLS
|
|
45
|
+
* call to the token endpoint, so the response is authentic by transport, which is what
|
|
46
|
+
* OIDC allows a confidential exchange to rely on. Never use this on a token that arrived
|
|
47
|
+
* from somewhere else.
|
|
48
|
+
*/
|
|
49
|
+
export declare function claimsOf(token: string): Record<string, unknown> | null;
|
|
50
|
+
/**
|
|
51
|
+
* The `exp` of a JWT, in epoch milliseconds.
|
|
52
|
+
*
|
|
53
|
+
* Reads the payload without verifying the signature, which is correct for this use: the
|
|
54
|
+
* client is deciding when to refresh, not whether to trust the token. The server that
|
|
55
|
+
* receives it verifies it properly.
|
|
56
|
+
*/
|
|
57
|
+
export declare function expiryFromJwt(token: string): number | null;
|
|
58
|
+
//# sourceMappingURL=pkce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAgBH,uEAAuE;AACvE,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,uDAAuD;AACvD,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGvE;AAED,8EAA8E;AAC9E,wBAAsB,cAAc,IAAI,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAGvF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK7D;AAED,0DAA0D;AAC1D,wBAAgB,UAAU,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,SAAa,GAAG,MAAM,GAAG,SAAS,CAGnF;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAStE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY1D"}
|
package/dist/pkce.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PKCE, and the random values the flow needs.
|
|
3
|
+
*
|
|
4
|
+
* A browser client cannot keep a secret, so the authorization code alone is not enough
|
|
5
|
+
* to prove the request came from the app that started it. PKCE closes that: the client
|
|
6
|
+
* invents a `code_verifier`, sends only its hash on the way out, and presents the
|
|
7
|
+
* original when exchanging the code. An attacker who intercepts the code has neither.
|
|
8
|
+
*
|
|
9
|
+
* `state` is the other half, and a different problem: it proves the callback belongs to
|
|
10
|
+
* a flow this client started, which is what stops a forged callback.
|
|
11
|
+
*
|
|
12
|
+
* Everything here goes through the Web Crypto API, which every browser with custom
|
|
13
|
+
* elements has, and which Node has had since 18.
|
|
14
|
+
*/
|
|
15
|
+
/** URL-safe base64 with the padding removed, as the spec requires. */
|
|
16
|
+
function base64url(bytes) {
|
|
17
|
+
let binary = '';
|
|
18
|
+
for (const byte of bytes)
|
|
19
|
+
binary += String.fromCharCode(byte);
|
|
20
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
21
|
+
}
|
|
22
|
+
/** Cryptographically random, never `Math.random`. */
|
|
23
|
+
function randomBytes(length) {
|
|
24
|
+
const bytes = new Uint8Array(length);
|
|
25
|
+
crypto.getRandomValues(bytes);
|
|
26
|
+
return bytes;
|
|
27
|
+
}
|
|
28
|
+
/** A `code_verifier`: 43 to 128 characters from the unreserved set. */
|
|
29
|
+
export function createVerifier() {
|
|
30
|
+
return base64url(randomBytes(32));
|
|
31
|
+
}
|
|
32
|
+
/** A `state` or `nonce`. Same shape, different job. */
|
|
33
|
+
export function createStateValue() {
|
|
34
|
+
return base64url(randomBytes(16));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The `code_challenge` for a verifier, as S256.
|
|
38
|
+
*
|
|
39
|
+
* The `plain` method is in the spec and is worth nothing: it sends the verifier itself.
|
|
40
|
+
* This package does not offer it.
|
|
41
|
+
*/
|
|
42
|
+
export async function createChallenge(verifier) {
|
|
43
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
|
44
|
+
return base64url(new Uint8Array(digest));
|
|
45
|
+
}
|
|
46
|
+
/** A verifier with its challenge, which is how a caller always wants them. */
|
|
47
|
+
export async function createPkcePair() {
|
|
48
|
+
const verifier = createVerifier();
|
|
49
|
+
return { verifier, challenge: await createChallenge(verifier) };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compare two `state` values without leaking where they differ.
|
|
53
|
+
*
|
|
54
|
+
* The timing of a `===` on strings depends on the first differing character. That is a
|
|
55
|
+
* weak signal here, since state is single-use and short-lived, but comparing it properly
|
|
56
|
+
* costs nothing.
|
|
57
|
+
*/
|
|
58
|
+
export function timingSafeEqual(a, b) {
|
|
59
|
+
if (a.length !== b.length)
|
|
60
|
+
return false;
|
|
61
|
+
let diff = 0;
|
|
62
|
+
for (let i = 0; i < a.length; i++)
|
|
63
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
64
|
+
return diff === 0;
|
|
65
|
+
}
|
|
66
|
+
/** Epoch milliseconds from an `expires_in` in seconds. */
|
|
67
|
+
export function expiryFrom(expiresIn, now = Date.now()) {
|
|
68
|
+
const seconds = typeof expiresIn === 'number' ? expiresIn : Number(expiresIn);
|
|
69
|
+
return Number.isFinite(seconds) && seconds > 0 ? now + seconds * 1000 : undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A JWT's payload, without verifying the signature.
|
|
73
|
+
*
|
|
74
|
+
* Legitimate for a token this client fetched itself: the code exchange is a direct TLS
|
|
75
|
+
* call to the token endpoint, so the response is authentic by transport, which is what
|
|
76
|
+
* OIDC allows a confidential exchange to rely on. Never use this on a token that arrived
|
|
77
|
+
* from somewhere else.
|
|
78
|
+
*/
|
|
79
|
+
export function claimsOf(token) {
|
|
80
|
+
const parts = token.split('.');
|
|
81
|
+
if (parts.length < 2)
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
85
|
+
return JSON.parse(atob(payload.padEnd(Math.ceil(payload.length / 4) * 4, '=')));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The `exp` of a JWT, in epoch milliseconds.
|
|
93
|
+
*
|
|
94
|
+
* Reads the payload without verifying the signature, which is correct for this use: the
|
|
95
|
+
* client is deciding when to refresh, not whether to trust the token. The server that
|
|
96
|
+
* receives it verifies it properly.
|
|
97
|
+
*/
|
|
98
|
+
export function expiryFromJwt(token) {
|
|
99
|
+
const parts = token.split('.');
|
|
100
|
+
if (parts.length < 2)
|
|
101
|
+
return null;
|
|
102
|
+
try {
|
|
103
|
+
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
104
|
+
const json = JSON.parse(atob(payload.padEnd(Math.ceil(payload.length / 4) * 4, '=')));
|
|
105
|
+
return typeof json.exp === 'number' ? json.exp * 1000 : null;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// An opaque access token is not a JWT and has no `exp` to read. The caller falls
|
|
109
|
+
// back to `expires_in`.
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/pkce.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/pkce.ts
|
|
2
|
+
function base64url(bytes) {
|
|
3
|
+
let binary = "";
|
|
4
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
5
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6
|
+
}
|
|
7
|
+
function randomBytes(length) {
|
|
8
|
+
const bytes = new Uint8Array(length);
|
|
9
|
+
crypto.getRandomValues(bytes);
|
|
10
|
+
return bytes;
|
|
11
|
+
}
|
|
12
|
+
function createVerifier() {
|
|
13
|
+
return base64url(randomBytes(32));
|
|
14
|
+
}
|
|
15
|
+
function createStateValue() {
|
|
16
|
+
return base64url(randomBytes(16));
|
|
17
|
+
}
|
|
18
|
+
async function createChallenge(verifier) {
|
|
19
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
20
|
+
return base64url(new Uint8Array(digest));
|
|
21
|
+
}
|
|
22
|
+
async function createPkcePair() {
|
|
23
|
+
const verifier = createVerifier();
|
|
24
|
+
return { verifier, challenge: await createChallenge(verifier) };
|
|
25
|
+
}
|
|
26
|
+
function timingSafeEqual(a, b) {
|
|
27
|
+
if (a.length !== b.length) return false;
|
|
28
|
+
let diff = 0;
|
|
29
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
30
|
+
return diff === 0;
|
|
31
|
+
}
|
|
32
|
+
function expiryFrom(expiresIn, now = Date.now()) {
|
|
33
|
+
const seconds = typeof expiresIn === "number" ? expiresIn : Number(expiresIn);
|
|
34
|
+
return Number.isFinite(seconds) && seconds > 0 ? now + seconds * 1e3 : void 0;
|
|
35
|
+
}
|
|
36
|
+
function claimsOf(token) {
|
|
37
|
+
const parts = token.split(".");
|
|
38
|
+
if (parts.length < 2) return null;
|
|
39
|
+
try {
|
|
40
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
41
|
+
return JSON.parse(atob(payload.padEnd(Math.ceil(payload.length / 4) * 4, "=")));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function expiryFromJwt(token) {
|
|
47
|
+
const parts = token.split(".");
|
|
48
|
+
if (parts.length < 2) return null;
|
|
49
|
+
try {
|
|
50
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
51
|
+
const json = JSON.parse(atob(payload.padEnd(Math.ceil(payload.length / 4) * 4, "=")));
|
|
52
|
+
return typeof json.exp === "number" ? json.exp * 1e3 : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
claimsOf,
|
|
59
|
+
createChallenge,
|
|
60
|
+
createPkcePair,
|
|
61
|
+
createStateValue,
|
|
62
|
+
createVerifier,
|
|
63
|
+
expiryFrom,
|
|
64
|
+
expiryFromJwt,
|
|
65
|
+
timingSafeEqual
|
|
66
|
+
};
|
package/dist/presets.cjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
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/presets.ts
|
|
21
|
+
var presets_exports = {};
|
|
22
|
+
__export(presets_exports, {
|
|
23
|
+
presets: () => presets
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(presets_exports);
|
|
26
|
+
|
|
27
|
+
// src/roles.ts
|
|
28
|
+
var rolesFrom = {
|
|
29
|
+
/** Keycloak, realm roles. Where it puts them unless a client mapper is configured. */
|
|
30
|
+
keycloakRealm: (claims) => {
|
|
31
|
+
const realm = claims.realm_access;
|
|
32
|
+
return realm?.roles ?? [];
|
|
33
|
+
},
|
|
34
|
+
/** Keycloak, roles scoped to one client rather than the realm. */
|
|
35
|
+
keycloakClient: (clientId) => (claims) => {
|
|
36
|
+
const all = claims.resource_access;
|
|
37
|
+
return all?.[clientId]?.roles ?? [];
|
|
38
|
+
},
|
|
39
|
+
/** Okta, and anything else using a groups claim. Needs a groups claim on the token. */
|
|
40
|
+
groups: (claims) => Array.isArray(claims.groups) ? claims.groups : [],
|
|
41
|
+
/** Cognito, which namespaces the claim. */
|
|
42
|
+
cognito: (claims) => {
|
|
43
|
+
const groups = claims["cognito:groups"];
|
|
44
|
+
return Array.isArray(groups) ? groups : [];
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* Auth0, which requires a namespaced custom claim added by an action.
|
|
48
|
+
*
|
|
49
|
+
* `rolesFrom.namespaced('https://adafri.com/roles')`. Auth0 silently drops a custom claim
|
|
50
|
+
* that is not a URI, so an empty list here usually means the namespace, not the action.
|
|
51
|
+
*/
|
|
52
|
+
namespaced: (claim) => (claims) => {
|
|
53
|
+
const value = claims[claim];
|
|
54
|
+
return Array.isArray(value) ? value : [];
|
|
55
|
+
},
|
|
56
|
+
/** The `scope` claim read as a role list, for a provider issuing no roles at all. */
|
|
57
|
+
scope: (claims) => typeof claims.scope === "string" ? claims.scope.split(" ") : []
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/presets.ts
|
|
61
|
+
var trim = (url) => url.replace(/\/+$/, "");
|
|
62
|
+
var presets = {
|
|
63
|
+
/**
|
|
64
|
+
* Any OIDC provider publishing a discovery document.
|
|
65
|
+
*
|
|
66
|
+
* The agnostic path, and the one to reach for first. A provider not listed below almost
|
|
67
|
+
* certainly works through this.
|
|
68
|
+
*/
|
|
69
|
+
oidc(options) {
|
|
70
|
+
return {
|
|
71
|
+
label: options.issuer,
|
|
72
|
+
client: { issuer: trim(options.issuer), scopes: options.scopes },
|
|
73
|
+
roles: options.roles ?? rolesFrom.scope,
|
|
74
|
+
verifier: (clientId) => ({
|
|
75
|
+
issuer: trim(options.issuer),
|
|
76
|
+
audience: options.audience ?? clientId,
|
|
77
|
+
...options.audience === false ? { authorizedParty: clientId } : {}
|
|
78
|
+
}),
|
|
79
|
+
constraints: []
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* A plain OAuth2 server with no discovery document.
|
|
84
|
+
*
|
|
85
|
+
* Every endpoint is given, because there is nothing to ask. Token verification needs an
|
|
86
|
+
* issuer regardless, since a verifier will not accept a token without one.
|
|
87
|
+
*/
|
|
88
|
+
oauth2(options) {
|
|
89
|
+
return {
|
|
90
|
+
label: options.issuer,
|
|
91
|
+
client: { endpoints: options.endpoints, scopes: options.scopes },
|
|
92
|
+
roles: options.roles ?? rolesFrom.scope,
|
|
93
|
+
verifier: (clientId) => ({
|
|
94
|
+
issuer: trim(options.issuer),
|
|
95
|
+
audience: options.audience ?? clientId,
|
|
96
|
+
...options.audience === false ? { authorizedParty: clientId } : {}
|
|
97
|
+
}),
|
|
98
|
+
constraints: ["Publishes no discovery document, so every endpoint is configured by hand."]
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
/** Keycloak. A realm is its own issuer. */
|
|
102
|
+
keycloak(options) {
|
|
103
|
+
const issuer = `${trim(options.baseUrl)}/realms/${options.realm}`;
|
|
104
|
+
return {
|
|
105
|
+
label: "Keycloak",
|
|
106
|
+
client: {
|
|
107
|
+
issuer,
|
|
108
|
+
scopes: options.scopes ?? ["openid", "profile", "email"]
|
|
109
|
+
},
|
|
110
|
+
roles: rolesFrom.keycloakRealm,
|
|
111
|
+
// No `aud` on an access token until an audience mapper is configured, and the client is
|
|
112
|
+
// named in `azp` instead. So the check moves to `authorizedParty` rather than being
|
|
113
|
+
// dropped: `audience: false` alone would accept a token minted for any client of the
|
|
114
|
+
// realm, which is the check being skipped.
|
|
115
|
+
verifier: (clientId) => ({ issuer, audience: false, authorizedParty: clientId }),
|
|
116
|
+
constraints: [
|
|
117
|
+
"Puts no `aud` on an access token until a dedicated audience mapper is configured. Configure one and switch the verifier to `audience: clientId`.",
|
|
118
|
+
"Revokes the whole session when an authorization code is replayed, so a replay test has to run last."
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
/**
|
|
123
|
+
* Auth0.
|
|
124
|
+
*
|
|
125
|
+
* `audience` is the field that decides what kind of token comes back. Without it Auth0
|
|
126
|
+
* issues an opaque token for its own userinfo endpoint and the JWT checks have nothing to
|
|
127
|
+
* work with.
|
|
128
|
+
*/
|
|
129
|
+
auth0(options) {
|
|
130
|
+
const issuer = `https://${options.domain.replace(/^https?:\/\//, "").replace(/\/+$/, "")}/`;
|
|
131
|
+
return {
|
|
132
|
+
label: "Auth0",
|
|
133
|
+
client: {
|
|
134
|
+
issuer,
|
|
135
|
+
scopes: options.scopes ?? ["openid", "profile", "email", "offline_access"],
|
|
136
|
+
...options.audience ? { extraParams: { audience: options.audience } } : {}
|
|
137
|
+
},
|
|
138
|
+
roles: options.rolesClaim ? rolesFrom.namespaced(options.rolesClaim) : rolesFrom.scope,
|
|
139
|
+
verifier: (clientId) => ({
|
|
140
|
+
issuer,
|
|
141
|
+
// The API identifier is the audience of an access token. Without one the token is
|
|
142
|
+
// opaque and there is nothing for a verifier to check.
|
|
143
|
+
audience: options.audience ?? false,
|
|
144
|
+
...options.audience ? {} : { authorizedParty: clientId }
|
|
145
|
+
}),
|
|
146
|
+
constraints: [
|
|
147
|
+
"Issues an opaque access token unless `audience` names an API, so token verification needs it.",
|
|
148
|
+
"A refresh token needs three things together: `offline_access` in the scopes, an audience whose API has Allow Offline Access on, and Refresh Token Rotation enabled on the application. Any one missing and the scope is ignored silently.",
|
|
149
|
+
"Has no roles claim of its own. An action must add a namespaced one, and Auth0 drops a custom claim that is not a URI.",
|
|
150
|
+
"The issuer carries a trailing slash. A verifier pinned without it rejects every token."
|
|
151
|
+
]
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
/**
|
|
155
|
+
* Okta.
|
|
156
|
+
*
|
|
157
|
+
* Two authorization servers that are not interchangeable: a custom one per application,
|
|
158
|
+
* and the org itself for Okta's own management API.
|
|
159
|
+
*/
|
|
160
|
+
okta(options) {
|
|
161
|
+
const host = `https://${options.domain.replace(/^https?:\/\//, "").replace(/\/+$/, "")}`;
|
|
162
|
+
const server = options.authorizationServer === void 0 ? "default" : options.authorizationServer;
|
|
163
|
+
const issuer = server === null ? host : `${host}/oauth2/${server}`;
|
|
164
|
+
return {
|
|
165
|
+
label: "Okta",
|
|
166
|
+
client: {
|
|
167
|
+
issuer,
|
|
168
|
+
scopes: options.scopes ?? ["openid", "profile", "email", "offline_access"],
|
|
169
|
+
dpop: options.dpop ?? true
|
|
170
|
+
},
|
|
171
|
+
roles: rolesFrom.groups,
|
|
172
|
+
verifier: (clientId) => ({
|
|
173
|
+
issuer,
|
|
174
|
+
// An access token from a custom server is minted for that server's audience, named in
|
|
175
|
+
// the console and `api://default` by default. It does not carry the client id.
|
|
176
|
+
audience: false,
|
|
177
|
+
authorizedParty: clientId
|
|
178
|
+
}),
|
|
179
|
+
constraints: [
|
|
180
|
+
"Two authorization servers: /oauth2/<name> for an application, the bare domain for the management API. They are not interchangeable.",
|
|
181
|
+
"Turns DPoP on by default for a new application, and an integrator org will not let you untick it.",
|
|
182
|
+
"Binds the refresh token to the DPoP key as well as the access token, so a refresh must be proved with the key that obtained it.",
|
|
183
|
+
"A public SPA cannot introspect at all. An API Services client with `private_key_jwt` has to ask instead.",
|
|
184
|
+
"Client credentials against the org server must use `private_key_jwt`, whatever the application is set to."
|
|
185
|
+
]
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
/**
|
|
189
|
+
* Google.
|
|
190
|
+
*
|
|
191
|
+
* Refuses a public client outright, so server mode is the only option: the token endpoint
|
|
192
|
+
* checks for a client secret before it looks at PKCE.
|
|
193
|
+
*/
|
|
194
|
+
google(options = {}) {
|
|
195
|
+
return {
|
|
196
|
+
label: "Google",
|
|
197
|
+
client: {
|
|
198
|
+
issuer: "https://accounts.google.com",
|
|
199
|
+
scopes: options.scopes ?? ["openid", "profile", "email"],
|
|
200
|
+
...options.hostedDomain ? { extraParams: { hd: options.hostedDomain } } : {}
|
|
201
|
+
},
|
|
202
|
+
roles: rolesFrom.scope,
|
|
203
|
+
verifier: (clientId) => ({
|
|
204
|
+
// Names itself two ways, and a service pinned to one rejects tokens carrying the
|
|
205
|
+
// other. Discovery uses the first.
|
|
206
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
207
|
+
audience: clientId
|
|
208
|
+
}),
|
|
209
|
+
constraints: [
|
|
210
|
+
"Refuses a public client: the token endpoint reports `client_secret is missing` before checking PKCE, so browser mode cannot work.",
|
|
211
|
+
"Publishes its issuer both with and without a scheme, so a verifier needs both spellings.",
|
|
212
|
+
"Issues a refresh token only on the first consent unless `prompt=consent` and `access_type=offline` are sent."
|
|
213
|
+
]
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
/**
|
|
217
|
+
* Dex.
|
|
218
|
+
*
|
|
219
|
+
* Sends no CORS headers, on discovery or the token endpoint, so a browser cannot talk to it
|
|
220
|
+
* at all. Server mode only, and giving endpoints explicitly does not help: the token request
|
|
221
|
+
* is blocked too.
|
|
222
|
+
*/
|
|
223
|
+
dex(options) {
|
|
224
|
+
const issuer = trim(options.baseUrl);
|
|
225
|
+
return {
|
|
226
|
+
label: "Dex",
|
|
227
|
+
client: {
|
|
228
|
+
issuer,
|
|
229
|
+
scopes: options.scopes ?? ["openid", "profile", "email", "offline_access"]
|
|
230
|
+
},
|
|
231
|
+
roles: rolesFrom.groups,
|
|
232
|
+
verifier: (clientId) => ({ issuer, audience: clientId }),
|
|
233
|
+
constraints: [
|
|
234
|
+
"Sends no CORS headers on discovery or the token endpoint, so browser mode cannot work. Naming the endpoints explicitly only moves the failure to the token request.",
|
|
235
|
+
"Has no admin interface. Users are static entries in its config file, and a password is a bcrypt hash.",
|
|
236
|
+
"Publishes no revocation endpoint."
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|