@incorta/auth 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/{config-CHNLJVau.d.cts → app-access-BSsKtvbi.d.cts} +72 -1
- package/dist/{config-CHNLJVau.d.ts → app-access-BSsKtvbi.d.ts} +72 -1
- package/dist/chunk-TMXOEIA2.js +102 -0
- package/dist/chunk-TMXOEIA2.js.map +1 -0
- package/dist/express.cjs +52 -2
- package/dist/express.cjs.map +1 -1
- package/dist/express.d.cts +20 -3
- package/dist/express.d.ts +20 -3
- package/dist/express.js +47 -2
- package/dist/express.js.map +1 -1
- package/dist/index.cjs +331 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -3
- package/dist/index.d.ts +34 -3
- package/dist/index.js +242 -3
- package/dist/index.js.map +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppAccessTokenRejectedError,
|
|
3
|
+
AppAccessUnavailableError,
|
|
4
|
+
EDIT_BIT,
|
|
5
|
+
SHARE_BIT,
|
|
6
|
+
VIEW_BIT,
|
|
7
|
+
createAppAccessChecker
|
|
8
|
+
} from "./chunk-TMXOEIA2.js";
|
|
9
|
+
|
|
1
10
|
// src/core/config.ts
|
|
11
|
+
function resolveAppAccess(input, envValue) {
|
|
12
|
+
if (input) return input;
|
|
13
|
+
const raw = envValue?.trim().toLowerCase();
|
|
14
|
+
if (!raw) return "authenticated";
|
|
15
|
+
if (raw === "public" || raw === "authenticated" || raw === "catalog") return raw;
|
|
16
|
+
console.warn(
|
|
17
|
+
`[incorta-auth] Unrecognized INCORTA_APP_ACCESS value "${envValue}" \u2014 using "authenticated".`
|
|
18
|
+
);
|
|
19
|
+
return "authenticated";
|
|
20
|
+
}
|
|
2
21
|
function required(value, name, envVar) {
|
|
3
22
|
if (!value) {
|
|
4
23
|
throw new Error(
|
|
@@ -72,6 +91,8 @@ function resolveConfig(input = {}) {
|
|
|
72
91
|
cookieSameSite,
|
|
73
92
|
cookiePartitioned,
|
|
74
93
|
exposeAccessToken: input.exposeAccessToken ?? false,
|
|
94
|
+
appAccess: resolveAppAccess(input.appAccess, env.INCORTA_APP_ACCESS),
|
|
95
|
+
embeddedSignIn: input.embeddedSignIn ?? (env.INCORTA_EMBEDDED_SIGN_IN?.toLowerCase() === "new-tab" ? "new-tab" : "redirect"),
|
|
75
96
|
issuer,
|
|
76
97
|
discoveryUrl: input.advanced?.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`,
|
|
77
98
|
jwksOverride: input.advanced?.jwks,
|
|
@@ -380,7 +401,17 @@ function withErrorParam(target, code) {
|
|
|
380
401
|
function randomToken() {
|
|
381
402
|
return randomBytes(16).toString("base64url");
|
|
382
403
|
}
|
|
404
|
+
function escapeHtml(value) {
|
|
405
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
406
|
+
}
|
|
407
|
+
function wantsHtml(request) {
|
|
408
|
+
return request.method.toUpperCase() === "GET" && (request.headers.get("accept") ?? "").includes("text/html");
|
|
409
|
+
}
|
|
410
|
+
function requestPath(url) {
|
|
411
|
+
return `${url.pathname}${url.search}`;
|
|
412
|
+
}
|
|
383
413
|
function createHandler(config) {
|
|
414
|
+
const appAccessChecker = createAppAccessChecker(config);
|
|
384
415
|
const sessionCookieOptions = (secure) => ({
|
|
385
416
|
httpOnly: true,
|
|
386
417
|
// SameSite=None requires Secure — force the pair even on plain http
|
|
@@ -413,6 +444,9 @@ function createHandler(config) {
|
|
|
413
444
|
authorizeUrl.searchParams.set("scope", config.scope);
|
|
414
445
|
authorizeUrl.searchParams.set("state", state);
|
|
415
446
|
authorizeUrl.searchParams.set("nonce", nonce);
|
|
447
|
+
if (config.embeddedSignIn === "new-tab") {
|
|
448
|
+
return newTabSignInPage(authorizeUrl.toString(), `${origin}/`, [stateCookie]);
|
|
449
|
+
}
|
|
416
450
|
return redirect(authorizeUrl.toString(), [stateCookie]);
|
|
417
451
|
}
|
|
418
452
|
async function callback(request, url) {
|
|
@@ -527,6 +561,18 @@ function createHandler(config) {
|
|
|
527
561
|
setCookies
|
|
528
562
|
);
|
|
529
563
|
}
|
|
564
|
+
async function refreshRoute(request, url) {
|
|
565
|
+
const origin = requestOrigin(config, request);
|
|
566
|
+
const returnTo = validateReturnTo(url.searchParams.get("redirect_to"), origin);
|
|
567
|
+
const { session, setCookies } = await loadSession(request);
|
|
568
|
+
if (!session) {
|
|
569
|
+
return redirect(
|
|
570
|
+
`${config.basePath}/login?redirect_to=${encodeURIComponent(returnTo)}`,
|
|
571
|
+
setCookies
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return redirect(returnTo, setCookies);
|
|
575
|
+
}
|
|
530
576
|
async function logout(request, url) {
|
|
531
577
|
const secure = isSecureRequest(config, request);
|
|
532
578
|
const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));
|
|
@@ -579,6 +625,143 @@ function createHandler(config) {
|
|
|
579
625
|
}
|
|
580
626
|
);
|
|
581
627
|
}
|
|
628
|
+
function newTabSignInPage(authorizeUrl, appRoot, cookies) {
|
|
629
|
+
const body = `<!doctype html>
|
|
630
|
+
<html lang="en">
|
|
631
|
+
<head>
|
|
632
|
+
<meta charset="utf-8" />
|
|
633
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
634
|
+
<title>Sign in with Incorta</title>
|
|
635
|
+
<style>
|
|
636
|
+
body { margin: 0; display: grid; place-items: center; min-height: 100vh;
|
|
637
|
+
background: #fafafa; color: #18181b;
|
|
638
|
+
font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
639
|
+
main { max-width: 26rem; padding: 2rem; text-align: center; }
|
|
640
|
+
h1 { font-size: 1.2rem; margin: 0 0 .5rem; }
|
|
641
|
+
p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }
|
|
642
|
+
a.btn { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;
|
|
643
|
+
background: #18181b; color: #fafafa; text-decoration: none;
|
|
644
|
+
font-weight: 600; }
|
|
645
|
+
</style>
|
|
646
|
+
</head>
|
|
647
|
+
<body>
|
|
648
|
+
<main id="frame" hidden>
|
|
649
|
+
<h1>Sign in with Incorta</h1>
|
|
650
|
+
<p>
|
|
651
|
+
This app signs you in with your Incorta account. The embedded preview
|
|
652
|
+
can’t complete sign-in here — continue in a new tab.
|
|
653
|
+
</p>
|
|
654
|
+
<a class="btn" href="${appRoot}" target="_blank" rel="noopener noreferrer">Sign in with Incorta ↗</a>
|
|
655
|
+
</main>
|
|
656
|
+
<script>
|
|
657
|
+
(function () {
|
|
658
|
+
var authorize = ${JSON.stringify(authorizeUrl)};
|
|
659
|
+
if (window.top === window.self) {
|
|
660
|
+
window.location.replace(authorize);
|
|
661
|
+
} else {
|
|
662
|
+
document.getElementById("frame").hidden = false;
|
|
663
|
+
}
|
|
664
|
+
})();
|
|
665
|
+
</script>
|
|
666
|
+
<noscript><a href="${appRoot}">Continue to sign in</a></noscript>
|
|
667
|
+
</body>
|
|
668
|
+
</html>`;
|
|
669
|
+
const headers = new Headers({
|
|
670
|
+
"content-type": "text/html; charset=utf-8",
|
|
671
|
+
"cache-control": "no-store"
|
|
672
|
+
});
|
|
673
|
+
for (const cookie of cookies) headers.append("set-cookie", cookie);
|
|
674
|
+
return new Response(body, { status: 200, headers });
|
|
675
|
+
}
|
|
676
|
+
function accessDeniedResponse(request, session) {
|
|
677
|
+
if (!wantsHtml(request)) {
|
|
678
|
+
return json(403, { error: "app_access_denied" });
|
|
679
|
+
}
|
|
680
|
+
const identity = escapeHtml(session.user.name?.trim() || session.user.sub);
|
|
681
|
+
const detail = session.user.name?.trim() && session.user.name.trim() !== session.user.sub ? `${identity} (${escapeHtml(session.user.sub)})` : identity;
|
|
682
|
+
const switchHref = `${config.basePath}/logout`;
|
|
683
|
+
return new Response(
|
|
684
|
+
`<!doctype html>
|
|
685
|
+
<html lang="en">
|
|
686
|
+
<head>
|
|
687
|
+
<meta charset="utf-8" />
|
|
688
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
689
|
+
<title>No access</title>
|
|
690
|
+
<style>
|
|
691
|
+
body { margin: 0; display: grid; place-items: center; min-height: 100vh;
|
|
692
|
+
background: #fafafa; color: #18181b;
|
|
693
|
+
font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
694
|
+
main { max-width: 26rem; padding: 2rem; text-align: center; }
|
|
695
|
+
h1 { font-size: 1.25rem; margin: 0 0 .5rem; }
|
|
696
|
+
p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0 0 1.25rem; }
|
|
697
|
+
strong { color: #18181b; font-weight: 600; }
|
|
698
|
+
a { display: inline-block; padding: .55rem 1.1rem; border-radius: .5rem;
|
|
699
|
+
background: #18181b; color: #fafafa; font-size: .9rem;
|
|
700
|
+
text-decoration: none; }
|
|
701
|
+
</style>
|
|
702
|
+
</head>
|
|
703
|
+
<body>
|
|
704
|
+
<main>
|
|
705
|
+
<h1>You don’t have access to this application</h1>
|
|
706
|
+
<p>
|
|
707
|
+
Ask the app’s owner to share it with you in Incorta.
|
|
708
|
+
You are signed in as <strong>${detail}</strong>.
|
|
709
|
+
</p>
|
|
710
|
+
<a href="${switchHref}">Sign in with a different account</a>
|
|
711
|
+
</main>
|
|
712
|
+
</body>
|
|
713
|
+
</html>`,
|
|
714
|
+
{
|
|
715
|
+
status: 403,
|
|
716
|
+
headers: {
|
|
717
|
+
"content-type": "text/html; charset=utf-8",
|
|
718
|
+
"cache-control": "no-store"
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
function accessUnavailableResponse(request) {
|
|
724
|
+
if (!wantsHtml(request)) {
|
|
725
|
+
return json(503, { error: "app_access_unavailable" });
|
|
726
|
+
}
|
|
727
|
+
return new Response(
|
|
728
|
+
`<!doctype html>
|
|
729
|
+
<html lang="en">
|
|
730
|
+
<head>
|
|
731
|
+
<meta charset="utf-8" />
|
|
732
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
733
|
+
<meta http-equiv="refresh" content="15" />
|
|
734
|
+
<title>Temporarily unavailable</title>
|
|
735
|
+
<style>
|
|
736
|
+
body { margin: 0; display: grid; place-items: center; min-height: 100vh;
|
|
737
|
+
background: #fafafa; color: #18181b;
|
|
738
|
+
font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
739
|
+
main { max-width: 26rem; padding: 2rem; text-align: center; }
|
|
740
|
+
h1 { font-size: 1.25rem; margin: 0 0 .5rem; }
|
|
741
|
+
p { color: #52525b; font-size: .9rem; line-height: 1.5; margin: 0; }
|
|
742
|
+
</style>
|
|
743
|
+
</head>
|
|
744
|
+
<body>
|
|
745
|
+
<main>
|
|
746
|
+
<h1>This app is temporarily unavailable</h1>
|
|
747
|
+
<p>
|
|
748
|
+
Your access could not be verified because Incorta did not respond.
|
|
749
|
+
This page retries automatically — you can also refresh in a
|
|
750
|
+
moment.
|
|
751
|
+
</p>
|
|
752
|
+
</main>
|
|
753
|
+
</body>
|
|
754
|
+
</html>`,
|
|
755
|
+
{
|
|
756
|
+
status: 503,
|
|
757
|
+
headers: {
|
|
758
|
+
"content-type": "text/html; charset=utf-8",
|
|
759
|
+
"cache-control": "no-store",
|
|
760
|
+
"retry-after": "30"
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
}
|
|
582
765
|
async function handler(request) {
|
|
583
766
|
const url = new URL(request.url);
|
|
584
767
|
if (url.pathname !== config.basePath && !url.pathname.startsWith(`${config.basePath}/`)) {
|
|
@@ -591,6 +774,7 @@ function createHandler(config) {
|
|
|
591
774
|
if (route === "/callback" && method === "GET") return await callback(request, url);
|
|
592
775
|
if (route === "/session" && method === "GET") return await sessionRoute(request);
|
|
593
776
|
if (route === "/token" && method === "GET") return await tokenRoute(request);
|
|
777
|
+
if (route === "/refresh" && method === "GET") return await refreshRoute(request, url);
|
|
594
778
|
if (route === "/logout" && (method === "POST" || method === "GET")) {
|
|
595
779
|
return await logout(request, url);
|
|
596
780
|
}
|
|
@@ -616,7 +800,57 @@ function createHandler(config) {
|
|
|
616
800
|
expiresAt
|
|
617
801
|
};
|
|
618
802
|
}
|
|
619
|
-
|
|
803
|
+
function getAppAccess(session) {
|
|
804
|
+
return appAccessChecker.check(session.accessToken, session.user.sub);
|
|
805
|
+
}
|
|
806
|
+
async function gate(request) {
|
|
807
|
+
const url = new URL(request.url);
|
|
808
|
+
if (url.pathname === config.basePath || url.pathname.startsWith(`${config.basePath}/`)) {
|
|
809
|
+
return handler(request);
|
|
810
|
+
}
|
|
811
|
+
if (config.appAccess === "public") return null;
|
|
812
|
+
const session = await getSession(request);
|
|
813
|
+
if (!session) {
|
|
814
|
+
if (wantsHtml(request)) {
|
|
815
|
+
return redirect(
|
|
816
|
+
`${config.basePath}/login?redirect_to=${encodeURIComponent(requestPath(url))}`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
return json(401, { error: "unauthenticated" });
|
|
820
|
+
}
|
|
821
|
+
if (config.appAccess !== "catalog") return null;
|
|
822
|
+
if (session.accessTokenExpiresAt <= Date.now()) {
|
|
823
|
+
if (wantsHtml(request)) {
|
|
824
|
+
return redirect(
|
|
825
|
+
`${config.basePath}/refresh?redirect_to=${encodeURIComponent(requestPath(url))}`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
return json(401, { error: "token_expired" });
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
const verdict = await getAppAccess(session);
|
|
832
|
+
if (verdict.allowed) return null;
|
|
833
|
+
return accessDeniedResponse(request, session);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (error instanceof AppAccessTokenRejectedError) {
|
|
836
|
+
const secure = isSecureRequest(config, request);
|
|
837
|
+
const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));
|
|
838
|
+
if (wantsHtml(request)) {
|
|
839
|
+
return redirect(
|
|
840
|
+
`${config.basePath}/login?redirect_to=${encodeURIComponent(requestPath(url))}`,
|
|
841
|
+
[clearSession]
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
return json(401, { error: "unauthenticated" }, [clearSession]);
|
|
845
|
+
}
|
|
846
|
+
if (!(error instanceof AppAccessUnavailableError)) {
|
|
847
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
848
|
+
console.error("[incorta-auth] app access check failed unexpectedly:", message);
|
|
849
|
+
}
|
|
850
|
+
return accessUnavailableResponse(request);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return { handler, getSession, gate, getAppAccess };
|
|
620
854
|
}
|
|
621
855
|
|
|
622
856
|
// src/core/register.ts
|
|
@@ -751,11 +985,16 @@ async function deleteClient(options) {
|
|
|
751
985
|
// src/index.ts
|
|
752
986
|
function createIncortaAuth(config = {}) {
|
|
753
987
|
const resolved = resolveConfig(config);
|
|
754
|
-
const { handler, getSession } = createHandler(resolved);
|
|
755
|
-
return { handler, getSession, config: resolved };
|
|
988
|
+
const { handler, getSession, gate, getAppAccess } = createHandler(resolved);
|
|
989
|
+
return { handler, getSession, gate, getAppAccess, config: resolved };
|
|
756
990
|
}
|
|
757
991
|
export {
|
|
992
|
+
AppAccessTokenRejectedError,
|
|
993
|
+
AppAccessUnavailableError,
|
|
994
|
+
EDIT_BIT,
|
|
758
995
|
IncortaAuthError,
|
|
996
|
+
SHARE_BIT,
|
|
997
|
+
VIEW_BIT,
|
|
759
998
|
createIncortaAuth,
|
|
760
999
|
deleteClient,
|
|
761
1000
|
getClient,
|