@wtfalch/auth 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/dist/auth.d.ts +33 -3
- package/dist/auth.js +56 -10
- package/dist/broker.d.ts +29 -0
- package/dist/broker.js +11 -0
- package/dist/next.d.ts +4 -0
- package/dist/next.js +58 -8
- package/dist/redirect.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
Sign in against [auth.wtfalch.dev](https://auth.wtfalch.dev) from a Next.js app.
|
|
4
4
|
|
|
5
5
|
A thin OIDC client for one self-hosted ZITADEL instance, plus a client for the
|
|
6
|
-
sign-in service that sits in front of it. Apps host their own sign-in
|
|
7
|
-
|
|
6
|
+
sign-in service that sits in front of it. Apps host their own password sign-in
|
|
7
|
+
pages. Flows requiring MFA or another unsupported login policy continue at
|
|
8
|
+
the identity service's hosted login.
|
|
8
9
|
|
|
9
10
|
Built for the wtfalch estate, published because valet consumes it from a
|
|
10
11
|
container and a `file:` path does not survive a Docker build.
|
|
@@ -39,6 +40,26 @@ middleware for the paths that need a session, and read the person with
|
|
|
39
40
|
`createAuth` from `@wtfalch/auth` is the same thing without the framework, for
|
|
40
41
|
a CLI or a worker that has no router to import.
|
|
41
42
|
|
|
43
|
+
## MFA and hosted login
|
|
44
|
+
|
|
45
|
+
The broker checks the organisation's login policy and enrolled authentication
|
|
46
|
+
methods before completing password sign-in, signup, or password reset. MFA,
|
|
47
|
+
disabled password authentication, and unknown authentication methods require
|
|
48
|
+
the hosted login. Email links always continue there: ZITADEL email OTP is a
|
|
49
|
+
second factor and cannot authenticate a session by itself.
|
|
50
|
+
|
|
51
|
+
The Next.js adapter handles these redirects and preserves the OIDC
|
|
52
|
+
transaction. With the framework-neutral API, a successful result carrying
|
|
53
|
+
`hosted: true` means authentication is still pending: redirect to
|
|
54
|
+
`redirectTo` (sign-in/signup) or `location` (reset), writing any returned
|
|
55
|
+
cookies first. Do not pass a hosted URL to `complete`. The eventual OIDC
|
|
56
|
+
callback completes authentication with the original PKCE, state, nonce, and
|
|
57
|
+
destination.
|
|
58
|
+
|
|
59
|
+
Auth-request lookup failures lead to the configured error page with
|
|
60
|
+
`auth_error=request` or `auth_error=unavailable`. That page should show a
|
|
61
|
+
retry link; it must not automatically restart authorization.
|
|
62
|
+
|
|
42
63
|
## Configuration
|
|
43
64
|
|
|
44
65
|
| | |
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JWTPayload } from 'jose';
|
|
2
|
-
import { type AuthRequest, type NewUser } from './broker.js';
|
|
2
|
+
import { type AuthRequest, type NewUser, type PeoplePage } from './broker.js';
|
|
3
3
|
import { type AuthOptions } from './config.js';
|
|
4
4
|
import { type SetCookie } from './cookies.js';
|
|
5
5
|
export interface User {
|
|
@@ -37,12 +37,13 @@ export type Gate = {
|
|
|
37
37
|
cookies: SetCookie[];
|
|
38
38
|
};
|
|
39
39
|
export type Intent = 'login' | 'register';
|
|
40
|
-
export type SignInError = 'invalid_credentials' | 'request' | 'unavailable';
|
|
40
|
+
export type SignInError = 'invalid_credentials' | 'too_many' | 'request' | 'unavailable';
|
|
41
41
|
export type ResetError = 'invalid_code' | 'invalid_password' | 'unavailable';
|
|
42
|
-
export type SignUpError = 'email_taken' | 'invalid' | 'request' | 'unavailable';
|
|
42
|
+
export type SignUpError = 'registration_closed' | 'email_taken' | 'username_taken' | 'username_invalid' | 'invalid' | 'request' | 'unavailable';
|
|
43
43
|
export type SignInResult = {
|
|
44
44
|
ok: true;
|
|
45
45
|
redirectTo: string;
|
|
46
|
+
hosted?: true;
|
|
46
47
|
} | {
|
|
47
48
|
ok: false;
|
|
48
49
|
error: SignInError;
|
|
@@ -50,6 +51,7 @@ export type SignInResult = {
|
|
|
50
51
|
export type SignUpResult = {
|
|
51
52
|
ok: true;
|
|
52
53
|
redirectTo: string;
|
|
54
|
+
hosted?: true;
|
|
53
55
|
} | {
|
|
54
56
|
ok: false;
|
|
55
57
|
error: SignUpError;
|
|
@@ -64,6 +66,7 @@ export type SignedIn = {
|
|
|
64
66
|
ok: true;
|
|
65
67
|
location: string;
|
|
66
68
|
cookies: SetCookie[];
|
|
69
|
+
hosted?: true;
|
|
67
70
|
} | {
|
|
68
71
|
ok: false;
|
|
69
72
|
error: ResetError;
|
|
@@ -121,6 +124,33 @@ export interface Auth {
|
|
|
121
124
|
}): Promise<ReadResult>;
|
|
122
125
|
gate(request: Request, rules?: GateRules): Promise<Gate>;
|
|
123
126
|
startUrl(next?: string | null, intent?: Intent): string;
|
|
127
|
+
/**
|
|
128
|
+
* Where to send somebody when a sign-in step failed for a reason they
|
|
129
|
+
* cannot fix by retrying it automatically: `onError` with `?auth_error=`,
|
|
130
|
+
* which is the same shape the callback's own failures use. Exposed because
|
|
131
|
+
* the Next adapter has to reach it from a server component, where a
|
|
132
|
+
* `Response` is no use.
|
|
133
|
+
*/
|
|
134
|
+
errorUrl(reason: string): string;
|
|
135
|
+
/**
|
|
136
|
+
* The organisation's people, for an operator surface. The scope is this
|
|
137
|
+
* app's own organisation, decided by the service from the key rather than
|
|
138
|
+
* by an argument here, so there is no call that reads another one's.
|
|
139
|
+
*/
|
|
140
|
+
people(options?: {
|
|
141
|
+
limit?: number;
|
|
142
|
+
offset?: number;
|
|
143
|
+
}): Promise<PeoplePage>;
|
|
144
|
+
/**
|
|
145
|
+
* Deactivate or reactivate somebody. Refused for a user outside this app's
|
|
146
|
+
* organisation. Deactivating stops them signing in and keeps the row, the
|
|
147
|
+
* memberships and the trail; there is no delete here, deliberately —
|
|
148
|
+
* erasure is an authorisation concern with its own audited path.
|
|
149
|
+
*/
|
|
150
|
+
setPersonActive(userId: string, active: boolean): Promise<{
|
|
151
|
+
userId: string;
|
|
152
|
+
state: string;
|
|
153
|
+
}>;
|
|
124
154
|
/** The auth request the issuer sent the browser here with. Refuses one for another application. */
|
|
125
155
|
authRequest(id: string): Promise<AuthRequest>;
|
|
126
156
|
signIn(input: {
|
package/dist/auth.js
CHANGED
|
@@ -180,6 +180,8 @@ export function createAuth(input) {
|
|
|
180
180
|
return { kind: 'deny', cookies };
|
|
181
181
|
return { kind: 'redirect', location: startUrl(`${url.pathname}${url.search}`), cookies };
|
|
182
182
|
};
|
|
183
|
+
const people = (options) => broker().people(options);
|
|
184
|
+
const setPersonActive = (userId, active) => broker().setPersonActive(userId, active);
|
|
183
185
|
const authRequest = (id) => broker().authRequest(id);
|
|
184
186
|
/**
|
|
185
187
|
* An OIDC flow this app starts and finishes itself, for a person arriving
|
|
@@ -207,6 +209,35 @@ export function createAuth(input) {
|
|
|
207
209
|
const cookie = await sealSession(options(), { idt: tokens.idToken, rt: tokens.refreshToken });
|
|
208
210
|
return { location: new URL(next, options().appUrl).href, cookies: [cookie] };
|
|
209
211
|
};
|
|
212
|
+
const hostedUrl = (id) => {
|
|
213
|
+
const url = new URL('/login', options().issuer);
|
|
214
|
+
url.searchParams.set('authRequest', id);
|
|
215
|
+
return url.href;
|
|
216
|
+
};
|
|
217
|
+
const needsHostedLogin = (error) => error instanceof BrokerError && error.error === 'hosted_login_required';
|
|
218
|
+
// Email/reset flows started on the server must transfer their PKCE transaction
|
|
219
|
+
// to the browser before handing off to the issuer's full authentication UI.
|
|
220
|
+
const finishOrHandoff = async (action, flow, next) => {
|
|
221
|
+
try {
|
|
222
|
+
return await finish(await action(), flow, next);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (!needsHostedLogin(error))
|
|
226
|
+
throw error;
|
|
227
|
+
return {
|
|
228
|
+
location: hostedUrl(flow.id),
|
|
229
|
+
hosted: true,
|
|
230
|
+
cookies: [
|
|
231
|
+
await sealTransaction(options(), {
|
|
232
|
+
st: flow.state,
|
|
233
|
+
nc: flow.nonce,
|
|
234
|
+
cv: flow.verifier,
|
|
235
|
+
nx: next,
|
|
236
|
+
}),
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|
|
210
241
|
const resendVerification = (email, next) => quietly(() => broker().sendVerification(email, safeNextPath(next, '')));
|
|
211
242
|
const requestPasswordReset = (email, next) => quietly(() => broker().sendReset(email, safeNextPath(next, '')));
|
|
212
243
|
const sendLink = (email, next) => quietly(() => broker().sendLink(email, safeNextPath(next, '')));
|
|
@@ -214,14 +245,8 @@ export function createAuth(input) {
|
|
|
214
245
|
const resetPassword = async ({ userId, code, password, next, }) => {
|
|
215
246
|
try {
|
|
216
247
|
const flow = await serverFlow();
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
userId,
|
|
220
|
-
code,
|
|
221
|
-
password,
|
|
222
|
-
});
|
|
223
|
-
const { location, cookies } = await finish(callbackUrl, flow, safeNextPath(next, options().afterLogin));
|
|
224
|
-
return { ok: true, location, cookies };
|
|
248
|
+
const completed = await finishOrHandoff(() => broker().completeReset({ authRequestId: flow.id, userId, code, password }), flow, safeNextPath(next, options().afterLogin));
|
|
249
|
+
return { ok: true, ...completed };
|
|
225
250
|
}
|
|
226
251
|
catch (error) {
|
|
227
252
|
if (error instanceof BrokerError && error.error === 'invalid_password') {
|
|
@@ -241,8 +266,7 @@ export function createAuth(input) {
|
|
|
241
266
|
return failed('link', []);
|
|
242
267
|
try {
|
|
243
268
|
const flow = await serverFlow();
|
|
244
|
-
const
|
|
245
|
-
const { location, cookies } = await finish(callbackUrl, flow, safeNextPath(params.get('next'), options().afterLogin));
|
|
269
|
+
const { location, cookies } = await finishOrHandoff(() => broker().followLink({ authRequestId: flow.id, sessionId, code }), flow, safeNextPath(params.get('next'), options().afterLogin));
|
|
246
270
|
return redirect(location, cookies);
|
|
247
271
|
}
|
|
248
272
|
catch {
|
|
@@ -254,6 +278,8 @@ export function createAuth(input) {
|
|
|
254
278
|
return { ok: true, redirectTo: await broker().signIn({ authRequestId, email, password }) };
|
|
255
279
|
}
|
|
256
280
|
catch (error) {
|
|
281
|
+
if (needsHostedLogin(error))
|
|
282
|
+
return { ok: true, redirectTo: hostedUrl(authRequestId), hosted: true };
|
|
257
283
|
return { ok: false, error: signInError(error) };
|
|
258
284
|
}
|
|
259
285
|
};
|
|
@@ -262,9 +288,21 @@ export function createAuth(input) {
|
|
|
262
288
|
return { ok: true, redirectTo: await broker().signUp({ authRequestId, ...user }) };
|
|
263
289
|
}
|
|
264
290
|
catch (error) {
|
|
291
|
+
if (needsHostedLogin(error))
|
|
292
|
+
return { ok: true, redirectTo: hostedUrl(authRequestId), hosted: true };
|
|
265
293
|
if (error instanceof BrokerError && error.error === 'email_taken') {
|
|
266
294
|
return { ok: false, error: 'email_taken', message: error.detail ?? error.error };
|
|
267
295
|
}
|
|
296
|
+
if (error instanceof BrokerError && error.error === 'registration_closed') {
|
|
297
|
+
return { ok: false, error: 'registration_closed', message: error.detail ?? error.error };
|
|
298
|
+
}
|
|
299
|
+
// Both carry a sentence about the name, for the form to show as it is.
|
|
300
|
+
if (error instanceof BrokerError && error.error === 'username_taken') {
|
|
301
|
+
return { ok: false, error: 'username_taken', message: error.detail ?? error.error };
|
|
302
|
+
}
|
|
303
|
+
if (error instanceof BrokerError && error.error === 'username_invalid') {
|
|
304
|
+
return { ok: false, error: 'username_invalid', message: error.detail ?? error.error };
|
|
305
|
+
}
|
|
268
306
|
if (error instanceof BrokerError && error.error === 'request') {
|
|
269
307
|
return { ok: false, error: 'request', message: error.detail ?? error.error };
|
|
270
308
|
}
|
|
@@ -279,6 +317,8 @@ export function createAuth(input) {
|
|
|
279
317
|
url.searchParams.set('auth_error', reason);
|
|
280
318
|
return { location: url.href, cookies };
|
|
281
319
|
};
|
|
320
|
+
/** `failure`'s location on its own, for a caller that redirects rather than answering. */
|
|
321
|
+
const errorUrl = (reason) => failure(reason, []).location;
|
|
282
322
|
const failed = (reason, cookies) => {
|
|
283
323
|
const { location } = failure(reason, []);
|
|
284
324
|
return redirect(location, cookies);
|
|
@@ -303,6 +343,9 @@ export function createAuth(input) {
|
|
|
303
343
|
readCookie,
|
|
304
344
|
gate,
|
|
305
345
|
startUrl,
|
|
346
|
+
errorUrl,
|
|
347
|
+
people,
|
|
348
|
+
setPersonActive,
|
|
306
349
|
authRequest,
|
|
307
350
|
signIn,
|
|
308
351
|
signUp,
|
|
@@ -390,6 +433,9 @@ function signInError(error) {
|
|
|
390
433
|
// A key the service will not take is the app's problem, not the visitor's.
|
|
391
434
|
if (error instanceof BrokerError && error.error === 'unauthorized')
|
|
392
435
|
return 'unavailable';
|
|
436
|
+
// The sign-in service's brute-force backstop tripped; ask them to wait.
|
|
437
|
+
if (error instanceof BrokerError && error.error === 'too_many')
|
|
438
|
+
return 'too_many';
|
|
393
439
|
if (error instanceof BrokerError && error.status < 500)
|
|
394
440
|
return 'invalid_credentials';
|
|
395
441
|
return 'unavailable';
|
package/dist/broker.d.ts
CHANGED
|
@@ -9,6 +9,27 @@ export interface NewUser {
|
|
|
9
9
|
password: string;
|
|
10
10
|
givenName: string;
|
|
11
11
|
familyName: string;
|
|
12
|
+
/**
|
|
13
|
+
* A login name they chose, where the app asks for one. Three to thirty of
|
|
14
|
+
* letters, digits, `.`, `_` and `-`; the service refuses reserved names
|
|
15
|
+
* and words it will not have. Without it the email is the login name.
|
|
16
|
+
*/
|
|
17
|
+
username?: string;
|
|
18
|
+
}
|
|
19
|
+
/** One of the organisation's people, as the sign-in service reports them. */
|
|
20
|
+
export interface Person {
|
|
21
|
+
readonly userId: string;
|
|
22
|
+
readonly username: string;
|
|
23
|
+
readonly displayName: string;
|
|
24
|
+
readonly email: string | null;
|
|
25
|
+
readonly emailVerified: boolean;
|
|
26
|
+
/** `active`, `inactive`, or `unknown` if the issuer said something newer. */
|
|
27
|
+
readonly state: string;
|
|
28
|
+
}
|
|
29
|
+
export interface PeoplePage {
|
|
30
|
+
/** Everyone in the organisation, not only this page. */
|
|
31
|
+
readonly total: number;
|
|
32
|
+
readonly people: readonly Person[];
|
|
12
33
|
}
|
|
13
34
|
export declare class BrokerError extends Error {
|
|
14
35
|
readonly status: number;
|
|
@@ -25,6 +46,14 @@ export declare class Broker {
|
|
|
25
46
|
private readonly options;
|
|
26
47
|
constructor(options: ResolvedOptions);
|
|
27
48
|
private call;
|
|
49
|
+
people(options?: {
|
|
50
|
+
limit?: number;
|
|
51
|
+
offset?: number;
|
|
52
|
+
}): Promise<PeoplePage>;
|
|
53
|
+
setPersonActive(userId: string, active: boolean): Promise<{
|
|
54
|
+
userId: string;
|
|
55
|
+
state: string;
|
|
56
|
+
}>;
|
|
28
57
|
authRequest(id: string): Promise<AuthRequest>;
|
|
29
58
|
signIn(input: {
|
|
30
59
|
authRequestId: string;
|
package/dist/broker.js
CHANGED
|
@@ -51,6 +51,17 @@ export class Broker {
|
|
|
51
51
|
}
|
|
52
52
|
return data;
|
|
53
53
|
}
|
|
54
|
+
people(options = {}) {
|
|
55
|
+
const query = {};
|
|
56
|
+
if (options.limit !== undefined)
|
|
57
|
+
query.limit = options.limit;
|
|
58
|
+
if (options.offset !== undefined)
|
|
59
|
+
query.offset = options.offset;
|
|
60
|
+
return this.call('GET', '/people', query);
|
|
61
|
+
}
|
|
62
|
+
setPersonActive(userId, active) {
|
|
63
|
+
return this.call('POST', active ? '/people/reactivate' : '/people/deactivate', { userId });
|
|
64
|
+
}
|
|
54
65
|
authRequest(id) {
|
|
55
66
|
return this.call('GET', '/auth-request', { id });
|
|
56
67
|
}
|
package/dist/next.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export interface NextAuth {
|
|
|
53
53
|
userId: string;
|
|
54
54
|
invited: boolean;
|
|
55
55
|
}>;
|
|
56
|
+
/** The organisation's people, for an operator surface. Scoped to this app's organisation by the service. */
|
|
57
|
+
people: Auth['people'];
|
|
58
|
+
/** Deactivate or reactivate somebody in this app's organisation. */
|
|
59
|
+
setPersonActive: Auth['setPersonActive'];
|
|
56
60
|
/** Sets the password from a reset link, signs the person in, and redirects. */
|
|
57
61
|
resetPassword: (input: {
|
|
58
62
|
userId: string;
|
package/dist/next.js
CHANGED
|
@@ -2,7 +2,19 @@ import { cookies } from 'next/headers';
|
|
|
2
2
|
import { redirect } from 'next/navigation';
|
|
3
3
|
import { NextResponse } from 'next/server';
|
|
4
4
|
import { createAuth, } from './auth.js';
|
|
5
|
+
import { BrokerError } from './broker.js';
|
|
5
6
|
import { withCookies } from './cookies.js';
|
|
7
|
+
/**
|
|
8
|
+
* Which of the two failures it was, in the vocabulary `auth_error` already
|
|
9
|
+
* uses. A refusal under 500 is about this request — the broker says unknown,
|
|
10
|
+
* expired, or not this application's — and retrying it will fail again. A
|
|
11
|
+
* 500, or no answer at all, is about the service, and retrying later may
|
|
12
|
+
* well work. The page says which, because "try again" is good advice for one
|
|
13
|
+
* and useless for the other.
|
|
14
|
+
*/
|
|
15
|
+
function authRequestReason(error) {
|
|
16
|
+
return error instanceof BrokerError && error.status < 500 ? 'request' : 'unavailable';
|
|
17
|
+
}
|
|
6
18
|
export function nextAuth(options) {
|
|
7
19
|
const auth = createAuth(options);
|
|
8
20
|
const proxy = async (request, rules) => {
|
|
@@ -39,16 +51,44 @@ export function nextAuth(options) {
|
|
|
39
51
|
redirect(auth.startUrl(next));
|
|
40
52
|
return user;
|
|
41
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* The auth request the issuer sent the browser here with.
|
|
56
|
+
*
|
|
57
|
+
* **No id starts a flow; an id that will not resolve does not.** The
|
|
58
|
+
* distinction is the whole of this function, and getting it wrong is not a
|
|
59
|
+
* degraded page but an infinite one. Starting a flow because resolution
|
|
60
|
+
* failed sends the browser to the issuer, which sends it back here with a
|
|
61
|
+
* fresh id, which fails the same way — two requests to the issuer per lap,
|
|
62
|
+
* no error page, nothing in a log, forever. That is not hypothetical: a
|
|
63
|
+
* client id missing from the broker's registry produced exactly this on
|
|
64
|
+
* manage.wtfalch.dev, and the visible symptom was Cloudflare rate-limiting
|
|
65
|
+
* the operator, twenty minutes before anybody could see a 403 underneath
|
|
66
|
+
* it. The refusal was diagnosable from the first request and the loop hid
|
|
67
|
+
* it behind thousands.
|
|
68
|
+
*
|
|
69
|
+
* So a failure lands on `onError` with a reason, exactly as the callback's
|
|
70
|
+
* own failures do, and the person clicks to try again. A human click cannot
|
|
71
|
+
* loop. The reason distinguishes the two cases worth telling apart: the
|
|
72
|
+
* broker refused this request (unknown, expired, or belonging to another
|
|
73
|
+
* application) or the broker could not be reached at all.
|
|
74
|
+
*
|
|
75
|
+
* The cause goes to the server log either way. A library swallowing the one
|
|
76
|
+
* error that explains the page is what made this expensive.
|
|
77
|
+
*/
|
|
42
78
|
const authRequest = async (searchParams, opts = {}) => {
|
|
43
79
|
const params = await searchParams;
|
|
44
80
|
const id = typeof params.authRequest === 'string' ? params.authRequest : null;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
// Nobody has started a flow yet. This cannot loop: the issuer always
|
|
82
|
+
// sends the browser back carrying an id.
|
|
83
|
+
if (!id)
|
|
84
|
+
redirect(auth.startUrl(opts.next, opts.intent));
|
|
85
|
+
try {
|
|
86
|
+
return await auth.authRequest(id);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error('@wtfalch/auth: could not resolve the auth request', error);
|
|
90
|
+
redirect(auth.errorUrl(authRequestReason(error)));
|
|
50
91
|
}
|
|
51
|
-
redirect(auth.startUrl(opts.next, opts.intent));
|
|
52
92
|
};
|
|
53
93
|
// A server action cannot redirect into a route handler, so the callback runs here and the cookies are set on the action's response.
|
|
54
94
|
const finish = async (callbackUrl) => {
|
|
@@ -78,19 +118,27 @@ export function nextAuth(options) {
|
|
|
78
118
|
else
|
|
79
119
|
store.delete({ name: c.name, path: '/' });
|
|
80
120
|
}
|
|
121
|
+
if (result.hosted)
|
|
122
|
+
redirect(result.location);
|
|
81
123
|
const url = new URL(result.location);
|
|
82
124
|
redirect(`${url.pathname}${url.search}`);
|
|
83
125
|
};
|
|
84
126
|
const signIn = async (input) => {
|
|
85
127
|
const result = await auth.signIn(input);
|
|
86
|
-
if (result.ok)
|
|
128
|
+
if (result.ok) {
|
|
129
|
+
if (result.hosted)
|
|
130
|
+
redirect(result.redirectTo);
|
|
87
131
|
return finish(result.redirectTo);
|
|
132
|
+
}
|
|
88
133
|
return { error: result.error };
|
|
89
134
|
};
|
|
90
135
|
const signUp = async (input) => {
|
|
91
136
|
const result = await auth.signUp(input);
|
|
92
|
-
if (result.ok)
|
|
137
|
+
if (result.ok) {
|
|
138
|
+
if (result.hosted)
|
|
139
|
+
redirect(result.redirectTo);
|
|
93
140
|
return finish(result.redirectTo);
|
|
141
|
+
}
|
|
94
142
|
return { error: result.error, message: result.message };
|
|
95
143
|
};
|
|
96
144
|
return {
|
|
@@ -106,6 +154,8 @@ export function nextAuth(options) {
|
|
|
106
154
|
requestPasswordReset: auth.requestPasswordReset,
|
|
107
155
|
sendLink: auth.sendLink,
|
|
108
156
|
invite: auth.invite,
|
|
157
|
+
people: auth.people,
|
|
158
|
+
setPersonActive: auth.setPersonActive,
|
|
109
159
|
resetPassword,
|
|
110
160
|
};
|
|
111
161
|
}
|
package/dist/redirect.js
CHANGED
|
@@ -12,5 +12,12 @@ export function safeNextPath(next, fallback) {
|
|
|
12
12
|
}
|
|
13
13
|
if (url.origin !== 'https://app.invalid')
|
|
14
14
|
return fallback;
|
|
15
|
-
|
|
15
|
+
const path = `${url.pathname}${url.search}`;
|
|
16
|
+
// The URL parser can normalise an input like "/..//evil" into a
|
|
17
|
+
// protocol-relative "//evil", which a caller resolving it against an origin
|
|
18
|
+
// would send to another host. Guard the OUTPUT, not just the input.
|
|
19
|
+
if (!path.startsWith('/') || path.startsWith('//') || path.startsWith('/\\')) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
return path;
|
|
16
23
|
}
|