@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
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snokam/auth",
|
|
3
3
|
"description": "Snøkam auth components",
|
|
4
|
-
"version": "
|
|
5
|
-
"main": "src/index.
|
|
6
|
-
"types": "src/index.
|
|
4
|
+
"version": "3.0.2",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"src",
|
|
9
9
|
"package.json",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"@snokam/employees-client": "^1.3.0",
|
|
26
26
|
"next-auth": "4.24.15",
|
|
27
27
|
"path-to-regexp": "^8.4.2",
|
|
28
|
-
"@snokam/
|
|
28
|
+
"@snokam/platform-client": "2.0.0",
|
|
29
29
|
"@snokam/identity": "1.1.0",
|
|
30
|
-
"@snokam/platform-client": "1.0.0",
|
|
31
30
|
"@snokam/react-query": "1.1.0",
|
|
32
|
-
"@snokam/
|
|
31
|
+
"@snokam/tenant": "2.0.1",
|
|
32
|
+
"@snokam/ui": "2.0.3"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"next": "
|
|
35
|
+
"next": "16.3.3",
|
|
36
36
|
"react": "19.2.8",
|
|
37
37
|
"react-dom": "19.2.8"
|
|
38
38
|
},
|
|
@@ -41,13 +41,10 @@
|
|
|
41
41
|
"@types/react-dom": "^19.2.3"
|
|
42
42
|
},
|
|
43
43
|
"exports": {
|
|
44
|
-
".": "./src/index.
|
|
44
|
+
".": "./src/index.ts",
|
|
45
45
|
"./config": "./src/config.ts",
|
|
46
|
-
"./
|
|
47
|
-
"./
|
|
48
|
-
"./route-handler": "./src/route-handler.ts",
|
|
49
|
-
"./page-router": "./src/page-router/index.tsx",
|
|
50
|
-
"./page-router/server": "./src/page-router/server.ts",
|
|
46
|
+
"./middleware": "./src/middleware.tsx",
|
|
47
|
+
"./route-handler": "./src/route-handler/index.ts",
|
|
51
48
|
"./app-router": "./src/app-router/index.tsx",
|
|
52
49
|
"./app-router/server": "./src/app-router/server.ts",
|
|
53
50
|
"./password": "./src/password/api.ts",
|
|
@@ -5,8 +5,8 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|
|
5
5
|
import { useAuth } from "../../hooks";
|
|
6
6
|
import { signIn } from "next-auth/react";
|
|
7
7
|
import { PageLoader } from "@snokam/ui";
|
|
8
|
-
import { useOptionalConfig } from "@snokam/
|
|
9
|
-
import { safeCallbackTarget } from "../../
|
|
8
|
+
import { useOptionalConfig } from "@snokam/tenant";
|
|
9
|
+
import { safeCallbackTarget } from "../../session";
|
|
10
10
|
import Error from "../../components/Error";
|
|
11
11
|
import Consent from "../../components/Consent";
|
|
12
12
|
import LoggedOut from "../../components/LoggedOut";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { memo, ReactNode, Suspense, useEffect } from "react";
|
|
4
4
|
import { useSearchParams } from "next/navigation";
|
|
5
|
-
import { Auth } from "../../../
|
|
5
|
+
import { Auth } from "../../../types";
|
|
6
6
|
import { Authenticate } from "../../../components/Authenticate";
|
|
7
7
|
import { SessionProvider } from "next-auth/react";
|
|
8
8
|
import { useAuth } from "../../../hooks";
|
package/src/app-router/index.tsx
CHANGED
|
@@ -12,7 +12,7 @@ export { Unauthenticated } from "../components/Unauthenticated";
|
|
|
12
12
|
export { Authenticated } from "../components/Authenticated";
|
|
13
13
|
export { useAuth } from "../hooks";
|
|
14
14
|
export { EmployeeThemeSync } from "../theme/employee-theme-sync";
|
|
15
|
-
export { ScopesProvider, useScopes, useHasScope } from "../scopes
|
|
15
|
+
export { ScopesProvider, useScopes, useHasScope } from "../scopes";
|
|
16
16
|
export {
|
|
17
17
|
usePlatformRoles,
|
|
18
18
|
useHasPlatformRole,
|
|
@@ -20,12 +20,12 @@ export {
|
|
|
20
20
|
PLATFORM_OPERATOR,
|
|
21
21
|
} from "../roles/context";
|
|
22
22
|
export type { RolesState } from "../roles/context";
|
|
23
|
-
export type { ScopesState } from "../scopes
|
|
23
|
+
export type { ScopesState } from "../scopes";
|
|
24
24
|
export {
|
|
25
25
|
scopeGranted,
|
|
26
26
|
resolveEmployeeScopes,
|
|
27
27
|
type ScopeAccess,
|
|
28
|
-
} from "../scopes
|
|
28
|
+
} from "../scopes";
|
|
29
29
|
export {
|
|
30
30
|
ImpersonationProvider,
|
|
31
31
|
useImpersonation,
|
|
@@ -53,4 +53,4 @@ export default {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
export type { Session } from "next-auth";
|
|
56
|
-
export type { Auth } from "../
|
|
56
|
+
export type { Auth } from "../types";
|
package/src/app-router/server.ts
CHANGED
|
@@ -3,78 +3,30 @@ import {
|
|
|
3
3
|
type Session,
|
|
4
4
|
} from "next-auth";
|
|
5
5
|
import type { NextAuthOptions } from "next-auth";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
apiScope,
|
|
8
|
+
appUrl,
|
|
9
|
+
backendUrl,
|
|
10
|
+
managedBearer,
|
|
11
|
+
} from "@snokam/tenant/server";
|
|
9
12
|
|
|
10
|
-
export { getServerToken } from "
|
|
13
|
+
export { getServerToken } from "./token";
|
|
11
14
|
export { getNextAuthOptions } from "../options";
|
|
12
|
-
export type { Auth } from "../
|
|
13
|
-
|
|
14
|
-
export async function serverTenantToken(): Promise<string | undefined> {
|
|
15
|
-
const scope = serverApiScope();
|
|
16
|
-
if (!scope) return undefined;
|
|
17
|
-
return getManagedBearer(scope);
|
|
18
|
-
}
|
|
15
|
+
export type { Auth } from "../types";
|
|
19
16
|
|
|
20
|
-
export
|
|
21
|
-
if (!service) return "";
|
|
22
|
-
const envKey = `NEXT_PUBLIC_${service.toUpperCase().replace(/-/g, "_")}_API_BASE_URL`;
|
|
23
|
-
const override = process.env[envKey];
|
|
24
|
-
if (
|
|
25
|
-
override &&
|
|
26
|
-
(override.includes("localhost") || override.includes("127.0.0.1"))
|
|
27
|
-
) {
|
|
28
|
-
return override;
|
|
29
|
-
}
|
|
17
|
+
export const serverApiScope = apiScope;
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const domains = await getTenantDomains({
|
|
35
|
-
host,
|
|
36
|
-
platformApiBase: process.env.NEXT_PUBLIC_PLATFORM_API_BASE_URL ?? "",
|
|
37
|
-
authToken: await serverTenantToken(),
|
|
38
|
-
});
|
|
39
|
-
const custom = domains.find(
|
|
40
|
-
(domain) => domain.type === "backend" && domain.env === envKey
|
|
41
|
-
);
|
|
42
|
-
if (custom?.url) return custom.url;
|
|
43
|
-
|
|
44
|
-
const bare = host?.split(":")[0];
|
|
45
|
-
const tenantHost = (
|
|
46
|
-
bare === "localhost" || bare === "127.0.0.1"
|
|
47
|
-
? process.env.NEXT_PUBLIC_TENANT_HOST
|
|
48
|
-
: host
|
|
49
|
-
)?.split(":")[0];
|
|
50
|
-
return tenantHost ? `https://${tenantHost}/api/${service}` : (override ?? "");
|
|
19
|
+
export function serverTenantToken(): Promise<string | undefined> {
|
|
20
|
+
const scope = serverApiScope();
|
|
21
|
+
return scope ? managedBearer(scope) : Promise.resolve(undefined);
|
|
51
22
|
}
|
|
52
23
|
|
|
53
|
-
export function
|
|
54
|
-
|
|
55
|
-
return clientId ? `api://${clientId}/user_impersonation` : "";
|
|
24
|
+
export function resolveServerApiBase(service: string): Promise<string> {
|
|
25
|
+
return service ? backendUrl(service) : Promise.resolve("");
|
|
56
26
|
}
|
|
57
27
|
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
const host =
|
|
61
|
-
requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host");
|
|
62
|
-
const domains = await getTenantDomains({
|
|
63
|
-
host,
|
|
64
|
-
platformApiBase: process.env.NEXT_PUBLIC_PLATFORM_API_BASE_URL ?? "",
|
|
65
|
-
authToken: await serverTenantToken(),
|
|
66
|
-
});
|
|
67
|
-
const match = domains.find(
|
|
68
|
-
(domain) => domain.type === "frontend" && domain.app === app
|
|
69
|
-
);
|
|
70
|
-
if (match?.url) return match.url;
|
|
71
|
-
|
|
72
|
-
const bare = host?.split(":")[0];
|
|
73
|
-
const effective =
|
|
74
|
-
!bare || bare === "localhost" || bare === "127.0.0.1"
|
|
75
|
-
? process.env.NEXT_PUBLIC_TENANT_HOST?.split(":")[0]
|
|
76
|
-
: host;
|
|
77
|
-
return effective ? `https://${effective}` : "";
|
|
28
|
+
export function resolveServerAppUrl(app: string): Promise<string> {
|
|
29
|
+
return appUrl(app);
|
|
78
30
|
}
|
|
79
31
|
|
|
80
32
|
export async function getServerSession(
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { memo, useEffect } from "react";
|
|
2
|
-
import { Auth } from "../
|
|
2
|
+
import { Auth } from "../types";
|
|
3
3
|
import { signIn } from "next-auth/react";
|
|
4
4
|
import { useAuth } from "../hooks";
|
|
5
5
|
import { PageLoader } from "@snokam/ui";
|
|
6
|
-
import { useOptionalConfig } from "@snokam/
|
|
6
|
+
import { useOptionalConfig } from "@snokam/tenant";
|
|
7
7
|
|
|
8
8
|
export const Authenticate = memo((props: Auth.Authenticate.Props) => {
|
|
9
9
|
const { children, provider } = props;
|
|
@@ -5,7 +5,7 @@ import Styled from "./styled";
|
|
|
5
5
|
import { Layout } from "@snokam/ui";
|
|
6
6
|
import { TransitionType } from "@snokam/ui/layout";
|
|
7
7
|
import { useAuth } from "../../hooks";
|
|
8
|
-
import { useOptionalConfig } from "@snokam/
|
|
8
|
+
import { useOptionalConfig } from "@snokam/tenant";
|
|
9
9
|
import Button from "@mui/material/Button";
|
|
10
10
|
import { Avatar, Alert } from "@mui/material";
|
|
11
11
|
|
package/src/config.ts
CHANGED
package/src/handoff/handlers.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { getToken } from "next-auth/jwt";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
getSessionCookieSettings,
|
|
4
|
+
getAuthUrl,
|
|
5
|
+
requestOrigin,
|
|
6
|
+
safeCallbackTarget,
|
|
7
|
+
} from "../session";
|
|
4
8
|
import { sealHandoff } from "./ticket";
|
|
5
9
|
import { isAllowedReturnTo } from "./return-to";
|
|
6
|
-
import { safeCallbackTarget } from "../utils/callback-url";
|
|
7
10
|
import { revokePasswordToken } from "../password/api";
|
|
8
11
|
|
|
9
12
|
interface HandoffConfig {
|
package/src/handoff/return-to.ts
CHANGED
package/src/handoff/ticket.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createCipheriv,
|
|
3
|
-
createDecipheriv,
|
|
4
|
-
createHash,
|
|
5
|
-
randomBytes,
|
|
6
|
-
} from "crypto";
|
|
1
|
+
import { open, seal } from "../ticket/seal";
|
|
7
2
|
|
|
8
3
|
export interface HandoffClaims {
|
|
9
4
|
oid: string;
|
|
@@ -16,40 +11,14 @@ export interface HandoffClaims {
|
|
|
16
11
|
image?: string;
|
|
17
12
|
}
|
|
18
13
|
|
|
19
|
-
const ALGORITHM = "aes-256-gcm";
|
|
20
14
|
const DEFAULT_TTL_SECONDS = 300;
|
|
21
15
|
|
|
22
|
-
function rejectHandoff(reason: string): null {
|
|
23
|
-
console.warn(`[handoff] ticket rejected: ${reason}`);
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const keyFromSecret = (secret: string): Buffer => {
|
|
28
|
-
if (!secret)
|
|
29
|
-
throw new Error(
|
|
30
|
-
"Hand-off secret (NEXTAUTH_SECRET) is empty; refusing to seal/open a forgeable ticket."
|
|
31
|
-
);
|
|
32
|
-
return createHash("sha256").update(secret).digest();
|
|
33
|
-
};
|
|
34
|
-
|
|
35
16
|
export function sealHandoff(
|
|
36
17
|
claims: HandoffClaims,
|
|
37
18
|
secret: string,
|
|
38
19
|
ttlSeconds: number = DEFAULT_TTL_SECONDS
|
|
39
20
|
): string {
|
|
40
|
-
|
|
41
|
-
...claims,
|
|
42
|
-
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
|
|
43
|
-
});
|
|
44
|
-
const iv = randomBytes(12);
|
|
45
|
-
const cipher = createCipheriv(ALGORITHM, keyFromSecret(secret), iv);
|
|
46
|
-
const ciphertext = Buffer.concat([
|
|
47
|
-
cipher.update(payload, "utf8"),
|
|
48
|
-
cipher.final(),
|
|
49
|
-
]);
|
|
50
|
-
return [iv, cipher.getAuthTag(), ciphertext]
|
|
51
|
-
.map((part) => part.toString("base64url"))
|
|
52
|
-
.join(".");
|
|
21
|
+
return seal(claims, secret, ttlSeconds, "hand-off");
|
|
53
22
|
}
|
|
54
23
|
|
|
55
24
|
export function openHandoff(
|
|
@@ -58,40 +27,9 @@ export function openHandoff(
|
|
|
58
27
|
expectedBusiness?: string,
|
|
59
28
|
now: number = Date.now()
|
|
60
29
|
): HandoffClaims | null {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const [iv, tag, ciphertext] = parts.map((part) =>
|
|
68
|
-
Buffer.from(part, "base64url")
|
|
69
|
-
);
|
|
70
|
-
const decipher = createDecipheriv(ALGORITHM, keyFromSecret(secret), iv);
|
|
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 HandoffClaims & { exp: number };
|
|
78
|
-
if (payload.exp * 1000 < now) {
|
|
79
|
-
return rejectHandoff(
|
|
80
|
-
`expired (exp ${payload.exp}, now ${Math.floor(now / 1000)})`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (expectedBusiness && payload.business !== expectedBusiness) {
|
|
85
|
-
return rejectHandoff(
|
|
86
|
-
`business mismatch (ticket "${payload.business}" vs host "${expectedBusiness}")`
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const { exp: _exp, ...claims } = payload;
|
|
91
|
-
return claims;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
return rejectHandoff(
|
|
94
|
-
`decrypt failed (secret mismatch or tampered): ${(error as Error).message}`
|
|
95
|
-
);
|
|
96
|
-
}
|
|
30
|
+
return open<HandoffClaims>(ticket, secret, "hand-off", {
|
|
31
|
+
expectedBusiness,
|
|
32
|
+
now,
|
|
33
|
+
onReject: (reason) => console.warn(`[handoff] ticket rejected: ${reason}`),
|
|
34
|
+
});
|
|
97
35
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Auth } from "../
|
|
1
|
+
import { Auth } from "../types";
|
|
2
2
|
import { resolveAccessToken } from "../route-handler/token";
|
|
3
3
|
import {
|
|
4
4
|
buildImpersonationCookies,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
readImpersonationSession,
|
|
7
7
|
} from "./cookies";
|
|
8
8
|
import { sealImpersonationTicket, openImpersonationTicket } from "./ticket";
|
|
9
|
-
import { requestOrigin } from "../
|
|
9
|
+
import { requestOrigin } from "../session";
|
|
10
10
|
|
|
11
11
|
function platformBase(): string | null {
|
|
12
12
|
return (
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createCipheriv,
|
|
3
|
-
createDecipheriv,
|
|
4
|
-
createHash,
|
|
5
|
-
randomBytes,
|
|
6
|
-
} from "crypto";
|
|
1
|
+
import { open, seal } from "../ticket/seal";
|
|
7
2
|
|
|
8
|
-
// Cross-domain Vis som hand-off, sealed with the shared NEXTAUTH_SECRET — same trust model as login's ticket.
|
|
9
3
|
export interface ImpersonationTicket {
|
|
10
4
|
accessToken: string;
|
|
11
5
|
expiresOn: number;
|
|
@@ -17,35 +11,14 @@ export interface ImpersonationTicket {
|
|
|
17
11
|
returnTo?: string;
|
|
18
12
|
}
|
|
19
13
|
|
|
20
|
-
const ALGORITHM = "aes-256-gcm";
|
|
21
14
|
const TTL_SECONDS = 120;
|
|
22
15
|
|
|
23
|
-
const keyFromSecret = (secret: string): Buffer => {
|
|
24
|
-
if (!secret)
|
|
25
|
-
throw new Error(
|
|
26
|
-
"NEXTAUTH_SECRET is empty; refusing to seal/open a forgeable impersonation ticket."
|
|
27
|
-
);
|
|
28
|
-
return createHash("sha256").update(secret).digest();
|
|
29
|
-
};
|
|
30
|
-
|
|
31
16
|
export function sealImpersonationTicket(
|
|
32
17
|
claims: ImpersonationTicket,
|
|
33
18
|
secret: string,
|
|
34
19
|
ttlSeconds: number = TTL_SECONDS
|
|
35
20
|
): string {
|
|
36
|
-
|
|
37
|
-
...claims,
|
|
38
|
-
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
|
|
39
|
-
});
|
|
40
|
-
const iv = randomBytes(12);
|
|
41
|
-
const cipher = createCipheriv(ALGORITHM, keyFromSecret(secret), iv);
|
|
42
|
-
const ciphertext = Buffer.concat([
|
|
43
|
-
cipher.update(payload, "utf8"),
|
|
44
|
-
cipher.final(),
|
|
45
|
-
]);
|
|
46
|
-
return [iv, cipher.getAuthTag(), ciphertext]
|
|
47
|
-
.map((part) => part.toString("base64url"))
|
|
48
|
-
.join(".");
|
|
21
|
+
return seal(claims, secret, ttlSeconds, "impersonation");
|
|
49
22
|
}
|
|
50
23
|
|
|
51
24
|
export function openImpersonationTicket(
|
|
@@ -54,26 +27,8 @@ export function openImpersonationTicket(
|
|
|
54
27
|
expectedBusiness?: string,
|
|
55
28
|
now: number = Date.now()
|
|
56
29
|
): ImpersonationTicket | null {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Buffer.from(part, "base64url")
|
|
62
|
-
);
|
|
63
|
-
const decipher = createDecipheriv(ALGORITHM, keyFromSecret(secret), iv);
|
|
64
|
-
decipher.setAuthTag(tag);
|
|
65
|
-
const plaintext = Buffer.concat([
|
|
66
|
-
decipher.update(ciphertext),
|
|
67
|
-
decipher.final(),
|
|
68
|
-
]).toString("utf8");
|
|
69
|
-
const payload = JSON.parse(plaintext) as ImpersonationTicket & {
|
|
70
|
-
exp: number;
|
|
71
|
-
};
|
|
72
|
-
if (payload.exp * 1000 < now) return null;
|
|
73
|
-
if (expectedBusiness && payload.business !== expectedBusiness) return null;
|
|
74
|
-
const { exp: _exp, ...claims } = payload;
|
|
75
|
-
return claims;
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
30
|
+
return open<ImpersonationTicket>(ticket, secret, "impersonation", {
|
|
31
|
+
expectedBusiness,
|
|
32
|
+
now,
|
|
33
|
+
});
|
|
79
34
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./components";
|
|
2
|
+
export * from "./scopes";
|
|
3
|
+
|
|
4
|
+
export { useAuth, clearAuthTokenCache } from "./hooks";
|
|
5
|
+
|
|
6
|
+
export { buildAdminConsentUrl } from "./consent";
|
|
7
|
+
export type { AdminConsentParams } from "./consent";
|
|
8
|
+
export type { Auth } from "./types";
|
|
9
|
+
|
|
10
|
+
export { default as middleware } from "next-auth/middleware";
|
|
11
|
+
export { signIn } from "next-auth/react";
|
|
@@ -2,10 +2,9 @@ import { match } from "path-to-regexp";
|
|
|
2
2
|
import { getToken } from "next-auth/jwt";
|
|
3
3
|
import { type NextRequestWithAuth } from "next-auth/middleware";
|
|
4
4
|
import { NextResponse, NextFetchEvent } from "next/server";
|
|
5
|
-
import { Auth } from "./
|
|
6
|
-
import { getSessionCookieSettings } from "./
|
|
7
|
-
import { addCorsHeaders } from "./
|
|
8
|
-
import { getAuthUrl } from "./utils/url";
|
|
5
|
+
import { Auth } from "./types";
|
|
6
|
+
import { getSessionCookieSettings, getAuthUrl } from "./session";
|
|
7
|
+
import { addCorsHeaders } from "./cors";
|
|
9
8
|
|
|
10
9
|
export function buildAuthMiddleware({
|
|
11
10
|
matcher = [],
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { NextAuthOptions } from "next-auth";
|
|
2
|
+
import {
|
|
3
|
+
fetchEmployeeProfile,
|
|
4
|
+
isColorScheme,
|
|
5
|
+
type ColorScheme,
|
|
6
|
+
} from "./employee-profile";
|
|
7
|
+
import { requestOrigin } from "./request";
|
|
8
|
+
import { Auth } from "../types";
|
|
9
|
+
|
|
10
|
+
interface HandoffUser {
|
|
11
|
+
oid?: string;
|
|
12
|
+
tid?: string;
|
|
13
|
+
refreshToken?: string;
|
|
14
|
+
authMethod?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function callbacks(config: Auth.Config): NextAuthOptions["callbacks"] {
|
|
18
|
+
return {
|
|
19
|
+
async redirect({ url, baseUrl }) {
|
|
20
|
+
const origin = (await requestOrigin()) ?? baseUrl;
|
|
21
|
+
const { pathname, search, hash } = new URL(url, origin);
|
|
22
|
+
return origin + pathname + search + hash;
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async jwt({ token, account, profile, user, trigger, session }) {
|
|
26
|
+
if (account?.access_token) {
|
|
27
|
+
token.refreshToken = account.refresh_token;
|
|
28
|
+
token.expiresAt = account.expires_at;
|
|
29
|
+
const claims = profile as { oid?: string; tid?: string } | undefined;
|
|
30
|
+
if (claims?.oid) token.oid = claims.oid;
|
|
31
|
+
if (claims?.tid) token.tid = claims.tid;
|
|
32
|
+
const employee = await fetchEmployeeProfile(
|
|
33
|
+
{ ...config, tenantId: claims?.tid ?? config.tenantId },
|
|
34
|
+
account.refresh_token as string | undefined
|
|
35
|
+
);
|
|
36
|
+
if (employee.colorScheme) token.colorScheme = employee.colorScheme;
|
|
37
|
+
token.picture = employee.imageUrl ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const handoff = user as HandoffUser | undefined;
|
|
41
|
+
if (handoff?.refreshToken) {
|
|
42
|
+
token.oid = handoff.oid;
|
|
43
|
+
token.tid = handoff.tid;
|
|
44
|
+
token.refreshToken = handoff.refreshToken;
|
|
45
|
+
if (handoff.authMethod) token.authMethod = handoff.authMethod;
|
|
46
|
+
|
|
47
|
+
const employee = await fetchEmployeeProfile(
|
|
48
|
+
{ ...config, tenantId: handoff.tid ?? config.tenantId },
|
|
49
|
+
handoff.refreshToken,
|
|
50
|
+
handoff.authMethod
|
|
51
|
+
);
|
|
52
|
+
if (employee.colorScheme) token.colorScheme = employee.colorScheme;
|
|
53
|
+
if (employee.imageUrl) token.picture = employee.imageUrl;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (trigger === "update") {
|
|
57
|
+
if (isColorScheme(session?.colorScheme))
|
|
58
|
+
token.colorScheme = session.colorScheme;
|
|
59
|
+
|
|
60
|
+
if (session?.refreshProfile && token.refreshToken) {
|
|
61
|
+
const employee = await fetchEmployeeProfile(
|
|
62
|
+
{
|
|
63
|
+
...config,
|
|
64
|
+
tenantId: (token.tid as string | undefined) ?? config.tenantId,
|
|
65
|
+
},
|
|
66
|
+
token.refreshToken as string,
|
|
67
|
+
token.authMethod as string | undefined
|
|
68
|
+
);
|
|
69
|
+
if (employee.imageUrl) token.picture = employee.imageUrl;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return token;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async session({ session, token }) {
|
|
77
|
+
const claims = token as {
|
|
78
|
+
colorScheme?: unknown;
|
|
79
|
+
oid?: string;
|
|
80
|
+
tid?: string;
|
|
81
|
+
authMethod?: string;
|
|
82
|
+
};
|
|
83
|
+
const target = session as {
|
|
84
|
+
colorScheme?: ColorScheme;
|
|
85
|
+
oid?: string;
|
|
86
|
+
tid?: string;
|
|
87
|
+
authMethod?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (isColorScheme(claims.colorScheme))
|
|
91
|
+
target.colorScheme = claims.colorScheme;
|
|
92
|
+
target.oid = claims.oid;
|
|
93
|
+
target.tid = claims.tid;
|
|
94
|
+
if (claims.authMethod) target.authMethod = claims.authMethod;
|
|
95
|
+
|
|
96
|
+
return session;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NextAuthOptions } from "next-auth";
|
|
2
|
+
import { getSessionCookieSettings, getAuthUrl } from "../session";
|
|
3
|
+
|
|
4
|
+
export function cookies(): NextAuthOptions["cookies"] {
|
|
5
|
+
const { name, domain, secure } = getSessionCookieSettings(getAuthUrl());
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
sessionToken: {
|
|
9
|
+
name,
|
|
10
|
+
options: { path: "/", httpOnly: true, sameSite: "lax", secure, domain },
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|