@snokam/auth 2.3.3 → 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.
- package/CHANGELOG.md +644 -0
- package/package.json +11 -14
- 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/src/options.tsx
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import NextAuth, { NextAuthOptions } from "next-auth";
|
|
2
|
-
import AzureADProvider from "next-auth/providers/azure-ad";
|
|
3
|
-
import CredentialsProvider from "next-auth/providers/credentials";
|
|
4
|
-
import { getDefaultAuthParams } from "./utils/params";
|
|
5
|
-
import { ConfidentialClientApplication } from "@azure/msal-node";
|
|
6
|
-
import { Configuration, EmployeesClient } from "@snokam/employees-client";
|
|
7
|
-
import { headers } from "next/headers";
|
|
8
|
-
import {
|
|
9
|
-
getTenantDomains,
|
|
10
|
-
getTenantDomainsByEntraTenantId,
|
|
11
|
-
type TenantDomain,
|
|
12
|
-
} from "@snokam/config";
|
|
13
|
-
import { getManagedBearer } from "@snokam/identity";
|
|
14
|
-
import { exchangePasswordToken } from "./password/api";
|
|
15
|
-
import { exchangeSsoToken } from "./sso/exchange";
|
|
16
|
-
import { getSanityImageUrl } from "@snokam/ui/media/sanity";
|
|
17
|
-
import { getSessionCookieSettings } from "./utils/cookies";
|
|
18
|
-
import { getAuthUrl } from "./utils/url";
|
|
19
|
-
import { openHandoff } from "./handoff/ticket";
|
|
20
|
-
import { passwordLogin, redeemBootstrapToken } from "./password/api";
|
|
21
|
-
import { Auth } from "./utils/types";
|
|
22
|
-
|
|
23
|
-
async function resolveEmployeesBackend(
|
|
24
|
-
config: Auth.Config
|
|
25
|
-
): Promise<{ base: string; scope: string }> {
|
|
26
|
-
const scope = config.clientId
|
|
27
|
-
? `api://${config.clientId}/user_impersonation`
|
|
28
|
-
: "";
|
|
29
|
-
const requestHeaders = await headers();
|
|
30
|
-
const host =
|
|
31
|
-
requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host");
|
|
32
|
-
const platformApiBase = process.env.NEXT_PUBLIC_PLATFORM_API_BASE_URL ?? "";
|
|
33
|
-
const authToken = await getManagedBearer(scope);
|
|
34
|
-
const employeesBackend = (domains: TenantDomain[]) =>
|
|
35
|
-
domains.find(
|
|
36
|
-
(domain) => domain.type === "backend" && domain.app === "employees"
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
let match = employeesBackend(
|
|
40
|
-
await getTenantDomains({ host, platformApiBase, authToken })
|
|
41
|
-
);
|
|
42
|
-
if (!match?.url && config.tenantId) {
|
|
43
|
-
match = employeesBackend(
|
|
44
|
-
await getTenantDomainsByEntraTenantId({
|
|
45
|
-
entraTenantId: config.tenantId,
|
|
46
|
-
platformApiBase,
|
|
47
|
-
authToken,
|
|
48
|
-
})
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
if (match?.url) return { base: match.url, scope };
|
|
52
|
-
|
|
53
|
-
const bare = host?.split(":")[0];
|
|
54
|
-
const effective =
|
|
55
|
-
!bare || bare === "localhost" || bare === "127.0.0.1"
|
|
56
|
-
? process.env.NEXT_PUBLIC_TENANT_HOST?.split(":")[0]
|
|
57
|
-
: host;
|
|
58
|
-
return { base: effective ? `https://${effective}/api/employees` : "", scope };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
type ColorScheme = "light" | "dark" | "system";
|
|
62
|
-
|
|
63
|
-
function isColorScheme(value: unknown): value is ColorScheme {
|
|
64
|
-
return value === "light" || value === "dark" || value === "system";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function requestHost(req?: {
|
|
68
|
-
headers?: Record<string, string | string[] | undefined>;
|
|
69
|
-
}): string | undefined {
|
|
70
|
-
const header = req?.headers?.["x-forwarded-host"] ?? req?.headers?.host;
|
|
71
|
-
const value = Array.isArray(header) ? header[0] : header;
|
|
72
|
-
return value?.split(",")[0].trim().split(":")[0] || undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function requestClientIp(req?: {
|
|
76
|
-
headers?: Record<string, string | string[] | undefined>;
|
|
77
|
-
}): string | undefined {
|
|
78
|
-
const pick = (name: string): string | undefined => {
|
|
79
|
-
const header = req?.headers?.[name];
|
|
80
|
-
const value = Array.isArray(header) ? header[0] : header;
|
|
81
|
-
return value?.split(",")[0].trim() || undefined;
|
|
82
|
-
};
|
|
83
|
-
// Trusted edge headers first (Front Door, unspoofable); rightmost XFF entry as a fallback.
|
|
84
|
-
const edge = pick("x-azure-clientip") ?? pick("x-azure-socketip");
|
|
85
|
-
if (edge) return edge;
|
|
86
|
-
const forwarded = req?.headers?.["x-forwarded-for"];
|
|
87
|
-
const value = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
88
|
-
return value?.split(",").pop()?.trim() || undefined;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function requestOrigin(): Promise<string | undefined> {
|
|
92
|
-
const h = await headers();
|
|
93
|
-
const host = (h.get("x-forwarded-host") ?? h.get("host") ?? "")
|
|
94
|
-
.split(",")[0]
|
|
95
|
-
.trim();
|
|
96
|
-
if (!host) return undefined;
|
|
97
|
-
const scheme =
|
|
98
|
-
h.get("x-forwarded-proto") ??
|
|
99
|
-
(host.startsWith("localhost") ? "http" : "https");
|
|
100
|
-
return `${scheme}://${host}`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
interface EmployeeProfile {
|
|
104
|
-
colorScheme?: ColorScheme;
|
|
105
|
-
imageUrl?: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function fetchEmployeeProfile(
|
|
109
|
-
config: Auth.Config,
|
|
110
|
-
refreshToken?: string,
|
|
111
|
-
authMethod?: string
|
|
112
|
-
): Promise<EmployeeProfile> {
|
|
113
|
-
if (!refreshToken) {
|
|
114
|
-
return {};
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
const { base, scope } = await resolveEmployeesBackend(config);
|
|
118
|
-
if (!base || !scope) return {};
|
|
119
|
-
|
|
120
|
-
let accessToken: string | undefined;
|
|
121
|
-
if (authMethod === "password") {
|
|
122
|
-
accessToken = (await exchangePasswordToken(refreshToken))?.accessToken;
|
|
123
|
-
} else {
|
|
124
|
-
const entra = (
|
|
125
|
-
await new ConfidentialClientApplication({
|
|
126
|
-
auth: {
|
|
127
|
-
clientId: config.clientId,
|
|
128
|
-
clientSecret: config.clientSecret,
|
|
129
|
-
authority: `https://login.microsoftonline.com/${config.tenantId}`,
|
|
130
|
-
},
|
|
131
|
-
}).acquireTokenByRefreshToken({ scopes: [scope], refreshToken })
|
|
132
|
-
)?.accessToken;
|
|
133
|
-
accessToken = entra
|
|
134
|
-
? (await exchangeSsoToken(entra))?.accessToken
|
|
135
|
-
: undefined;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!accessToken) return {};
|
|
139
|
-
const client = new EmployeesClient(
|
|
140
|
-
new Configuration({
|
|
141
|
-
basePath: base,
|
|
142
|
-
accessToken: `Bearer ${accessToken}`,
|
|
143
|
-
})
|
|
144
|
-
);
|
|
145
|
-
const me = await client.getMeProtected();
|
|
146
|
-
const profile = {
|
|
147
|
-
colorScheme: isColorScheme(me.settings?.colorScheme)
|
|
148
|
-
? me.settings.colorScheme
|
|
149
|
-
: undefined,
|
|
150
|
-
imageUrl: getSanityImageUrl(me.image?.asset?.url, {
|
|
151
|
-
width: 128,
|
|
152
|
-
height: 128,
|
|
153
|
-
fit: "crop",
|
|
154
|
-
}),
|
|
155
|
-
};
|
|
156
|
-
if (!profile.imageUrl) {
|
|
157
|
-
console.warn("[auth] employee profile resolved without an image");
|
|
158
|
-
}
|
|
159
|
-
return profile;
|
|
160
|
-
} catch (error) {
|
|
161
|
-
console.warn("[auth] employee profile fetch failed", error);
|
|
162
|
-
return {};
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export const getNextAuthOptions = (config: Auth.Config): NextAuthOptions => {
|
|
167
|
-
const authUrl = getAuthUrl();
|
|
168
|
-
const { name, domain, secure } = getSessionCookieSettings(authUrl);
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
secret: config.nextAuthSecret,
|
|
172
|
-
session: {
|
|
173
|
-
maxAge: 7 * 24 * 60 * 60,
|
|
174
|
-
updateAge: 24 * 60 * 60,
|
|
175
|
-
},
|
|
176
|
-
pages: {
|
|
177
|
-
signIn: "/auth/login",
|
|
178
|
-
},
|
|
179
|
-
providers: [
|
|
180
|
-
AzureADProvider({
|
|
181
|
-
clientId: config.clientId,
|
|
182
|
-
clientSecret: config.clientSecret,
|
|
183
|
-
tenantId: config.tenantId,
|
|
184
|
-
authorization: {
|
|
185
|
-
params: getDefaultAuthParams(config.tenantId),
|
|
186
|
-
},
|
|
187
|
-
checks: ["pkce", "state"],
|
|
188
|
-
profile(profile) {
|
|
189
|
-
return {
|
|
190
|
-
id: profile.sub,
|
|
191
|
-
name: profile.name,
|
|
192
|
-
email: profile.email,
|
|
193
|
-
image: null,
|
|
194
|
-
};
|
|
195
|
-
},
|
|
196
|
-
}),
|
|
197
|
-
CredentialsProvider({
|
|
198
|
-
id: "handoff",
|
|
199
|
-
name: "Snokam",
|
|
200
|
-
credentials: { ticket: { label: "ticket", type: "text" } },
|
|
201
|
-
async authorize(credentials, req) {
|
|
202
|
-
const expectedBusiness = requestHost(req);
|
|
203
|
-
const claims =
|
|
204
|
-
credentials?.ticket && expectedBusiness
|
|
205
|
-
? openHandoff(
|
|
206
|
-
credentials.ticket,
|
|
207
|
-
config.nextAuthSecret,
|
|
208
|
-
expectedBusiness
|
|
209
|
-
)
|
|
210
|
-
: null;
|
|
211
|
-
if (!claims) {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
return {
|
|
215
|
-
id: claims.oid,
|
|
216
|
-
email: claims.email,
|
|
217
|
-
name: claims.name,
|
|
218
|
-
image: claims.image ?? null,
|
|
219
|
-
oid: claims.oid,
|
|
220
|
-
tid: claims.tid,
|
|
221
|
-
refreshToken: claims.refreshToken,
|
|
222
|
-
authMethod: claims.authMethod || undefined,
|
|
223
|
-
} as unknown as { id: string };
|
|
224
|
-
},
|
|
225
|
-
}),
|
|
226
|
-
CredentialsProvider({
|
|
227
|
-
id: "password",
|
|
228
|
-
name: "Passord",
|
|
229
|
-
credentials: {
|
|
230
|
-
email: { label: "email", type: "text" },
|
|
231
|
-
password: { label: "password", type: "password" },
|
|
232
|
-
code: { label: "code", type: "text" },
|
|
233
|
-
host: { label: "host", type: "text" },
|
|
234
|
-
bootstrap: { label: "bootstrap", type: "text" },
|
|
235
|
-
},
|
|
236
|
-
async authorize(credentials, req) {
|
|
237
|
-
if (credentials?.bootstrap) {
|
|
238
|
-
const redeemed = await redeemBootstrapToken(credentials.bootstrap);
|
|
239
|
-
if (!redeemed) {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
id: redeemed.userId,
|
|
244
|
-
email: redeemed.email,
|
|
245
|
-
name: redeemed.name,
|
|
246
|
-
oid: redeemed.userId,
|
|
247
|
-
tid: redeemed.businessId,
|
|
248
|
-
refreshToken: redeemed.refreshToken,
|
|
249
|
-
authMethod: "password",
|
|
250
|
-
} as unknown as { id: string };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
!credentials?.email ||
|
|
255
|
-
!credentials.password ||
|
|
256
|
-
!credentials.host
|
|
257
|
-
) {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const outcome = await passwordLogin(
|
|
262
|
-
{
|
|
263
|
-
host: credentials.host,
|
|
264
|
-
email: credentials.email,
|
|
265
|
-
password: credentials.password,
|
|
266
|
-
code: credentials.code || undefined,
|
|
267
|
-
},
|
|
268
|
-
requestClientIp(req)
|
|
269
|
-
);
|
|
270
|
-
if (!outcome.login) {
|
|
271
|
-
throw new Error(outcome.error ?? "invalid_credentials");
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
id: outcome.login.userId,
|
|
276
|
-
email: outcome.login.email,
|
|
277
|
-
name: outcome.login.name,
|
|
278
|
-
oid: outcome.login.userId,
|
|
279
|
-
tid: outcome.login.businessId,
|
|
280
|
-
refreshToken: outcome.login.refreshToken,
|
|
281
|
-
authMethod: "password",
|
|
282
|
-
} as unknown as { id: string };
|
|
283
|
-
},
|
|
284
|
-
}),
|
|
285
|
-
],
|
|
286
|
-
cookies: {
|
|
287
|
-
sessionToken: {
|
|
288
|
-
name,
|
|
289
|
-
options: {
|
|
290
|
-
path: "/",
|
|
291
|
-
httpOnly: true,
|
|
292
|
-
sameSite: "lax",
|
|
293
|
-
secure,
|
|
294
|
-
domain,
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
callbacks: {
|
|
299
|
-
async redirect({ url, baseUrl }) {
|
|
300
|
-
const origin = (await requestOrigin()) ?? baseUrl;
|
|
301
|
-
const { pathname, search, hash } = new URL(url, origin);
|
|
302
|
-
return origin + pathname + search + hash;
|
|
303
|
-
},
|
|
304
|
-
async jwt({ token, account, profile, user, trigger, session }) {
|
|
305
|
-
if (account?.access_token) {
|
|
306
|
-
token.refreshToken = account.refresh_token;
|
|
307
|
-
token.expiresAt = account.expires_at;
|
|
308
|
-
const claims = profile as { oid?: string; tid?: string } | undefined;
|
|
309
|
-
if (claims?.oid) token.oid = claims.oid;
|
|
310
|
-
if (claims?.tid) token.tid = claims.tid;
|
|
311
|
-
const employee = await fetchEmployeeProfile(
|
|
312
|
-
{ ...config, tenantId: claims?.tid ?? config.tenantId },
|
|
313
|
-
account.refresh_token as string | undefined
|
|
314
|
-
);
|
|
315
|
-
if (employee.colorScheme) token.colorScheme = employee.colorScheme;
|
|
316
|
-
token.picture = employee.imageUrl ?? null;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const handoff = user as
|
|
320
|
-
| {
|
|
321
|
-
oid?: string;
|
|
322
|
-
tid?: string;
|
|
323
|
-
refreshToken?: string;
|
|
324
|
-
authMethod?: string;
|
|
325
|
-
}
|
|
326
|
-
| undefined;
|
|
327
|
-
if (handoff?.refreshToken) {
|
|
328
|
-
token.oid = handoff.oid;
|
|
329
|
-
token.tid = handoff.tid;
|
|
330
|
-
token.refreshToken = handoff.refreshToken;
|
|
331
|
-
if (handoff.authMethod) {
|
|
332
|
-
token.authMethod = handoff.authMethod;
|
|
333
|
-
}
|
|
334
|
-
const employee = await fetchEmployeeProfile(
|
|
335
|
-
{ ...config, tenantId: handoff.tid ?? config.tenantId },
|
|
336
|
-
handoff.refreshToken,
|
|
337
|
-
handoff.authMethod
|
|
338
|
-
);
|
|
339
|
-
if (employee.colorScheme) token.colorScheme = employee.colorScheme;
|
|
340
|
-
if (employee.imageUrl) token.picture = employee.imageUrl;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (trigger === "update") {
|
|
344
|
-
if (isColorScheme(session?.colorScheme)) {
|
|
345
|
-
token.colorScheme = session.colorScheme;
|
|
346
|
-
}
|
|
347
|
-
if (session?.refreshProfile && token.refreshToken) {
|
|
348
|
-
const employee = await fetchEmployeeProfile(
|
|
349
|
-
{
|
|
350
|
-
...config,
|
|
351
|
-
tenantId: (token.tid as string | undefined) ?? config.tenantId,
|
|
352
|
-
},
|
|
353
|
-
token.refreshToken as string,
|
|
354
|
-
token.authMethod as string | undefined
|
|
355
|
-
);
|
|
356
|
-
if (employee.imageUrl) token.picture = employee.imageUrl;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return token;
|
|
361
|
-
},
|
|
362
|
-
async session({ session, token }) {
|
|
363
|
-
const claims = token as {
|
|
364
|
-
colorScheme?: unknown;
|
|
365
|
-
oid?: string;
|
|
366
|
-
tid?: string;
|
|
367
|
-
authMethod?: string;
|
|
368
|
-
};
|
|
369
|
-
const target = session as {
|
|
370
|
-
colorScheme?: ColorScheme;
|
|
371
|
-
oid?: string;
|
|
372
|
-
tid?: string;
|
|
373
|
-
authMethod?: string;
|
|
374
|
-
};
|
|
375
|
-
if (isColorScheme(claims.colorScheme)) {
|
|
376
|
-
target.colorScheme = claims.colorScheme;
|
|
377
|
-
}
|
|
378
|
-
target.oid = claims.oid;
|
|
379
|
-
target.tid = claims.tid;
|
|
380
|
-
if (claims.authMethod) {
|
|
381
|
-
target.authMethod = claims.authMethod;
|
|
382
|
-
}
|
|
383
|
-
return session;
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
};
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
export default (config: Auth.Config) => NextAuth(getNextAuthOptions(config));
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useRef } from "react";
|
|
2
|
-
import { useRouter } from "next/router";
|
|
3
|
-
import { useAuth } from "../../hooks";
|
|
4
|
-
import { signIn } from "next-auth/react";
|
|
5
|
-
import { PageLoader } from "@snokam/ui";
|
|
6
|
-
import Error from "../../components/Error";
|
|
7
|
-
|
|
8
|
-
export const Login = () => {
|
|
9
|
-
const { isAuthenticated } = useAuth();
|
|
10
|
-
const [callbackUrl, setCallbackUrl] = useState<string | null>(null);
|
|
11
|
-
const [authParams, setAuthParams] = useState<Record<string, string>>({});
|
|
12
|
-
const [isError, setIsError] = useState<boolean>(false);
|
|
13
|
-
const hasSignedIn = useRef(false);
|
|
14
|
-
const router = useRouter();
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
const params = new URLSearchParams(window.location.search);
|
|
18
|
-
setCallbackUrl(params.get("callbackUrl"));
|
|
19
|
-
setIsError(!!params.get("error"));
|
|
20
|
-
|
|
21
|
-
const authParamsObj: Record<string, string> = {};
|
|
22
|
-
params.forEach((value, key) => {
|
|
23
|
-
if (key !== "error" && key !== "callbackUrl") {
|
|
24
|
-
authParamsObj[key] = value;
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
setAuthParams(authParamsObj);
|
|
28
|
-
}, []);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!callbackUrl) return;
|
|
32
|
-
if (isError) return;
|
|
33
|
-
if (hasSignedIn.current) return;
|
|
34
|
-
|
|
35
|
-
if (isAuthenticated) {
|
|
36
|
-
router.replace(callbackUrl || "/");
|
|
37
|
-
} else {
|
|
38
|
-
hasSignedIn.current = true;
|
|
39
|
-
signIn("azure-ad", { callbackUrl }, authParams);
|
|
40
|
-
}
|
|
41
|
-
}, [isAuthenticated, callbackUrl, authParams, isError, router, signIn]);
|
|
42
|
-
|
|
43
|
-
if (isError) {
|
|
44
|
-
return <Error />;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return <PageLoader />;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export default Login;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { memo, ReactNode, useEffect } from "react";
|
|
2
|
-
import { useRouter } from "next/router";
|
|
3
|
-
import { Auth } from "../../../utils/types";
|
|
4
|
-
import { Authenticate } from "../../../components/Authenticate";
|
|
5
|
-
import { SessionProvider } from "next-auth/react";
|
|
6
|
-
import { useAuth } from "../../../hooks";
|
|
7
|
-
|
|
8
|
-
export const Provider = memo((props: Auth.Provider.Props) => {
|
|
9
|
-
const { session, children, enforceLogin } = props;
|
|
10
|
-
const router = useRouter();
|
|
11
|
-
const query = router?.query;
|
|
12
|
-
|
|
13
|
-
if (query?.enforceLogin || enforceLogin) {
|
|
14
|
-
return (
|
|
15
|
-
<SessionProvider session={session}>
|
|
16
|
-
<Authenticate provider="azure-ad" {...props}>
|
|
17
|
-
{children}
|
|
18
|
-
</Authenticate>
|
|
19
|
-
</SessionProvider>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<SessionProvider session={session}>
|
|
25
|
-
<Wrapper>{children}</Wrapper>
|
|
26
|
-
</SessionProvider>
|
|
27
|
-
);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
export const Wrapper = ({ children }: { children: ReactNode }) => {
|
|
31
|
-
const { signOut } = useAuth();
|
|
32
|
-
const router = useRouter();
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (router?.query?.logout === "true") {
|
|
36
|
-
signOut({
|
|
37
|
-
callbackUrl: "/auth/login?callbackUrl=/",
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}, [router?.query?.logout]);
|
|
41
|
-
|
|
42
|
-
return <>{children}</>;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export default Provider;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Provider } from "./components/Provider";
|
|
4
|
-
import { Authenticate } from "../components/Authenticate";
|
|
5
|
-
import { Login } from "./components/Login";
|
|
6
|
-
import { Error } from "../components/Error";
|
|
7
|
-
|
|
8
|
-
export { Unauthenticated } from "../components/Unauthenticated";
|
|
9
|
-
export { Authenticated } from "../components/Authenticated";
|
|
10
|
-
export { useAuth } from "../hooks";
|
|
11
|
-
export { signIn, SessionProvider } from "next-auth/react";
|
|
12
|
-
export { Login, Error, Provider, Authenticate };
|
|
13
|
-
|
|
14
|
-
export default {
|
|
15
|
-
Login,
|
|
16
|
-
Error,
|
|
17
|
-
Provider,
|
|
18
|
-
Authenticate,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type { Session } from "next-auth";
|
|
22
|
-
export type { Auth } from "../utils/types";
|
package/src/server/auth.tsx
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { GetServerSidePropsContext, GetStaticPropsContext } from "next";
|
|
2
|
-
import { getServerSession as nextAuthGetServerSession } from "next-auth";
|
|
3
|
-
import { Auth } from "../utils/types";
|
|
4
|
-
import { getNextAuthOptions } from "../options";
|
|
5
|
-
|
|
6
|
-
export const getAuth = async (
|
|
7
|
-
context: GetServerSidePropsContext | GetStaticPropsContext,
|
|
8
|
-
authConfig: Auth.Config
|
|
9
|
-
) => {
|
|
10
|
-
if (!("req" in context && "res" in context)) {
|
|
11
|
-
return {
|
|
12
|
-
isAuthenticated: false,
|
|
13
|
-
session: null,
|
|
14
|
-
user: null,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const options = getNextAuthOptions(authConfig);
|
|
19
|
-
const session = await nextAuthGetServerSession(
|
|
20
|
-
context.req,
|
|
21
|
-
context.res,
|
|
22
|
-
options
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
isAuthenticated: !!session?.user,
|
|
27
|
-
user: session?.user || null,
|
|
28
|
-
session,
|
|
29
|
-
};
|
|
30
|
-
};
|
package/src/server/handler.tsx
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { getToken } from "next-auth/jwt";
|
|
2
|
-
import { ConfidentialClientApplication } from "@azure/msal-node";
|
|
3
|
-
import type { NextApiRequest, NextApiResponse } from "next";
|
|
4
|
-
import { getServerSession } from "next-auth";
|
|
5
|
-
import { Auth } from "../utils/types";
|
|
6
|
-
import { getNextAuthOptions } from "../options";
|
|
7
|
-
import { exchangeSsoToken } from "../sso/exchange";
|
|
8
|
-
|
|
9
|
-
export interface Props {
|
|
10
|
-
req: NextApiRequest;
|
|
11
|
-
res: NextApiResponse<{ accessToken: string; expiresOn: number } | string>;
|
|
12
|
-
authConfig: Auth.Config;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export default async function handler(props: Props) {
|
|
16
|
-
const { req, res, authConfig } = props;
|
|
17
|
-
const options = getNextAuthOptions(authConfig);
|
|
18
|
-
const session = await getServerSession(req, res, options);
|
|
19
|
-
|
|
20
|
-
if (!session?.user) {
|
|
21
|
-
return res.status(401).end("Not authenticated");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const scope = process.env.IDENTITY_AD_SCOPE;
|
|
25
|
-
if (!scope) {
|
|
26
|
-
return res.status(500).end("IDENTITY_AD_SCOPE is not configured");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const { nextAuthSecret } = authConfig;
|
|
31
|
-
const refetchTokenResult = await getToken({
|
|
32
|
-
req,
|
|
33
|
-
secret: nextAuthSecret,
|
|
34
|
-
cookieName: options.cookies?.sessionToken?.name,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
if (!refetchTokenResult) {
|
|
38
|
-
return res.status(500).end("Failed to get refetch token");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const cca = new ConfidentialClientApplication({
|
|
42
|
-
auth: {
|
|
43
|
-
clientId: authConfig.clientId,
|
|
44
|
-
clientSecret: authConfig.clientSecret,
|
|
45
|
-
authority: `https://login.microsoftonline.com/${authConfig.tenantId}`,
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const accessTokenResult = await cca.acquireTokenByRefreshToken({
|
|
50
|
-
scopes: [scope],
|
|
51
|
-
refreshToken: refetchTokenResult?.refreshToken as string,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (!accessTokenResult) {
|
|
55
|
-
return res.status(500).end("Failed to get access token");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const exchanged = await exchangeSsoToken(accessTokenResult.accessToken);
|
|
59
|
-
if (!exchanged) {
|
|
60
|
-
return res.status(502).end("SSO token exchange failed");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return res.status(200).json(exchanged);
|
|
64
|
-
} catch (e) {
|
|
65
|
-
console.error("Token exchange error:", e);
|
|
66
|
-
res.status(500).end("Token exchange failed");
|
|
67
|
-
}
|
|
68
|
-
}
|
package/src/server.tsx
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { getServerToken } from "./server/token";
|
|
2
|
-
import { default as Routes } from "./options";
|
|
3
|
-
import { default as Token } from "./server/handler";
|
|
4
|
-
import { Auth } from "./utils/types";
|
|
5
|
-
|
|
6
|
-
export { getServerToken };
|
|
7
|
-
export { getServerSession } from "next-auth/next";
|
|
8
|
-
export { getNextAuthOptions } from "./options";
|
|
9
|
-
export { getAuth } from "./server/auth";
|
|
10
|
-
|
|
11
|
-
export type { Auth };
|
|
12
|
-
|
|
13
|
-
export default {
|
|
14
|
-
Routes,
|
|
15
|
-
Token,
|
|
16
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|