@snokam/auth 2.3.2 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +650 -0
  2. package/package.json +11 -14
  3. package/src/app-router/components/Login.tsx +2 -2
  4. package/src/app-router/components/Provider/index.tsx +1 -1
  5. package/src/app-router/index.tsx +4 -4
  6. package/src/app-router/server.ts +16 -64
  7. package/src/components/Authenticate.tsx +2 -2
  8. package/src/components/Consent/index.tsx +1 -1
  9. package/src/components/Error/index.tsx +1 -1
  10. package/src/components/index.ts +5 -0
  11. package/src/config.ts +1 -1
  12. package/src/handoff/handlers.ts +6 -3
  13. package/src/handoff/return-to.ts +1 -1
  14. package/src/handoff/ticket.ts +7 -69
  15. package/src/impersonation/cookies.ts +1 -1
  16. package/src/impersonation/handlers.ts +2 -2
  17. package/src/impersonation/routes.ts +1 -1
  18. package/src/impersonation/ticket.ts +6 -51
  19. package/src/index.ts +11 -0
  20. package/src/{build-proxy.tsx → middleware.tsx} +3 -4
  21. package/src/options/callbacks.ts +99 -0
  22. package/src/options/cookies.ts +13 -0
  23. package/src/options/employee-profile.ts +90 -0
  24. package/src/options/index.tsx +19 -0
  25. package/src/options/providers.ts +117 -0
  26. package/src/options/request.ts +42 -0
  27. package/src/{route-handler.ts → route-handler/index.ts} +7 -7
  28. package/src/route-handler/token.ts +3 -5
  29. package/src/scopes/index.ts +8 -0
  30. package/src/scopes/matcher.ts +4 -1
  31. package/src/session/index.ts +3 -0
  32. package/src/theme/employee-theme-sync.tsx +1 -1
  33. package/src/ticket/seal.ts +95 -0
  34. package/src/index.tsx +0 -31
  35. package/src/options.tsx +0 -389
  36. package/src/page-router/components/Login.tsx +0 -50
  37. package/src/page-router/components/Provider/index.tsx +0 -45
  38. package/src/page-router/index.tsx +0 -22
  39. package/src/page-router/server.ts +0 -6
  40. package/src/server/auth.tsx +0 -30
  41. package/src/server/handler.tsx +0 -68
  42. package/src/server.tsx +0 -16
  43. /package/src/{server → app-router}/token.tsx +0 -0
  44. /package/src/{utils/consent.ts → consent.ts} +0 -0
  45. /package/src/{utils/cors.ts → cors.ts} +0 -0
  46. /package/src/{utils → options}/params.ts +0 -0
  47. /package/src/{utils → session}/callback-url.ts +0 -0
  48. /package/src/{utils → session}/cookies.ts +0 -0
  49. /package/src/{utils → session}/url.ts +0 -0
  50. /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": "2.3.2",
5
- "main": "src/index.tsx",
6
- "types": "src/index.tsx",
4
+ "version": "3.0.1",
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/config": "1.1.0",
29
- "@snokam/identity": "1.1.0",
30
- "@snokam/platform-client": "1.0.0",
28
+ "@snokam/platform-client": "2.0.0",
31
29
  "@snokam/react-query": "1.1.0",
32
- "@snokam/ui": "1.1.1"
30
+ "@snokam/tenant": "2.0.0",
31
+ "@snokam/identity": "1.1.0",
32
+ "@snokam/ui": "2.0.2"
33
33
  },
