@tidecloak/js 0.14.41-staging → 0.14.42-staging
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 +32 -47
- package/dist/cjs/lib/tidecloak-dpop.js +151 -42
- package/dist/cjs/lib/tidecloak-dpop.js.map +1 -1
- package/dist/cjs/lib/tidecloak.js +300 -378
- package/dist/cjs/lib/tidecloak.js.map +1 -1
- package/dist/cjs/src/AdminAPI.js +12 -19
- package/dist/cjs/src/AdminAPI.js.map +1 -1
- package/dist/cjs/src/IAMService.js +155 -2098
- package/dist/cjs/src/IAMService.js.map +1 -1
- package/dist/cjs/src/index.js +1 -7
- package/dist/cjs/src/index.js.map +1 -1
- package/dist/esm/lib/tidecloak-dpop.js +151 -42
- package/dist/esm/lib/tidecloak-dpop.js.map +1 -1
- package/dist/esm/lib/tidecloak.js +300 -377
- package/dist/esm/lib/tidecloak.js.map +1 -1
- package/dist/esm/src/AdminAPI.js +3 -7
- package/dist/esm/src/AdminAPI.js.map +1 -1
- package/dist/esm/src/IAMService.js +152 -2065
- package/dist/esm/src/IAMService.js.map +1 -1
- package/dist/esm/src/index.js +1 -7
- package/dist/esm/src/index.js.map +1 -1
- package/dist/types/AdminAPI.d.ts +1 -1
- package/dist/types/IAMService.d.ts +52 -603
- package/dist/types/index.d.ts +1 -11
- package/dist/types/lib/tidecloak-dpop.d.ts +19 -3
- package/dist/types/lib/tidecloak.d.ts +46 -38
- package/dist/types/src/AdminAPI.d.ts +1 -1
- package/dist/types/src/IAMService.d.ts +52 -603
- package/dist/types/src/index.d.ts +1 -11
- package/docs/FRONT_CHANNEL.md +55 -55
- package/lib/README.md +10 -14
- package/package.json +4 -25
- package/dist/cjs/lib/secureFetchPolicy.js +0 -45
- package/dist/cjs/lib/secureFetchPolicy.js.map +0 -1
- package/dist/cjs/src/dpopRoute.js +0 -30
- package/dist/cjs/src/dpopRoute.js.map +0 -1
- package/dist/cjs/src/dpopServer.js +0 -77
- package/dist/cjs/src/dpopServer.js.map +0 -1
- package/dist/cjs/src/generated/dpopAuthAsset.js +0 -7
- package/dist/cjs/src/generated/dpopAuthAsset.js.map +0 -1
- package/dist/cjs/src/policy-react.js +0 -51
- package/dist/cjs/src/policy-react.js.map +0 -1
- package/dist/cjs/src/types.js +0 -6
- package/dist/cjs/src/types.js.map +0 -1
- package/dist/cjs/src/utils/dpopAuthPage.js +0 -69
- package/dist/cjs/src/utils/dpopAuthPage.js.map +0 -1
- package/dist/cjs/src/utils/fetch.js +0 -39
- package/dist/cjs/src/utils/fetch.js.map +0 -1
- package/dist/cjs/src/utils/index.js +0 -17
- package/dist/cjs/src/utils/index.js.map +0 -1
- package/dist/cjs/src/utils/initOptions.js +0 -78
- package/dist/cjs/src/utils/initOptions.js.map +0 -1
- package/dist/cjs/src/utils/pkce.js +0 -48
- package/dist/cjs/src/utils/pkce.js.map +0 -1
- package/dist/cjs/src/utils/silentCheckSso.js +0 -138
- package/dist/cjs/src/utils/silentCheckSso.js.map +0 -1
- package/dist/cjs/src/vite.js +0 -43
- package/dist/cjs/src/vite.js.map +0 -1
- package/dist/esm/lib/secureFetchPolicy.js +0 -42
- package/dist/esm/lib/secureFetchPolicy.js.map +0 -1
- package/dist/esm/src/dpopRoute.js +0 -27
- package/dist/esm/src/dpopRoute.js.map +0 -1
- package/dist/esm/src/dpopServer.js +0 -68
- package/dist/esm/src/dpopServer.js.map +0 -1
- package/dist/esm/src/generated/dpopAuthAsset.js +0 -4
- package/dist/esm/src/generated/dpopAuthAsset.js.map +0 -1
- package/dist/esm/src/policy-react.js +0 -45
- package/dist/esm/src/policy-react.js.map +0 -1
- package/dist/esm/src/types.js +0 -5
- package/dist/esm/src/types.js.map +0 -1
- package/dist/esm/src/utils/dpopAuthPage.js +0 -63
- package/dist/esm/src/utils/dpopAuthPage.js.map +0 -1
- package/dist/esm/src/utils/fetch.js +0 -36
- package/dist/esm/src/utils/fetch.js.map +0 -1
- package/dist/esm/src/utils/index.js +0 -6
- package/dist/esm/src/utils/index.js.map +0 -1
- package/dist/esm/src/utils/initOptions.js +0 -75
- package/dist/esm/src/utils/initOptions.js.map +0 -1
- package/dist/esm/src/utils/pkce.js +0 -43
- package/dist/esm/src/utils/pkce.js.map +0 -1
- package/dist/esm/src/utils/silentCheckSso.js +0 -134
- package/dist/esm/src/utils/silentCheckSso.js.map +0 -1
- package/dist/esm/src/vite.js +0 -40
- package/dist/esm/src/vite.js.map +0 -1
- package/dist/types/dpopRoute.d.ts +0 -11
- package/dist/types/dpopServer.d.ts +0 -35
- package/dist/types/generated/dpopAuthAsset.d.ts +0 -2
- package/dist/types/lib/secureFetchPolicy.d.ts +0 -37
- package/dist/types/policy-react.d.ts +0 -40
- package/dist/types/src/dpopRoute.d.ts +0 -11
- package/dist/types/src/dpopServer.d.ts +0 -35
- package/dist/types/src/generated/dpopAuthAsset.d.ts +0 -2
- package/dist/types/src/policy-react.d.ts +0 -40
- package/dist/types/src/types.d.ts +0 -114
- package/dist/types/src/utils/dpopAuthPage.d.ts +0 -22
- package/dist/types/src/utils/fetch.d.ts +0 -11
- package/dist/types/src/utils/index.d.ts +0 -5
- package/dist/types/src/utils/initOptions.d.ts +0 -69
- package/dist/types/src/utils/pkce.d.ts +0 -24
- package/dist/types/src/utils/silentCheckSso.d.ts +0 -82
- package/dist/types/src/vite.d.ts +0 -13
- package/dist/types/types.d.ts +0 -114
- package/dist/types/utils/dpopAuthPage.d.ts +0 -22
- package/dist/types/utils/fetch.d.ts +0 -11
- package/dist/types/utils/index.d.ts +0 -5
- package/dist/types/utils/initOptions.d.ts +0 -69
- package/dist/types/utils/pkce.d.ts +0 -24
- package/dist/types/utils/silentCheckSso.d.ts +0 -82
- package/dist/types/vite.d.ts +0 -13
- package/docs/HYBRID_MODE.md +0 -185
- package/docs/NATIVE_MODE.md +0 -284
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
// Generated by scripts/gen-dpop-asset.cjs. Do not edit.
|
|
2
|
-
export const DPOP_AUTH_HTML = "<html>\n\n<head>\n <style>\n body {\n margin: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100vh;\n background: #f6f6f7;\n font-family: -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n }\n\n .status {\n text-align: center;\n opacity: 0;\n }\n\n .status.visible {\n opacity: 1;\n }\n\n .checkmark-circle {\n width: 72px;\n height: 72px;\n margin: 0 auto 16px;\n }\n\n .checkmark-circle circle {\n fill: none;\n stroke: #039855;\n stroke-width: 3;\n stroke-dasharray: 201;\n stroke-dashoffset: 201;\n }\n\n .checkmark-circle path {\n fill: none;\n stroke: #039855;\n stroke-width: 3;\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-dasharray: 50;\n stroke-dashoffset: 50;\n }\n\n .status.visible .checkmark-circle circle {\n animation: circle-draw 0.5s ease forwards;\n }\n\n .status.visible .checkmark-circle path {\n animation: check-draw 0.3s 0.4s ease forwards;\n }\n\n .status.visible .status-text {\n animation: fade-in 0.3s 0.6s ease forwards;\n }\n\n .status-text {\n font-size: 15px;\n color: #464647;\n margin: 0;\n opacity: 0;\n }\n\n @keyframes circle-draw {\n to {\n stroke-dashoffset: 0;\n }\n }\n\n @keyframes check-draw {\n to {\n stroke-dashoffset: 0;\n }\n }\n\n @keyframes fade-in {\n to {\n opacity: 1;\n }\n }\n </style>\n</head>\n\n<body>\n <div class=\"status\" id=\"status\">\n <svg class=\"checkmark-circle\" viewBox=\"0 0 72 72\">\n <circle cx=\"36\" cy=\"36\" r=\"32\" />\n <path d=\"M22 36 l10 10 l18 -20\" />\n </svg>\n <p class=\"status-text\">Session verified</p>\n </div>\n <script>\n const thisVersion = 1;\n\n const hexToStr = (hex) => {\n if (!hex || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) throw Error(\"Invalid hex string\");\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);\n return new TextDecoder().decode(bytes);\n };\n\n const createDbName = async (issuer, clientId) => {\n const encoder = new TextEncoder();\n const issuerHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(issuer))).slice(0, 8))\n .map(b => b.toString(16).padStart(2, '0')).join('');\n const clientHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(clientId))).slice(0, 8))\n .map(b => b.toString(16).padStart(2, '0')).join('');\n return `dpop:${issuerHash}:${clientHash}`;\n };\n\n // Determine target window: popup has window.opener, iframe has window.parent\n const isPopup = !!window.opener;\n const targetWindow = window.opener || window.parent;\n\n if (!targetWindow || targetWindow === window) {\n document.body.textContent = \"Error: No parent or opener window found.\";\n throw Error(\"No target window available for postMessage\");\n }\n\n const urlParams = new URL(window.location.href).searchParams;\n const enclaveVersion = urlParams.get(\"version\");\n if (!enclaveVersion) throw Error(\"No enclave version provided in url\");\n let openerOrigin = urlParams.get(\"openerOrigin\");\n if (!openerOrigin) throw Error(\"No opener origin provided in url\");\n openerOrigin = new URL(decodeURIComponent(openerOrigin)).origin;\n\n const showSuccess = () => {\n document.getElementById('status').classList.add('visible');\n };\n\n const version1Flow = async () => {\n const paths = new URL(window.location.href).pathname.split(\"/\");\n const iss = hexToStr(paths[paths.indexOf(\"iss\") + 1]);\n const aud = hexToStr(paths[paths.indexOf(\"aud\") + 1]);\n const dbname = await createDbName(iss, aud);\n let dpopKey;\n try {\n // Chrome 125+: requestStorageAccess({ indexedDB: true }) returns a handle\n // whose .indexedDB factory points directly at the unpartitioned bucket.\n // If permission was already granted (within 30 days) this resolves\n // automatically with no prompt.\n //\n // In popup mode we have first-party context so we can access IndexedDB directly.\n let idbFactory;\n if (isPopup) {\n idbFactory = window.indexedDB;\n } else {\n const handle = await document.requestStorageAccess({ indexedDB: true });\n idbFactory = handle.indexedDB;\n }\n\n const db = await new Promise((resolve, reject) => {\n const req = idbFactory.open(dbname);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n dpopKey = await new Promise((resolve, reject) => {\n const tx = db.transaction(\"main\", \"readonly\");\n const store = tx.objectStore(\"main\");\n const req = store.get(\"dpopState\");\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n db.close();\n if (!dpopKey || !dpopKey.keys) throw Error(\"No DPoP key found in IndexedDB\");\n } catch (ex) {\n const isStorageBlocked =\n ex?.name === \"NotAllowedError\" ||\n (ex instanceof DOMException && (\n ex.name === \"SecurityError\" ||\n ex.name === \"UnknownError\" ||\n ex.message?.includes(\"not a known object store name\")\n ));\n const detail = isStorageBlocked\n ? \"Browser is blocking third-party storage. Please allow storage access or disable strict tracking protection.\"\n : \"Failed to access DPoP key from IndexedDB.\";\n targetWindow.postMessage({ type: \"error\", message: \"db failed\", detail }, openerOrigin);\n document.body.textContent = detail;\n throw ex;\n }\n\n const challenge = () => new Promise((resolve) => {\n const handler = (event) => {\n if (event.origin !== openerOrigin) return;\n if (event.data.type === \"challenge\") {\n resolve(event.data.challenge);\n window.removeEventListener(\"message\", handler);\n }\n };\n window.addEventListener(\"message\", handler, false);\n });\n\n const pre_challenge = challenge();\n\n targetWindow.postMessage({ type: \"pageLoaded\", message: \"yay\" }, openerOrigin);\n\n const challengeResp = await pre_challenge;\n if (typeof challengeResp !== \"string\" || challengeResp?.length != 64) throw Error(\"Unexpect challenge format\");\n\n const te = new TextEncoder();\n const signatureInput = te.encode(\"dpop-auth-challenge:\" + challengeResp);\n const algName = dpopKey.keys.privateKey.algorithm.name;\n const signParams = algName === \"ECDSA\"\n ? { name: \"ECDSA\", hash: { \"P-256\": \"SHA-256\", \"P-384\": \"SHA-384\", \"P-521\": \"SHA-512\" }[dpopKey.keys.privateKey.algorithm.namedCurve] }\n : { name: algName };\n const signature = await crypto.subtle.sign(signParams, dpopKey.keys.privateKey, signatureInput);\n\n const publicJwk = await crypto.subtle.exportKey(\"jwk\", dpopKey.keys.publicKey);\n\n targetWindow.postMessage({\n type: \"challenge response\",\n message: {\n dpop_public: JSON.stringify({ crv: publicJwk.crv, kty: publicJwk.kty, x: publicJwk.x, y: publicJwk.y }),\n signature: btoa(String.fromCharCode(...new Uint8Array(signature)))\n }\n }, openerOrigin);\n\n showSuccess();\n };\n\n const versionFlows = new Map([[1, version1Flow]]);\n const latestFlow = version1Flow;\n\n (async () => {\n if (Number(enclaveVersion) >= thisVersion) await latestFlow();\n else {\n const flow = versionFlows.get(Number(enclaveVersion));\n if (!flow) throw Error(\"Cannot find supported enclave version: \" + enclaveVersion);\n await flow();\n }\n })();\n </script>\n</body>\n\n</html>";
|
|
3
|
-
export const DPOP_AUTH_CSP = "default-src 'self'; script-src 'self' 'sha256-utc6UrebuHOyLd/2aiMXS/p1EDy9UZBDe/XEMKDw9Mc='; style-src 'self' 'sha256-F7OJTdJYct4J+cQfuJUoDauitndqt8pAc8EbA8gwDPU='";
|
|
4
|
-
//# sourceMappingURL=dpopAuthAsset.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dpopAuthAsset.js","sourceRoot":"","sources":["../../../../src/generated/dpopAuthAsset.js"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,CAAC,MAAM,cAAc,GAAG,uvSAAuvS,CAAC;AACtxS,MAAM,CAAC,MAAM,aAAa,GAAG,qKAAqK,CAAC"}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated The policy editor is no longer part of @tidecloak/js. This entry
|
|
3
|
-
* point is kept so existing imports still resolve; every component throws when
|
|
4
|
-
* rendered. The @tidecloak/policy package it used to re-export was never
|
|
5
|
-
* published to npm.
|
|
6
|
-
*/
|
|
7
|
-
const moved = (name) => new Error(`${name} from "@tidecloak/js/policy-react" is no longer available: the policy editor has moved out of @tidecloak/js.`);
|
|
8
|
-
/** @typedef {any} Policy */
|
|
9
|
-
/** @typedef {any} PolicyBlock */
|
|
10
|
-
/** @typedef {any} Model */
|
|
11
|
-
/** @typedef {any} ModelField */
|
|
12
|
-
/** @typedef {any} Claim */
|
|
13
|
-
/**
|
|
14
|
-
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
15
|
-
* @param {any} _props
|
|
16
|
-
* @returns {never}
|
|
17
|
-
*/
|
|
18
|
-
export function PolicyBuilder(_props) {
|
|
19
|
-
throw moved("PolicyBuilder");
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
23
|
-
* @param {any} _props
|
|
24
|
-
* @returns {never}
|
|
25
|
-
*/
|
|
26
|
-
export function PolicyCanvas(_props) {
|
|
27
|
-
throw moved("PolicyCanvas");
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
31
|
-
* @param {any} _props
|
|
32
|
-
* @returns {never}
|
|
33
|
-
*/
|
|
34
|
-
export function PropertiesPanel(_props) {
|
|
35
|
-
throw moved("PropertiesPanel");
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
39
|
-
* @param {any} _props
|
|
40
|
-
* @returns {never}
|
|
41
|
-
*/
|
|
42
|
-
export function BlockPalette(_props) {
|
|
43
|
-
throw moved("BlockPalette");
|
|
44
|
-
}
|
|
45
|
-
//# sourceMappingURL=policy-react.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"policy-react.js","sourceRoot":"","sources":["../../../src/policy-react.js"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,EAAE,CACrB,IAAI,KAAK,CACP,GAAG,IAAI,8GAA8G,CACtH,CAAC;AAEJ,4BAA4B;AAC5B,iCAAiC;AACjC,2BAA2B;AAC3B,gCAAgC;AAChC,2BAA2B;AAE3B;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,MAAM;IAClC,MAAM,KAAK,CAAC,eAAe,CAAC,CAAC;AAC/B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAAM;IACjC,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC;AAC9B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAM;IACpC,MAAM,KAAK,CAAC,iBAAiB,CAAC,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAAM;IACjC,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC;AAC9B,CAAC"}
|
package/dist/esm/src/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// With DPoP on, the enclave loads tide_dpop_auth.html from this app's origin
|
|
2
|
-
// during login. If it is missing, login stalls ~4s and falls back to a popup
|
|
3
|
-
// with nothing logged. This check says which URL is missing. It never throws.
|
|
4
|
-
export function utf8ToHex(value) {
|
|
5
|
-
let out = '';
|
|
6
|
-
for (const b of new TextEncoder().encode(value))
|
|
7
|
-
out += b.toString(16).padStart(2, '0');
|
|
8
|
-
return out;
|
|
9
|
-
}
|
|
10
|
-
export function buildDpopAuthUrl({ origin, authServerUrl, realm, clientId }) {
|
|
11
|
-
if (!origin || !authServerUrl || !realm || !clientId)
|
|
12
|
-
return null;
|
|
13
|
-
const issuer = `${String(authServerUrl).replace(/\/+$/, '')}/realms/${realm}`;
|
|
14
|
-
return `${String(origin).replace(/\/+$/, '')}/tide_dpop/iss/${utf8ToHex(issuer)}/aud/${utf8ToHex(clientId)}/tide_dpop_auth.html`;
|
|
15
|
-
}
|
|
16
|
-
// Returns served | missing-headers | mismatch | not-served | inconclusive.
|
|
17
|
-
export async function probeDpopAuthPage({ url, fetchImpl = globalThis.fetch, timeoutMs = 2000 }) {
|
|
18
|
-
const controller = new AbortController();
|
|
19
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
20
|
-
let res;
|
|
21
|
-
try {
|
|
22
|
-
res = await fetchImpl(url, { method: 'HEAD', cache: 'no-store', credentials: 'omit', signal: controller.signal });
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return { outcome: 'inconclusive' };
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
clearTimeout(timer);
|
|
29
|
-
}
|
|
30
|
-
// 405/501: a GET-only route is still a working route.
|
|
31
|
-
if (res.status === 403)
|
|
32
|
-
return { outcome: 'mismatch' };
|
|
33
|
-
if (res.status === 404 || res.status === 410)
|
|
34
|
-
return { outcome: 'not-served' };
|
|
35
|
-
if (res.status < 200 || res.status >= 300)
|
|
36
|
-
return { outcome: 'inconclusive' };
|
|
37
|
-
const missing = ['Content-Security-Policy', 'Allow-CSP-From'].filter((h) => !res.headers.get(h));
|
|
38
|
-
return missing.length ? { outcome: 'missing-headers', missing } : { outcome: 'served' };
|
|
39
|
-
}
|
|
40
|
-
export async function preflightDpopAuthPage({ logger = console, ...params }) {
|
|
41
|
-
try {
|
|
42
|
-
const url = buildDpopAuthUrl(params);
|
|
43
|
-
if (!url)
|
|
44
|
-
return 'inconclusive';
|
|
45
|
-
const { outcome, missing } = await probeDpopAuthPage({ url, ...params });
|
|
46
|
-
if (outcome === 'not-served') {
|
|
47
|
-
logger.error(`[IAMService] DPoP is on but nothing is served at ${url}. Serve tide_dpop_auth.html there ` +
|
|
48
|
-
'with Content-Security-Policy and Allow-CSP-From: * headers, or logins will stall and fall back to a popup.');
|
|
49
|
-
}
|
|
50
|
-
else if (outcome === 'missing-headers') {
|
|
51
|
-
logger.warn(`[IAMService] ${url} is served without ${missing.join(' and ')}. The enclave cannot embed it.`);
|
|
52
|
-
}
|
|
53
|
-
else if (outcome === 'mismatch') {
|
|
54
|
-
// Behind a proxy, TideCloak may see a different base URL than the one in our config.
|
|
55
|
-
logger.warn(`[IAMService] ${url} answered 403. Check the realm and client id match your TideCloak config.`);
|
|
56
|
-
}
|
|
57
|
-
return outcome;
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return 'inconclusive';
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
//# sourceMappingURL=dpopAuthPage.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dpopAuthPage.js","sourceRoot":"","sources":["../../../../src/utils/dpopAuthPage.js"],"names":[],"mappings":"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"}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fetch utilities for HTTP requests.
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Fetch JSON with credentials included.
|
|
6
|
-
* @param {string} url - URL to fetch
|
|
7
|
-
* @param {RequestInit} [options={}] - Fetch options
|
|
8
|
-
* @returns {Promise<any>} Parsed JSON response
|
|
9
|
-
* @throws {Error} If request fails (with status and body properties)
|
|
10
|
-
*/
|
|
11
|
-
export async function fetchJson(url, options = {}) {
|
|
12
|
-
const res = await fetch(url, {
|
|
13
|
-
credentials: "include",
|
|
14
|
-
...options,
|
|
15
|
-
headers: {
|
|
16
|
-
"Content-Type": "application/json",
|
|
17
|
-
...(options.headers || {}),
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
const text = await res.text();
|
|
21
|
-
let json = null;
|
|
22
|
-
try {
|
|
23
|
-
json = text ? JSON.parse(text) : null;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
// ignore parse errors
|
|
27
|
-
}
|
|
28
|
-
if (!res.ok) {
|
|
29
|
-
const err = new Error((json === null || json === void 0 ? void 0 : json.message) || `Request failed (${res.status})`);
|
|
30
|
-
err.status = res.status;
|
|
31
|
-
err.body = json !== null && json !== void 0 ? json : text;
|
|
32
|
-
throw err;
|
|
33
|
-
}
|
|
34
|
-
return json;
|
|
35
|
-
}
|
|
36
|
-
//# sourceMappingURL=fetch.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../../../src/utils/fetch.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,EAAE;IAC/C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC3B,WAAW,EAAE,SAAS;QACtB,GAAG,OAAO;QACV,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;SAC3B;KACF,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,IAAI,GAAG,IAAI,CAAC;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,sBAAsB;IACxB,CAAC;IAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,CAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,OAAO,KAAI,mBAAmB,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;QACzE,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QACxB,GAAG,CAAC,IAAI,GAAG,IAAI,aAAJ,IAAI,cAAJ,IAAI,GAAI,IAAI,CAAC;QACxB,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { makePkce, randomPkceVerifier, base64UrlEncode } from './pkce.js';
|
|
2
|
-
export { fetchJson } from './fetch.js';
|
|
3
|
-
export { resolveSilentCheckSsoRedirectUri, SILENT_CHECK_SSO_FILENAME } from './silentCheckSso.js';
|
|
4
|
-
export { buildInitOptions } from './initOptions.js';
|
|
5
|
-
export { preflightDpopAuthPage } from './dpopAuthPage.js';
|
|
6
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/utils/index.js"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,gCAAgC,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAClG,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds the options object handed to `TideCloak.init()` from a caller's
|
|
3
|
-
* resolved TideCloak config.
|
|
4
|
-
*
|
|
5
|
-
* ## Why this is an ALLOWLIST, deliberately
|
|
6
|
-
*
|
|
7
|
-
* `IAMService.initIAM` has always built these options as an allowlist: it reads
|
|
8
|
-
* a handful of keys off the config and drops everything else on the floor. The
|
|
9
|
-
* config object is NOT a `KeycloakInitOptions` bag - it is an adapter/config
|
|
10
|
-
* snapshot (`tidecloak.json` + Tide fields + app-level OIDC hints), and callers
|
|
11
|
-
* legitimately put keys in it that are meaningful to `IAMService` (or to their
|
|
12
|
-
* own code) but that must NOT be handed to `init()`.
|
|
13
|
-
*
|
|
14
|
-
* PR #91 widened this allowlist to also forward `redirectUri`,
|
|
15
|
-
* `silentCheckSsoFallback`, `silentCheckSsoTimeout`, `scope` and a
|
|
16
|
-
* caller-supplied `pkceMethod`, on the reasoning that "each is inert unless the
|
|
17
|
-
* caller sets it". That reasoning is wrong in one important direction: consumers
|
|
18
|
-
* had been shipping configs with these keys set for as long as the keys were
|
|
19
|
-
* being SWALLOWED, so their behaviour was pinned to the SDK's defaults, not to
|
|
20
|
-
* the value in their config. Forwarding them is therefore a silent behaviour
|
|
21
|
-
* change for every existing consumer - the exact opposite of inert.
|
|
22
|
-
*
|
|
23
|
-
* `scope` is the sharpest edge: forwarding it rewrites the authorization
|
|
24
|
-
* request's scope, which changes the audience/roles on the issued access token
|
|
25
|
-
* and can strip a consumer's admin-API access.
|
|
26
|
-
*
|
|
27
|
-
* So: forward ONLY what the SDK genuinely needs, and keep the previously
|
|
28
|
-
* defaulted behaviour byte-for-byte. Any option that a consumer should be able
|
|
29
|
-
* to set must be added here CONSCIOUSLY, with its prior default preserved.
|
|
30
|
-
*
|
|
31
|
-
* ### Forwarded
|
|
32
|
-
* - `setupRequestEnclave` - passed through from the `initIAM` argument.
|
|
33
|
-
* - `onLoad` - always `"check-sso"`. NOT taken from config: callers (e.g. the
|
|
34
|
-
* admin console) set `onLoad: "login-required"`, and honouring it would send
|
|
35
|
-
* `#processInit` down the full-redirect branch and never run silent check-sso
|
|
36
|
-
* at all. That is a separate product decision.
|
|
37
|
-
* - `silentCheckSsoRedirectUri` - the resolved URI (see ./silentCheckSso.js).
|
|
38
|
-
* This is the ONE option PR #91 needed to make caller-supplied, and it stays.
|
|
39
|
-
* `undefined` => TideCloak skips silent check-sso and falls back to an
|
|
40
|
-
* interactive login rather than emitting a URI that would 400.
|
|
41
|
-
* - `pkceMethod` - always `"S256"`, as it was before PR #91.
|
|
42
|
-
* - `useDPoP` / `checkLoginIframe` - the two options `initIAM` has always
|
|
43
|
-
* forwarded. `useDPoP` is forwarded ONLY when the caller actually set it, and
|
|
44
|
-
* VERBATIM. DPoP is opt-in: nothing upstream may default it on, because
|
|
45
|
-
* enabling it appends `dpop_jkt` to the authorization request and the realm
|
|
46
|
-
* then issues a `cnf.jkt`-BOUND token, which a plain-Bearer consumer cannot
|
|
47
|
-
* use (RFC 9449) - a bare `401` with no explanation. Pinned in
|
|
48
|
-
* test/bootstrapDpop.test.js.
|
|
49
|
-
*
|
|
50
|
-
* ### Deliberately NOT forwarded (regression-tested in test/initOptions.test.js)
|
|
51
|
-
* `scope`, `redirectUri`, `silentCheckSsoFallback`, `silentCheckSsoTimeout`.
|
|
52
|
-
* Consumers depend on the SDK's defaults for these. `redirectUri` in particular
|
|
53
|
-
* is still READ from config by `IAMService.doLogin` / `doLogout` and is used as
|
|
54
|
-
* a derivation source for `silentCheckSsoRedirectUri`, so a sub-path-hosted app
|
|
55
|
-
* still gets a correct login redirect - it just does not have `kc.redirectUri`
|
|
56
|
-
* overridden at init time.
|
|
57
|
-
*
|
|
58
|
-
* @param {Object} args
|
|
59
|
-
* @param {Object|null|undefined} args.config Resolved TideCloak config.
|
|
60
|
-
* @param {boolean} args.setupRequestEnclave
|
|
61
|
-
* @param {string|undefined} args.silentCheckSsoRedirectUri Resolved URI, or
|
|
62
|
-
* `undefined` when no correct URI could be derived.
|
|
63
|
-
* @returns {Object} Options for `TideCloak.init()`.
|
|
64
|
-
*/
|
|
65
|
-
export function buildInitOptions({ config, setupRequestEnclave, silentCheckSsoRedirectUri, }) {
|
|
66
|
-
return {
|
|
67
|
-
setupRequestEnclave,
|
|
68
|
-
onLoad: "check-sso",
|
|
69
|
-
silentCheckSsoRedirectUri,
|
|
70
|
-
pkceMethod: "S256",
|
|
71
|
-
...((config === null || config === void 0 ? void 0 : config.useDPoP) && { useDPoP: config.useDPoP }),
|
|
72
|
-
...((config === null || config === void 0 ? void 0 : config.checkLoginIframe) === false && { checkLoginIframe: false }),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
//# sourceMappingURL=initOptions.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"initOptions.js","sourceRoot":"","sources":["../../../../src/utils/initOptions.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,MAAM,EACN,mBAAmB,EACnB,yBAAyB,GAC1B;IACC,OAAO;QACL,mBAAmB;QACnB,MAAM,EAAE,WAAW;QACnB,yBAAyB;QACzB,UAAU,EAAE,MAAM;QAClB,GAAG,CAAC,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,OAAO,KAAI,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;QACnD,GAAG,CAAC,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,gBAAgB,MAAK,KAAK,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC;KACvE,CAAC;AACJ,CAAC"}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PKCE (Proof Key for Code Exchange) utilities for OAuth 2.0 authorization code flow.
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Generate random PKCE verifier string.
|
|
6
|
-
* @param {number} len - Length of the verifier (default 96)
|
|
7
|
-
* @returns {string} Random verifier string
|
|
8
|
-
*/
|
|
9
|
-
export function randomPkceVerifier(len = 96) {
|
|
10
|
-
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
11
|
-
const bytes = new Uint8Array(len);
|
|
12
|
-
crypto.getRandomValues(bytes);
|
|
13
|
-
let out = "";
|
|
14
|
-
for (let i = 0; i < len; i++) {
|
|
15
|
-
out += charset[bytes[i] % charset.length];
|
|
16
|
-
}
|
|
17
|
-
return out;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Base64 URL encode an ArrayBuffer.
|
|
21
|
-
* @param {ArrayBuffer} arrayBuffer - Buffer to encode
|
|
22
|
-
* @returns {string} Base64 URL encoded string
|
|
23
|
-
*/
|
|
24
|
-
export function base64UrlEncode(arrayBuffer) {
|
|
25
|
-
const bytes = new Uint8Array(arrayBuffer);
|
|
26
|
-
let binary = "";
|
|
27
|
-
for (let i = 0; i < bytes.byteLength; i++) {
|
|
28
|
-
binary += String.fromCharCode(bytes[i]);
|
|
29
|
-
}
|
|
30
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Generate PKCE verifier and challenge.
|
|
34
|
-
* @returns {Promise<{verifier: string, challenge: string, method: string}>}
|
|
35
|
-
*/
|
|
36
|
-
export async function makePkce() {
|
|
37
|
-
const verifier = randomPkceVerifier(96);
|
|
38
|
-
const encoder = new TextEncoder();
|
|
39
|
-
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(verifier));
|
|
40
|
-
const challenge = base64UrlEncode(digest);
|
|
41
|
-
return { verifier, challenge, method: "S256" };
|
|
42
|
-
}
|
|
43
|
-
//# sourceMappingURL=pkce.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../../../src/utils/pkce.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAG,GAAG,EAAE;IACzC,MAAM,OAAO,GAAG,oEAAoE,CAAC;IACrF,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC;IAClC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,WAAW;IACzC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC;IAC1C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAClF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC5B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC/E,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC1C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACjD,CAAC"}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolution of the silent-check-SSO redirect URI.
|
|
3
|
-
*
|
|
4
|
-
* ## Why this exists
|
|
5
|
-
*
|
|
6
|
-
* `IAMService.initIAM` used to hardcode:
|
|
7
|
-
*
|
|
8
|
-
* ```js
|
|
9
|
-
* silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`
|
|
10
|
-
* ```
|
|
11
|
-
*
|
|
12
|
-
* That is only correct for an app served from the ORIGIN ROOT. For any app
|
|
13
|
-
* hosted under a sub-path (e.g. a console served at
|
|
14
|
-
* `https://host/realms/<realm>/tide-console/`) the origin-root URI is:
|
|
15
|
-
*
|
|
16
|
-
* - NOT served (there is no file at `/silent-check-sso.html`), and
|
|
17
|
-
* - NOT a registered `redirectUri` on the OIDC client,
|
|
18
|
-
*
|
|
19
|
-
* so the authorization request fails with `400 Invalid parameter: redirect_uri`.
|
|
20
|
-
* The silent iframe then never posts back and the app hangs on its bootstrap
|
|
21
|
-
* spinner. A silently-wrong URI is the worst possible outcome, so this module
|
|
22
|
-
* either derives a CORRECT base-aware URI or reports that it cannot, letting
|
|
23
|
-
* the caller fail loud and fall back to a redirect-based login.
|
|
24
|
-
*
|
|
25
|
-
* ## Resolution order
|
|
26
|
-
*
|
|
27
|
-
* 1. **Caller-supplied `silentCheckSsoRedirectUri`** - always wins, verbatim.
|
|
28
|
-
* 2. **`<base href>`** - the standard way a sub-path-hosted SPA declares where
|
|
29
|
-
* it lives. Resolved via the browser's own `document.baseURI`.
|
|
30
|
-
* 3. **The app's configured OIDC `redirectUri`** - the SDK already requires this
|
|
31
|
-
* to be base-aware and registered, so it is a sound source of truth for the
|
|
32
|
-
* app's base. The SDK's own convention is `<base>auth/redirect`, which is
|
|
33
|
-
* stripped to recover `<base>` exactly; any other redirect path is resolved
|
|
34
|
-
* relative to its own directory.
|
|
35
|
-
* 4. **Origin root** - only when the app declares NEITHER a base NOR a
|
|
36
|
-
* redirectUri. Such an app is relying on the SDK's origin-root default
|
|
37
|
-
* redirectUri (`${origin}/auth/redirect`) and is therefore root-hosted by
|
|
38
|
-
* construction. This preserves the historical behaviour for every existing
|
|
39
|
-
* root-hosted consumer.
|
|
40
|
-
*
|
|
41
|
-
* Anything that cannot be resolved (cross-origin or malformed redirectUri)
|
|
42
|
-
* returns `uri: undefined` plus a `reason`, so the caller can log an error and
|
|
43
|
-
* SKIP the silent check rather than emit a URI that will 400.
|
|
44
|
-
*/
|
|
45
|
-
/** Filename the SDK ships and that hosting providers are expected to serve. */
|
|
46
|
-
export const SILENT_CHECK_SSO_FILENAME = 'silent-check-sso.html';
|
|
47
|
-
/** The redirect path convention IAMService itself defaults to (`<base>auth/redirect`). */
|
|
48
|
-
const AUTH_REDIRECT_SUFFIX = 'auth/redirect';
|
|
49
|
-
/**
|
|
50
|
-
* @typedef {object} SilentCheckSsoResolution
|
|
51
|
-
* @property {string|undefined} uri The resolved URI, or undefined if none could be derived.
|
|
52
|
-
* @property {'config'|'base-href'|'redirect-uri-convention'|'redirect-uri'|'origin-root'|'none'} source
|
|
53
|
-
* @property {string} [reason] Why resolution failed (only when `uri` is undefined).
|
|
54
|
-
*/
|
|
55
|
-
/**
|
|
56
|
-
* Resolve the silent-check-SSO redirect URI.
|
|
57
|
-
*
|
|
58
|
-
* Pure: every environment input is passed in explicitly, so this is unit-testable
|
|
59
|
-
* with no DOM. See the module docblock for the resolution order.
|
|
60
|
-
*
|
|
61
|
-
* @param {object} params
|
|
62
|
-
* @param {string} params.origin The app's origin (`window.location.origin`).
|
|
63
|
-
* @param {string} [params.configured] Caller-supplied `silentCheckSsoRedirectUri`.
|
|
64
|
-
* @param {string} [params.baseHref] `document.baseURI`, but ONLY when a `<base href>` element exists.
|
|
65
|
-
* @param {string} [params.redirectUri] Caller-supplied OIDC `redirectUri`.
|
|
66
|
-
* @returns {SilentCheckSsoResolution}
|
|
67
|
-
*/
|
|
68
|
-
export function resolveSilentCheckSsoRedirectUri({ origin, configured, baseHref, redirectUri } = {}) {
|
|
69
|
-
// 1. An explicit value ALWAYS wins, on every init path. Dropping it is the bug
|
|
70
|
-
// that took the sub-path-hosted console down.
|
|
71
|
-
if (isNonEmptyString(configured)) {
|
|
72
|
-
return { uri: configured.trim(), source: 'config' };
|
|
73
|
-
}
|
|
74
|
-
if (!isNonEmptyString(origin)) {
|
|
75
|
-
return { uri: undefined, source: 'none', reason: 'no origin available (non-browser context)' };
|
|
76
|
-
}
|
|
77
|
-
// 2. A declared <base href> is the app telling us exactly where it is hosted.
|
|
78
|
-
if (isNonEmptyString(baseHref)) {
|
|
79
|
-
try {
|
|
80
|
-
const base = new URL(baseHref, origin);
|
|
81
|
-
return { uri: new URL(SILENT_CHECK_SSO_FILENAME, base).href, source: 'base-href' };
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
// Malformed <base href> - fall through to the redirectUri, which is more
|
|
85
|
-
// load-bearing anyway (login is already broken if it is wrong).
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// 3. Derive the base from the app's own configured OIDC redirectUri.
|
|
89
|
-
if (isNonEmptyString(redirectUri)) {
|
|
90
|
-
let parsed;
|
|
91
|
-
try {
|
|
92
|
-
parsed = new URL(redirectUri, origin);
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
return {
|
|
96
|
-
uri: undefined,
|
|
97
|
-
source: 'none',
|
|
98
|
-
reason: `configured redirectUri (${redirectUri}) is not a valid URL, so the app base cannot be derived`
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
if (parsed.origin !== origin) {
|
|
102
|
-
return {
|
|
103
|
-
uri: undefined,
|
|
104
|
-
source: 'none',
|
|
105
|
-
reason: `configured redirectUri (${redirectUri}) is on a different origin than the app (${origin}), so the app base cannot be derived`
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
// The SDK's own convention: `<base>auth/redirect`. Strip the suffix to
|
|
109
|
-
// recover `<base>` (keeping its trailing slash) rather than resolving
|
|
110
|
-
// relative to `<base>auth/`, which would be one directory too deep.
|
|
111
|
-
if (parsed.pathname.endsWith(`/${AUTH_REDIRECT_SUFFIX}`)) {
|
|
112
|
-
const basePath = parsed.pathname.slice(0, parsed.pathname.length - AUTH_REDIRECT_SUFFIX.length);
|
|
113
|
-
return {
|
|
114
|
-
uri: new URL(SILENT_CHECK_SSO_FILENAME, new URL(basePath, origin)).href,
|
|
115
|
-
source: 'redirect-uri-convention'
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
// Any other redirect path: resolve the filename against the redirect URI's
|
|
119
|
-
// own directory (`/app/callback` -> `/app/silent-check-sso.html`).
|
|
120
|
-
return { uri: new URL(SILENT_CHECK_SSO_FILENAME, parsed).href, source: 'redirect-uri' };
|
|
121
|
-
}
|
|
122
|
-
// 4. Neither a base nor a redirectUri was declared. The app is therefore
|
|
123
|
-
// relying on the SDK's origin-root default redirectUri, i.e. it is
|
|
124
|
-
// root-hosted, and origin-root is the right answer.
|
|
125
|
-
return { uri: `${origin}/${SILENT_CHECK_SSO_FILENAME}`, source: 'origin-root' };
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* @param {unknown} value
|
|
129
|
-
* @returns {value is string}
|
|
130
|
-
*/
|
|
131
|
-
function isNonEmptyString(value) {
|
|
132
|
-
return typeof value === 'string' && value.trim() !== '';
|
|
133
|
-
}
|
|
134
|
-
//# sourceMappingURL=silentCheckSso.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"silentCheckSso.js","sourceRoot":"","sources":["../../../../src/utils/silentCheckSso.js"],"names":[],"mappings":"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"}
|
package/dist/esm/src/vite.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// Serves tide_dpop_auth.html from `vite dev` and `vite preview`:
|
|
2
|
-
//
|
|
3
|
-
// import { tideDpopPlugin } from '@tidecloak/js/vite';
|
|
4
|
-
// export default { plugins: [tideDpopPlugin({ config: tidecloak })] };
|
|
5
|
-
//
|
|
6
|
-
// A static production build still needs the two headers set on the host.
|
|
7
|
-
import { resolveDpopAuthRequest, expectationsFromConfig } from './dpopServer.js';
|
|
8
|
-
/**
|
|
9
|
-
* @param {Object} [options]
|
|
10
|
-
* @param {Object} [options.config] TideCloak adapter config (tidecloak.json). Used to
|
|
11
|
-
* reject requests for another realm or client.
|
|
12
|
-
*/
|
|
13
|
-
export function tideDpopPlugin({ config } = {}) {
|
|
14
|
-
const { issuer, client } = expectationsFromConfig(config);
|
|
15
|
-
const middleware = (req, res, next) => {
|
|
16
|
-
var _a, _b;
|
|
17
|
-
const result = resolveDpopAuthRequest({
|
|
18
|
-
pathname: (_a = req.url) !== null && _a !== void 0 ? _a : '',
|
|
19
|
-
expectedIssuer: issuer,
|
|
20
|
-
expectedClient: client,
|
|
21
|
-
method: req.method,
|
|
22
|
-
});
|
|
23
|
-
if (!result)
|
|
24
|
-
return next();
|
|
25
|
-
res.statusCode = result.status;
|
|
26
|
-
for (const [name, value] of Object.entries(result.headers))
|
|
27
|
-
res.setHeader(name, value);
|
|
28
|
-
res.end((_b = result.body) !== null && _b !== void 0 ? _b : '');
|
|
29
|
-
};
|
|
30
|
-
const install = (server) => {
|
|
31
|
-
if (!issuer || !client) {
|
|
32
|
-
console.warn('[tidecloak] Serving the DPoP page without an issuer/client check. Pass `config` to enable it.');
|
|
33
|
-
}
|
|
34
|
-
// Registered here, before Vite's own middleware, so the SPA fallback cannot answer first.
|
|
35
|
-
server.middlewares.use(middleware);
|
|
36
|
-
};
|
|
37
|
-
return { name: 'tidecloak-dpop-auth-page', configureServer: install, configurePreviewServer: install };
|
|
38
|
-
}
|
|
39
|
-
export default tideDpopPlugin;
|
|
40
|
-
//# sourceMappingURL=vite.js.map
|
package/dist/esm/src/vite.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"vite.js","sourceRoot":"","sources":["../../../src/vite.js"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,yDAAyD;AACzD,yEAAyE;AACzE,EAAE;AACF,yEAAyE;AAEzE,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEjF;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE;IAC5C,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAE1D,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;;QACpC,MAAM,MAAM,GAAG,sBAAsB,CAAC;YACpC,QAAQ,QAAE,GAAG,CAAC,GAAG,mCAAI,EAAE;YACvB,cAAc,EAAE,MAAM;YACtB,cAAc,EAAE,MAAM;YACtB,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC,CAAC;QACH,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,EAAE,CAAC;QAE3B,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;QAC/B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;YAAE,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACvF,GAAG,CAAC,GAAG,OAAC,MAAM,CAAC,IAAI,mCAAI,EAAE,CAAC,CAAC;IAC7B,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,EAAE;QACzB,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACvB,OAAO,CAAC,IAAI,CAAC,+FAA+F,CAAC,CAAC;QAChH,CAAC;QACD,0FAA0F;QAC1F,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACrC,CAAC,CAAC;IAEF,OAAO,EAAE,IAAI,EAAE,0BAA0B,EAAE,eAAe,EAAE,OAAO,EAAE,sBAAsB,EAAE,OAAO,EAAE,CAAC;AACzG,CAAC;AAED,eAAe,cAAc,CAAC"}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @param {Object} [options]
|
|
3
|
-
* @param {Object} [options.config] TideCloak adapter config (tidecloak.json). Used to
|
|
4
|
-
* reject requests for another realm or client. Omitted or empty, it serves any.
|
|
5
|
-
*/
|
|
6
|
-
export declare function createDpopRoute({ config }?: {
|
|
7
|
-
config?: Object;
|
|
8
|
-
}): {
|
|
9
|
-
GET: (request: any) => Response;
|
|
10
|
-
HEAD: (request: any) => Response;
|
|
11
|
-
};
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { DPOP_AUTH_HTML, DPOP_AUTH_CSP } from './generated/dpopAuthAsset.js';
|
|
2
|
-
export { DPOP_AUTH_HTML, DPOP_AUTH_CSP };
|
|
3
|
-
export declare function dpopAuthHeaders(): {
|
|
4
|
-
'Content-Type': string;
|
|
5
|
-
'Content-Security-Policy': string;
|
|
6
|
-
'Allow-CSP-From': string;
|
|
7
|
-
'Cache-Control': string;
|
|
8
|
-
};
|
|
9
|
-
export declare function hexToUtf8(hex: any): string | null;
|
|
10
|
-
export declare function dpopAuthPathFor(issuer: any, clientId: any): string;
|
|
11
|
-
/**
|
|
12
|
-
* Issuer and client id to accept, from a TideCloak adapter config.
|
|
13
|
-
* Either is undefined when the config does not carry it (e.g. the `{}` placeholder).
|
|
14
|
-
*/
|
|
15
|
-
export declare function expectationsFromConfig(config?: {}): {
|
|
16
|
-
issuer: string | undefined;
|
|
17
|
-
client: any;
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* The response for a request path, as `{ status, headers, body }`, or null when
|
|
21
|
-
* the path is not the DPoP page. Omitted expectations are not checked.
|
|
22
|
-
*/
|
|
23
|
-
export declare function resolveDpopAuthRequest({ pathname, expectedIssuer, expectedClient, method }: {
|
|
24
|
-
expectedClient: any;
|
|
25
|
-
expectedIssuer: any;
|
|
26
|
-
method?: string | undefined;
|
|
27
|
-
pathname: any;
|
|
28
|
-
}): {
|
|
29
|
-
status: any;
|
|
30
|
-
headers: {
|
|
31
|
-
'Content-Type': string;
|
|
32
|
-
'Cache-Control': string;
|
|
33
|
-
};
|
|
34
|
-
body: any;
|
|
35
|
-
} | null;
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
export declare const DPOP_AUTH_HTML = "<html>\n\n<head>\n <style>\n body {\n margin: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100vh;\n background: #f6f6f7;\n font-family: -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n }\n\n .status {\n text-align: center;\n opacity: 0;\n }\n\n .status.visible {\n opacity: 1;\n }\n\n .checkmark-circle {\n width: 72px;\n height: 72px;\n margin: 0 auto 16px;\n }\n\n .checkmark-circle circle {\n fill: none;\n stroke: #039855;\n stroke-width: 3;\n stroke-dasharray: 201;\n stroke-dashoffset: 201;\n }\n\n .checkmark-circle path {\n fill: none;\n stroke: #039855;\n stroke-width: 3;\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-dasharray: 50;\n stroke-dashoffset: 50;\n }\n\n .status.visible .checkmark-circle circle {\n animation: circle-draw 0.5s ease forwards;\n }\n\n .status.visible .checkmark-circle path {\n animation: check-draw 0.3s 0.4s ease forwards;\n }\n\n .status.visible .status-text {\n animation: fade-in 0.3s 0.6s ease forwards;\n }\n\n .status-text {\n font-size: 15px;\n color: #464647;\n margin: 0;\n opacity: 0;\n }\n\n @keyframes circle-draw {\n to {\n stroke-dashoffset: 0;\n }\n }\n\n @keyframes check-draw {\n to {\n stroke-dashoffset: 0;\n }\n }\n\n @keyframes fade-in {\n to {\n opacity: 1;\n }\n }\n </style>\n</head>\n\n<body>\n <div class=\"status\" id=\"status\">\n <svg class=\"checkmark-circle\" viewBox=\"0 0 72 72\">\n <circle cx=\"36\" cy=\"36\" r=\"32\" />\n <path d=\"M22 36 l10 10 l18 -20\" />\n </svg>\n <p class=\"status-text\">Session verified</p>\n </div>\n <script>\n const thisVersion = 1;\n\n const hexToStr = (hex) => {\n if (!hex || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) throw Error(\"Invalid hex string\");\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);\n return new TextDecoder().decode(bytes);\n };\n\n const createDbName = async (issuer, clientId) => {\n const encoder = new TextEncoder();\n const issuerHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(issuer))).slice(0, 8))\n .map(b => b.toString(16).padStart(2, '0')).join('');\n const clientHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(clientId))).slice(0, 8))\n .map(b => b.toString(16).padStart(2, '0')).join('');\n return `dpop:${issuerHash}:${clientHash}`;\n };\n\n // Determine target window: popup has window.opener, iframe has window.parent\n const isPopup = !!window.opener;\n const targetWindow = window.opener || window.parent;\n\n if (!targetWindow || targetWindow === window) {\n document.body.textContent = \"Error: No parent or opener window found.\";\n throw Error(\"No target window available for postMessage\");\n }\n\n const urlParams = new URL(window.location.href).searchParams;\n const enclaveVersion = urlParams.get(\"version\");\n if (!enclaveVersion) throw Error(\"No enclave version provided in url\");\n let openerOrigin = urlParams.get(\"openerOrigin\");\n if (!openerOrigin) throw Error(\"No opener origin provided in url\");\n openerOrigin = new URL(decodeURIComponent(openerOrigin)).origin;\n\n const showSuccess = () => {\n document.getElementById('status').classList.add('visible');\n };\n\n const version1Flow = async () => {\n const paths = new URL(window.location.href).pathname.split(\"/\");\n const iss = hexToStr(paths[paths.indexOf(\"iss\") + 1]);\n const aud = hexToStr(paths[paths.indexOf(\"aud\") + 1]);\n const dbname = await createDbName(iss, aud);\n let dpopKey;\n try {\n // Chrome 125+: requestStorageAccess({ indexedDB: true }) returns a handle\n // whose .indexedDB factory points directly at the unpartitioned bucket.\n // If permission was already granted (within 30 days) this resolves\n // automatically with no prompt.\n //\n // In popup mode we have first-party context so we can access IndexedDB directly.\n let idbFactory;\n if (isPopup) {\n idbFactory = window.indexedDB;\n } else {\n const handle = await document.requestStorageAccess({ indexedDB: true });\n idbFactory = handle.indexedDB;\n }\n\n const db = await new Promise((resolve, reject) => {\n const req = idbFactory.open(dbname);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n dpopKey = await new Promise((resolve, reject) => {\n const tx = db.transaction(\"main\", \"readonly\");\n const store = tx.objectStore(\"main\");\n const req = store.get(\"dpopState\");\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n db.close();\n if (!dpopKey || !dpopKey.keys) throw Error(\"No DPoP key found in IndexedDB\");\n } catch (ex) {\n const isStorageBlocked =\n ex?.name === \"NotAllowedError\" ||\n (ex instanceof DOMException && (\n ex.name === \"SecurityError\" ||\n ex.name === \"UnknownError\" ||\n ex.message?.includes(\"not a known object store name\")\n ));\n const detail = isStorageBlocked\n ? \"Browser is blocking third-party storage. Please allow storage access or disable strict tracking protection.\"\n : \"Failed to access DPoP key from IndexedDB.\";\n targetWindow.postMessage({ type: \"error\", message: \"db failed\", detail }, openerOrigin);\n document.body.textContent = detail;\n throw ex;\n }\n\n const challenge = () => new Promise((resolve) => {\n const handler = (event) => {\n if (event.origin !== openerOrigin) return;\n if (event.data.type === \"challenge\") {\n resolve(event.data.challenge);\n window.removeEventListener(\"message\", handler);\n }\n };\n window.addEventListener(\"message\", handler, false);\n });\n\n const pre_challenge = challenge();\n\n targetWindow.postMessage({ type: \"pageLoaded\", message: \"yay\" }, openerOrigin);\n\n const challengeResp = await pre_challenge;\n if (typeof challengeResp !== \"string\" || challengeResp?.length != 64) throw Error(\"Unexpect challenge format\");\n\n const te = new TextEncoder();\n const signatureInput = te.encode(\"dpop-auth-challenge:\" + challengeResp);\n const algName = dpopKey.keys.privateKey.algorithm.name;\n const signParams = algName === \"ECDSA\"\n ? { name: \"ECDSA\", hash: { \"P-256\": \"SHA-256\", \"P-384\": \"SHA-384\", \"P-521\": \"SHA-512\" }[dpopKey.keys.privateKey.algorithm.namedCurve] }\n : { name: algName };\n const signature = await crypto.subtle.sign(signParams, dpopKey.keys.privateKey, signatureInput);\n\n const publicJwk = await crypto.subtle.exportKey(\"jwk\", dpopKey.keys.publicKey);\n\n targetWindow.postMessage({\n type: \"challenge response\",\n message: {\n dpop_public: JSON.stringify({ crv: publicJwk.crv, kty: publicJwk.kty, x: publicJwk.x, y: publicJwk.y }),\n signature: btoa(String.fromCharCode(...new Uint8Array(signature)))\n }\n }, openerOrigin);\n\n showSuccess();\n };\n\n const versionFlows = new Map([[1, version1Flow]]);\n const latestFlow = version1Flow;\n\n (async () => {\n if (Number(enclaveVersion) >= thisVersion) await latestFlow();\n else {\n const flow = versionFlows.get(Number(enclaveVersion));\n if (!flow) throw Error(\"Cannot find supported enclave version: \" + enclaveVersion);\n await flow();\n }\n })();\n </script>\n</body>\n\n</html>";
|
|
2
|
-
export declare const DPOP_AUTH_CSP = "default-src 'self'; script-src 'self' 'sha256-utc6UrebuHOyLd/2aiMXS/p1EDy9UZBDe/XEMKDw9Mc='; style-src 'self' 'sha256-F7OJTdJYct4J+cQfuJUoDauitndqt8pAc8EbA8gwDPU='";
|