@snokam/auth 2.3.3 → 3.0.2
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/CHANGELOG.md +648 -0
- package/package.json +10 -13
- package/src/app-router/components/Login.tsx +2 -2
- package/src/app-router/components/Provider/index.tsx +1 -1
- package/src/app-router/index.tsx +4 -4
- package/src/app-router/server.ts +16 -64
- package/src/components/Authenticate.tsx +2 -2
- package/src/components/Consent/index.tsx +1 -1
- package/src/components/Error/index.tsx +1 -1
- package/src/components/index.ts +5 -0
- package/src/config.ts +1 -1
- package/src/handoff/handlers.ts +6 -3
- package/src/handoff/return-to.ts +1 -1
- package/src/handoff/ticket.ts +7 -69
- package/src/impersonation/cookies.ts +1 -1
- package/src/impersonation/handlers.ts +2 -2
- package/src/impersonation/routes.ts +1 -1
- package/src/impersonation/ticket.ts +6 -51
- package/src/index.ts +11 -0
- package/src/{build-proxy.tsx → middleware.tsx} +3 -4
- package/src/options/callbacks.ts +99 -0
- package/src/options/cookies.ts +13 -0
- package/src/options/employee-profile.ts +90 -0
- package/src/options/index.tsx +19 -0
- package/src/options/providers.ts +117 -0
- package/src/options/request.ts +42 -0
- package/src/{route-handler.ts → route-handler/index.ts} +7 -7
- package/src/route-handler/token.ts +3 -5
- package/src/scopes/index.ts +8 -0
- package/src/scopes/matcher.ts +4 -1
- package/src/session/index.ts +3 -0
- package/src/theme/employee-theme-sync.tsx +1 -1
- package/src/ticket/seal.ts +95 -0
- package/src/index.tsx +0 -31
- package/src/options.tsx +0 -389
- package/src/page-router/components/Login.tsx +0 -50
- package/src/page-router/components/Provider/index.tsx +0 -45
- package/src/page-router/index.tsx +0 -22
- package/src/page-router/server.ts +0 -6
- package/src/server/auth.tsx +0 -30
- package/src/server/handler.tsx +0 -68
- package/src/server.tsx +0 -16
- /package/src/{server → app-router}/token.tsx +0 -0
- /package/src/{utils/consent.ts → consent.ts} +0 -0
- /package/src/{utils/cors.ts → cors.ts} +0 -0
- /package/src/{utils → options}/params.ts +0 -0
- /package/src/{utils → session}/callback-url.ts +0 -0
- /package/src/{utils → session}/cookies.ts +0 -0
- /package/src/{utils → session}/url.ts +0 -0
- /package/src/{utils/types.ts → types.ts} +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ConfidentialClientApplication } from "@azure/msal-node";
|
|
2
|
+
import { Configuration, EmployeesClient } from "@snokam/employees-client";
|
|
3
|
+
import { backendUrl } from "@snokam/tenant/server";
|
|
4
|
+
import { getSanityImageUrl } from "@snokam/ui/media/sanity";
|
|
5
|
+
import { exchangePasswordToken } from "../password/api";
|
|
6
|
+
import { exchangeSsoToken } from "../sso/exchange";
|
|
7
|
+
import { Auth } from "../types";
|
|
8
|
+
|
|
9
|
+
async function resolveEmployeesBackend(
|
|
10
|
+
config: Auth.Config
|
|
11
|
+
): Promise<{ base: string; scope: string }> {
|
|
12
|
+
const scope = config.clientId
|
|
13
|
+
? `api://${config.clientId}/user_impersonation`
|
|
14
|
+
: "";
|
|
15
|
+
return {
|
|
16
|
+
base: await backendUrl("employees", {
|
|
17
|
+
app: "employees",
|
|
18
|
+
entraTenantIdFallback: config.tenantId,
|
|
19
|
+
}),
|
|
20
|
+
scope,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ColorScheme = "light" | "dark" | "system";
|
|
25
|
+
|
|
26
|
+
export function isColorScheme(value: unknown): value is ColorScheme {
|
|
27
|
+
return value === "light" || value === "dark" || value === "system";
|
|
28
|
+
}
|
|
29
|
+
interface EmployeeProfile {
|
|
30
|
+
colorScheme?: ColorScheme;
|
|
31
|
+
imageUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function fetchEmployeeProfile(
|
|
35
|
+
config: Auth.Config,
|
|
36
|
+
refreshToken?: string,
|
|
37
|
+
authMethod?: string
|
|
38
|
+
): Promise<EmployeeProfile> {
|
|
39
|
+
if (!refreshToken) {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const { base, scope } = await resolveEmployeesBackend(config);
|
|
44
|
+
if (!base || !scope) return {};
|
|
45
|
+
|
|
46
|
+
let accessToken: string | undefined;
|
|
47
|
+
if (authMethod === "password") {
|
|
48
|
+
accessToken = (await exchangePasswordToken(refreshToken))?.accessToken;
|
|
49
|
+
} else {
|
|
50
|
+
const entra = (
|
|
51
|
+
await new ConfidentialClientApplication({
|
|
52
|
+
auth: {
|
|
53
|
+
clientId: config.clientId,
|
|
54
|
+
clientSecret: config.clientSecret,
|
|
55
|
+
authority: `https://login.microsoftonline.com/${config.tenantId}`,
|
|
56
|
+
},
|
|
57
|
+
}).acquireTokenByRefreshToken({ scopes: [scope], refreshToken })
|
|
58
|
+
)?.accessToken;
|
|
59
|
+
accessToken = entra
|
|
60
|
+
? (await exchangeSsoToken(entra))?.accessToken
|
|
61
|
+
: undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!accessToken) return {};
|
|
65
|
+
const client = new EmployeesClient(
|
|
66
|
+
new Configuration({
|
|
67
|
+
basePath: base,
|
|
68
|
+
accessToken: `Bearer ${accessToken}`,
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
const me = await client.getMeProtected();
|
|
72
|
+
const profile = {
|
|
73
|
+
colorScheme: isColorScheme(me.settings?.colorScheme)
|
|
74
|
+
? me.settings.colorScheme
|
|
75
|
+
: undefined,
|
|
76
|
+
imageUrl: getSanityImageUrl(me.image?.asset?.url, {
|
|
77
|
+
width: 128,
|
|
78
|
+
height: 128,
|
|
79
|
+
fit: "crop",
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
if (!profile.imageUrl) {
|
|
83
|
+
console.warn("[auth] employee profile resolved without an image");
|
|
84
|
+
}
|
|
85
|
+
return profile;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.warn("[auth] employee profile fetch failed", error);
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import NextAuth, { NextAuthOptions } from "next-auth";
|
|
2
|
+
import { callbacks } from "./callbacks";
|
|
3
|
+
import { cookies } from "./cookies";
|
|
4
|
+
import { providers } from "./providers";
|
|
5
|
+
import { Auth } from "../types";
|
|
6
|
+
|
|
7
|
+
const WEEK_IN_SECONDS = 7 * 24 * 60 * 60;
|
|
8
|
+
const DAY_IN_SECONDS = 24 * 60 * 60;
|
|
9
|
+
|
|
10
|
+
export const getNextAuthOptions = (config: Auth.Config): NextAuthOptions => ({
|
|
11
|
+
secret: config.nextAuthSecret,
|
|
12
|
+
session: { maxAge: WEEK_IN_SECONDS, updateAge: DAY_IN_SECONDS },
|
|
13
|
+
pages: { signIn: "/auth/login" },
|
|
14
|
+
providers: providers(config),
|
|
15
|
+
cookies: cookies(),
|
|
16
|
+
callbacks: callbacks(config),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export default (config: Auth.Config) => NextAuth(getNextAuthOptions(config));
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Provider } from "next-auth/providers/index";
|
|
2
|
+
import AzureADProvider from "next-auth/providers/azure-ad";
|
|
3
|
+
import CredentialsProvider from "next-auth/providers/credentials";
|
|
4
|
+
import { getDefaultAuthParams } from "../options/params";
|
|
5
|
+
import { openHandoff } from "../handoff/ticket";
|
|
6
|
+
import { passwordLogin, redeemBootstrapToken } from "../password/api";
|
|
7
|
+
import { requestClientIp, requestHost } from "./request";
|
|
8
|
+
import { Auth } from "../types";
|
|
9
|
+
|
|
10
|
+
type AuthorizedUser = { id: string };
|
|
11
|
+
|
|
12
|
+
export function providers(config: Auth.Config): Provider[] {
|
|
13
|
+
return [entraId(config), handoff(config), password()];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function entraId(config: Auth.Config) {
|
|
17
|
+
return AzureADProvider({
|
|
18
|
+
clientId: config.clientId,
|
|
19
|
+
clientSecret: config.clientSecret,
|
|
20
|
+
tenantId: config.tenantId,
|
|
21
|
+
authorization: { params: getDefaultAuthParams(config.tenantId) },
|
|
22
|
+
checks: ["pkce", "state"],
|
|
23
|
+
profile(profile) {
|
|
24
|
+
return {
|
|
25
|
+
id: profile.sub,
|
|
26
|
+
name: profile.name,
|
|
27
|
+
email: profile.email,
|
|
28
|
+
image: null,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handoff(config: Auth.Config) {
|
|
35
|
+
return CredentialsProvider({
|
|
36
|
+
id: "handoff",
|
|
37
|
+
name: "Snokam",
|
|
38
|
+
credentials: { ticket: { label: "ticket", type: "text" } },
|
|
39
|
+
async authorize(credentials, req) {
|
|
40
|
+
const expectedBusiness = requestHost(req);
|
|
41
|
+
if (!credentials?.ticket || !expectedBusiness) return null;
|
|
42
|
+
|
|
43
|
+
const claims = openHandoff(
|
|
44
|
+
credentials.ticket,
|
|
45
|
+
config.nextAuthSecret,
|
|
46
|
+
expectedBusiness
|
|
47
|
+
);
|
|
48
|
+
if (!claims) return null;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: claims.oid,
|
|
52
|
+
email: claims.email,
|
|
53
|
+
name: claims.name,
|
|
54
|
+
image: claims.image ?? null,
|
|
55
|
+
oid: claims.oid,
|
|
56
|
+
tid: claims.tid,
|
|
57
|
+
refreshToken: claims.refreshToken,
|
|
58
|
+
authMethod: claims.authMethod || undefined,
|
|
59
|
+
} as unknown as AuthorizedUser;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function password() {
|
|
65
|
+
return CredentialsProvider({
|
|
66
|
+
id: "password",
|
|
67
|
+
name: "Passord",
|
|
68
|
+
credentials: {
|
|
69
|
+
email: { label: "email", type: "text" },
|
|
70
|
+
password: { label: "password", type: "password" },
|
|
71
|
+
code: { label: "code", type: "text" },
|
|
72
|
+
host: { label: "host", type: "text" },
|
|
73
|
+
bootstrap: { label: "bootstrap", type: "text" },
|
|
74
|
+
},
|
|
75
|
+
async authorize(credentials, req) {
|
|
76
|
+
if (credentials?.bootstrap) {
|
|
77
|
+
const redeemed = await redeemBootstrapToken(credentials.bootstrap);
|
|
78
|
+
if (!redeemed) return null;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
id: redeemed.userId,
|
|
82
|
+
email: redeemed.email,
|
|
83
|
+
name: redeemed.name,
|
|
84
|
+
oid: redeemed.userId,
|
|
85
|
+
tid: redeemed.businessId,
|
|
86
|
+
refreshToken: redeemed.refreshToken,
|
|
87
|
+
authMethod: "password",
|
|
88
|
+
} as unknown as AuthorizedUser;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!credentials?.email || !credentials.password || !credentials.host)
|
|
92
|
+
return null;
|
|
93
|
+
|
|
94
|
+
const outcome = await passwordLogin(
|
|
95
|
+
{
|
|
96
|
+
host: credentials.host,
|
|
97
|
+
email: credentials.email,
|
|
98
|
+
password: credentials.password,
|
|
99
|
+
code: credentials.code || undefined,
|
|
100
|
+
},
|
|
101
|
+
requestClientIp(req)
|
|
102
|
+
);
|
|
103
|
+
if (!outcome.login)
|
|
104
|
+
throw new Error(outcome.error ?? "invalid_credentials");
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
id: outcome.login.userId,
|
|
108
|
+
email: outcome.login.email,
|
|
109
|
+
name: outcome.login.name,
|
|
110
|
+
oid: outcome.login.userId,
|
|
111
|
+
tid: outcome.login.businessId,
|
|
112
|
+
refreshToken: outcome.login.refreshToken,
|
|
113
|
+
authMethod: "password",
|
|
114
|
+
} as unknown as AuthorizedUser;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { headers } from "next/headers";
|
|
2
|
+
|
|
3
|
+
type HeaderBag = Record<string, string | string[] | undefined>;
|
|
4
|
+
|
|
5
|
+
function first(bag: HeaderBag | undefined, name: string): string | undefined {
|
|
6
|
+
const header = bag?.[name];
|
|
7
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
8
|
+
return value?.split(",")[0].trim() || undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function requestHost(req?: { headers?: HeaderBag }): string | undefined {
|
|
12
|
+
const header = req?.headers?.["x-forwarded-host"] ?? req?.headers?.host;
|
|
13
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
14
|
+
return value?.split(",")[0].trim().split(":")[0] || undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function requestClientIp(req?: {
|
|
18
|
+
headers?: HeaderBag;
|
|
19
|
+
}): string | undefined {
|
|
20
|
+
// Trusted edge headers first (Front Door, unspoofable); rightmost XFF entry as a fallback.
|
|
21
|
+
const edge =
|
|
22
|
+
first(req?.headers, "x-azure-clientip") ??
|
|
23
|
+
first(req?.headers, "x-azure-socketip");
|
|
24
|
+
if (edge) return edge;
|
|
25
|
+
|
|
26
|
+
const forwarded = req?.headers?.["x-forwarded-for"];
|
|
27
|
+
const value = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
28
|
+
return value?.split(",").pop()?.trim() || undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function requestOrigin(): Promise<string | undefined> {
|
|
32
|
+
const incoming = await headers();
|
|
33
|
+
const host = (incoming.get("x-forwarded-host") ?? incoming.get("host") ?? "")
|
|
34
|
+
.split(",")[0]
|
|
35
|
+
.trim();
|
|
36
|
+
if (!host) return undefined;
|
|
37
|
+
|
|
38
|
+
const scheme =
|
|
39
|
+
incoming.get("x-forwarded-proto") ??
|
|
40
|
+
(host.startsWith("localhost") ? "http" : "https");
|
|
41
|
+
return `${scheme}://${host}`;
|
|
42
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import NextAuth from "next-auth";
|
|
2
2
|
import type { NextAuthOptions } from "next-auth";
|
|
3
|
-
import { resolveCookieDomain } from "
|
|
3
|
+
import { resolveCookieDomain } from "../session";
|
|
4
4
|
|
|
5
5
|
type AuthOptionsFor = NextAuthOptions | ((req: Request) => NextAuthOptions);
|
|
6
6
|
|
|
@@ -36,17 +36,17 @@ export function createAuthHandler(options: AuthOptionsFor) {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export { getNextAuthOptions } from "
|
|
40
|
-
export { createTokenHandler } from "./
|
|
39
|
+
export { getNextAuthOptions } from "../options";
|
|
40
|
+
export { createTokenHandler } from "./token";
|
|
41
41
|
export {
|
|
42
42
|
createImpersonationStartHandler,
|
|
43
43
|
createImpersonationEndHandler,
|
|
44
44
|
createImpersonationAdoptHandler,
|
|
45
|
-
} from "
|
|
46
|
-
export { createImpersonationRouteHandlers } from "
|
|
45
|
+
} from "../impersonation/handlers";
|
|
46
|
+
export { createImpersonationRouteHandlers } from "../impersonation/routes";
|
|
47
47
|
export {
|
|
48
48
|
createHandoffStartHandler,
|
|
49
49
|
createHandoffFinishHandler,
|
|
50
50
|
createHandoffLogoutHandler,
|
|
51
|
-
} from "
|
|
52
|
-
export type { Auth } from "
|
|
51
|
+
} from "../handoff/handlers";
|
|
52
|
+
export type { Auth } from "../types";
|
|
@@ -3,16 +3,14 @@ import {
|
|
|
3
3
|
ConfidentialClientApplication,
|
|
4
4
|
InteractionRequiredAuthError,
|
|
5
5
|
} from "@azure/msal-node";
|
|
6
|
-
import { Auth } from "../
|
|
7
|
-
import { getAuthUrl } from "../
|
|
8
|
-
import { getSessionCookieSettings } from "../utils/cookies";
|
|
6
|
+
import { Auth } from "../types";
|
|
7
|
+
import { getAuthUrl, getSessionCookieSettings } from "../session";
|
|
9
8
|
import { exchangePasswordToken } from "../password/api";
|
|
10
9
|
import { readImpersonationSession } from "../impersonation/cookies";
|
|
11
10
|
import { exchangeSsoToken } from "../sso/exchange";
|
|
12
11
|
|
|
13
12
|
export type TokenResult =
|
|
14
|
-
|
|
15
|
-
| { error: Response };
|
|
13
|
+
{ accessToken: string; expiresOn: number } | { error: Response };
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
16
|
* Resolves the SIGNED-IN admin's own product access token (never the impersonation token). Shared
|
package/src/scopes/matcher.ts
CHANGED
|
@@ -19,7 +19,10 @@ export function scopeGranted(
|
|
|
19
19
|
|
|
20
20
|
const grantedAction = separator < 0 ? "*" : scope.slice(separator + 1);
|
|
21
21
|
if (grantedAction === "*") return true;
|
|
22
|
-
if (
|
|
22
|
+
if (
|
|
23
|
+
access === "read" &&
|
|
24
|
+
(grantedAction === "read" || grantedAction === "write")
|
|
25
|
+
)
|
|
23
26
|
return true;
|
|
24
27
|
if (access === "write" && grantedAction === "write") return true;
|
|
25
28
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import { Configuration, EmployeesClient } from "@snokam/employees-client";
|
|
5
|
-
import { useOptionalConfig } from "@snokam/
|
|
5
|
+
import { useOptionalConfig } from "@snokam/tenant";
|
|
6
6
|
import { useClientQuery } from "@snokam/react-query";
|
|
7
7
|
import { useAdminTheme } from "@snokam/ui/theme/admin";
|
|
8
8
|
import type { ColorSchemeMode } from "@snokam/ui/theme/admin";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCipheriv,
|
|
3
|
+
createDecipheriv,
|
|
4
|
+
createHash,
|
|
5
|
+
randomBytes,
|
|
6
|
+
} from "crypto";
|
|
7
|
+
|
|
8
|
+
const ALGORITHM = "aes-256-gcm";
|
|
9
|
+
|
|
10
|
+
interface OpenOptions {
|
|
11
|
+
expectedBusiness?: string;
|
|
12
|
+
now?: number;
|
|
13
|
+
onReject?: (reason: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function keyFromSecret(secret: string, purpose: string): Buffer {
|
|
17
|
+
if (!secret)
|
|
18
|
+
throw new Error(
|
|
19
|
+
`NEXTAUTH_SECRET is empty; refusing to seal/open a forgeable ${purpose} ticket.`
|
|
20
|
+
);
|
|
21
|
+
return createHash("sha256").update(secret).digest();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function seal<T extends object>(
|
|
25
|
+
claims: T,
|
|
26
|
+
secret: string,
|
|
27
|
+
ttlSeconds: number,
|
|
28
|
+
purpose: string
|
|
29
|
+
): string {
|
|
30
|
+
const payload = JSON.stringify({
|
|
31
|
+
...claims,
|
|
32
|
+
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
|
|
33
|
+
});
|
|
34
|
+
const iv = randomBytes(12);
|
|
35
|
+
const cipher = createCipheriv(ALGORITHM, keyFromSecret(secret, purpose), iv);
|
|
36
|
+
const ciphertext = Buffer.concat([
|
|
37
|
+
cipher.update(payload, "utf8"),
|
|
38
|
+
cipher.final(),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
return [iv, cipher.getAuthTag(), ciphertext]
|
|
42
|
+
.map((part) => part.toString("base64url"))
|
|
43
|
+
.join(".");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function open<T extends { business: string }>(
|
|
47
|
+
ticket: string,
|
|
48
|
+
secret: string,
|
|
49
|
+
purpose: string,
|
|
50
|
+
options: OpenOptions = {}
|
|
51
|
+
): T | null {
|
|
52
|
+
const { expectedBusiness, now = Date.now(), onReject } = options;
|
|
53
|
+
const reject = (reason: string): null => {
|
|
54
|
+
onReject?.(reason);
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const parts = ticket.split(".");
|
|
59
|
+
if (parts.length !== 3)
|
|
60
|
+
return reject("malformed ticket (expected iv.tag.ciphertext)");
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const [iv, tag, ciphertext] = parts.map((part) =>
|
|
64
|
+
Buffer.from(part, "base64url")
|
|
65
|
+
);
|
|
66
|
+
const decipher = createDecipheriv(
|
|
67
|
+
ALGORITHM,
|
|
68
|
+
keyFromSecret(secret, purpose),
|
|
69
|
+
iv
|
|
70
|
+
);
|
|
71
|
+
decipher.setAuthTag(tag);
|
|
72
|
+
const plaintext = Buffer.concat([
|
|
73
|
+
decipher.update(ciphertext),
|
|
74
|
+
decipher.final(),
|
|
75
|
+
]).toString("utf8");
|
|
76
|
+
|
|
77
|
+
const payload = JSON.parse(plaintext) as T & { exp: number };
|
|
78
|
+
if (payload.exp * 1000 < now)
|
|
79
|
+
return reject(
|
|
80
|
+
`expired (exp ${payload.exp}, now ${Math.floor(now / 1000)})`
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (expectedBusiness && payload.business !== expectedBusiness)
|
|
84
|
+
return reject(
|
|
85
|
+
`business mismatch (ticket "${payload.business}" vs host "${expectedBusiness}")`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const { exp: _exp, ...claims } = payload;
|
|
89
|
+
return claims as unknown as T;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return reject(
|
|
92
|
+
`decrypt failed (secret mismatch or tampered): ${(error as Error).message}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/index.tsx
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { Provider } from "./page-router/components/Provider";
|
|
2
|
-
import { Authenticate } from "./components/Authenticate";
|
|
3
|
-
import { Login } from "./page-router/components/Login";
|
|
4
|
-
import { Auth } from "./utils/types";
|
|
5
|
-
import { Error } from "./components/Error";
|
|
6
|
-
|
|
7
|
-
export { Unauthenticated } from "./components/Unauthenticated";
|
|
8
|
-
export { Authenticated } from "./components/Authenticated";
|
|
9
|
-
|
|
10
|
-
export { useAuth, clearAuthTokenCache } from "./hooks";
|
|
11
|
-
export { ScopesProvider, useScopes, useHasScope } from "./scopes/context";
|
|
12
|
-
export type { ScopesState } from "./scopes/context";
|
|
13
|
-
export {
|
|
14
|
-
scopeGranted,
|
|
15
|
-
resolveEmployeeScopes,
|
|
16
|
-
type ScopeAccess,
|
|
17
|
-
} from "./scopes/matcher";
|
|
18
|
-
export { Consent } from "./components/Consent";
|
|
19
|
-
export { buildAdminConsentUrl } from "./utils/consent";
|
|
20
|
-
export type { AdminConsentParams } from "./utils/consent";
|
|
21
|
-
export { default as middleware } from "next-auth/middleware";
|
|
22
|
-
export { signIn } from "next-auth/react";
|
|
23
|
-
|
|
24
|
-
export default {
|
|
25
|
-
Login,
|
|
26
|
-
Error,
|
|
27
|
-
Provider,
|
|
28
|
-
Authenticate,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type { Auth };
|