34
34
  "peerDependencies": {
35
- "next": "16.2.11",
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.tsx",
44
+ ".": "./src/index.ts",
45
45
  "./config": "./src/config.ts",
46
- "./server": "./src/server.tsx",
47
- "./middleware": "./src/build-proxy.tsx",
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/config";
9
- import { safeCallbackTarget } from "../../utils/callback-url";
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 "../../../utils/types";
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";
@@ -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/context";
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/context";
23
+ export type { ScopesState } from "../scopes";
24
24
  export {
25
25
  scopeGranted,
26
26
  resolveEmployeeScopes,
27
27
  type ScopeAccess,
28
- } from "../scopes/matcher";
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 "../utils/types";
56
+ export type { Auth } from "../types";
@@ -3,78 +3,30 @@ import {
3
3
  type Session,
4
4
  } from "next-auth";
5
5
  import type { NextAuthOptions } from "next-auth";
6
- import { headers } from "next/headers";
7
- import { getTenantDomains } from "@snokam/config";
8
- import { getManagedBearer } from "@snokam/identity";
6
+ import {
7
+ apiScope,
8
+ appUrl,
9
+ backendUrl,
10
+ managedBearer,
11
+ } from "@snokam/tenant/server";
9
12
 
10
- export { getServerToken } from "../server/token";
13
+ export { getServerToken } from "./token";
11
14
  export { getNextAuthOptions } from "../options";
12
- export type { Auth } from "../utils/types";
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 async function resolveServerApiBase(service: string): Promise<string> {
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
- const requestHeaders = await headers();
32
- const host =
33
- requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host");
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 serverApiScope(): string {
54
- const clientId = process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID ?? "";
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 async function resolveServerAppUrl(app: string): Promise<string> {
59
- const requestHeaders = await headers();
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 "../utils/types";
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/config";
6
+ import { useOptionalConfig } from "@snokam/tenant";
7
7
 
8
8
  export const Authenticate = memo((props: Auth.Authenticate.Props) => {
9
9
  const { children, provider } = props;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import Button from "@mui/material/Button";
4
- import { useOptionalConfig } from "@snokam/config";
4
+ import { useOptionalConfig } from "@snokam/tenant";
5
5
  import { Error } from "../Error";
6
6
 
7
7
  export const Consent = () => {
@@ -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/config";
8
+ import { useOptionalConfig } from "@snokam/tenant";
9
9
  import Button from "@mui/material/Button";
10
10
  import { Avatar, Alert } from "@mui/material";
11
11
 
@@ -0,0 +1,5 @@
1
+ export { Authenticate } from "./Authenticate";
2
+ export { Authenticated } from "./Authenticated";
3
+ export { Unauthenticated } from "./Unauthenticated";
4
+ export { Consent } from "./Consent";
5
+ export { Error } from "./Error";
package/src/config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Auth } from "./utils/types";
1
+ import type { Auth } from "./types";
2
2
 
3
3
  export const getAuthConfig = (): Auth.Config => ({
4
4
  clientId: process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID ?? "",
@@ -1,9 +1,12 @@
1
1
  import { getToken } from "next-auth/jwt";
2
- import { getSessionCookieSettings } from "../utils/cookies";
3
- import { getAuthUrl, requestOrigin } from "../utils/url";
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 {
@@ -1,4 +1,4 @@
1
- import { isKnownTenantHost } from "@snokam/config";
1
+ import { isKnownTenantHost } from "@snokam/tenant";
2
2
  import { getManagedBearer } from "@snokam/identity";
3
3
 
4
4
  interface ReturnToOptions {
@@ -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
- const payload = JSON.stringify({
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
- const parts = ticket.split(".");
62
- if (parts.length !== 3) {
63
- return rejectHandoff("malformed ticket (expected iv.tag.ciphertext)");
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 { resolveCookieDomain } from "../utils/cookies";
1
+ import { resolveCookieDomain } from "../session";
2
2
 
3
3
  /** HttpOnly — carries the impersonation bearer; only the token handler ever reads it. */
4
4
  export const IMP_SESSION_COOKIE = "imp_session";
@@ -1,4 +1,4 @@
1
- import { Auth } from "../utils/types";
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 "../utils/url";
9
+ import { requestOrigin } from "../session";
10
10
 
11
11
  function platformBase(): string | null {
12
12
  return (
@@ -1,4 +1,4 @@
1
- import { Auth } from "../utils/types";
1
+ import { Auth } from "../types";
2
2
  import {
3
3
  createImpersonationStartHandler,
4
4
  createImpersonationEndHandler,
@@ -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
- const payload = JSON.stringify({
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
- const parts = ticket.split(".");
58
- if (parts.length !== 3) return null;
59
- try {
60
- const [iv, tag, ciphertext] = parts.map((part) =>
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 "./utils/types";
6
- import { getSessionCookieSettings } from "./utils/cookies";
7
- import { addCorsHeaders } from "./utils/cors";
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
+ }