@wtfalch/auth 0.19.0 → 0.20.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 CHANGED
@@ -94,6 +94,15 @@ Auth-request lookup failures lead to the configured error page with
94
94
  `auth_error=request` or `auth_error=unavailable`. That page should show a
95
95
  retry link; it must not automatically restart authorization.
96
96
 
97
+ When a sign-in fails after it started (reasons such as `state`, `unavailable`,
98
+ `exchange`, `denied`, `organization`, `cookie`), the error URL is
99
+ `<onError>?auth_error=<reason>&next=<path>`. `next` is the page the person came
100
+ for, always a path starting with one `/`, URL-encoded as a single parameter, and
101
+ present only when the sealed value is a safe same-origin path. An `onError` that
102
+ already has a query string keeps it. `expired` and `request` carry no `next`:
103
+ there is no transaction to read it from. Use it for the retry link
104
+ (`/auth/start?next=<path>`); a page that ignores it behaves as before.
105
+
97
106
  ## In a browser app
98
107
 
99
108
  `@wtfalch/auth/browser` is the other half: a public PKCE client for an app that
package/dist/auth.d.ts CHANGED
@@ -278,6 +278,7 @@ export interface Auth {
278
278
  }): Promise<{
279
279
  userId: string;
280
280
  invited: boolean;
281
+ personalEmailStored: boolean;
281
282
  }>;
282
283
  handle(request: Request): Promise<Response>;
283
284
  /**
@@ -331,6 +332,17 @@ export interface Auth {
331
332
  userId: string;
332
333
  state: string;
333
334
  }>;
335
+ /**
336
+ * An admin's reset for somebody in this app's own organisation whom
337
+ * `requestPasswordReset` cannot otherwise get back in -- returns a fresh
338
+ * one-time password the caller is trusted to deliver some other way,
339
+ * rather than mailing it. Refused for a user outside this app's
340
+ * organisation.
341
+ */
342
+ adminResetPassword(userId: string): Promise<{
343
+ userId: string;
344
+ password: string;
345
+ }>;
334
346
  /**
335
347
  * Namespace mode: end every session this person has in this service, on
336
348
  * every replica, at their next request. Deactivating someone does this too.
package/dist/auth.js CHANGED
@@ -159,9 +159,9 @@ export function createAuth(input) {
159
159
  return failure('expired', options().namespace ? [] : [cleared]);
160
160
  const params = new URL(callbackUrl).searchParams;
161
161
  if (params.get('state') !== transaction.st)
162
- return failure('state', options().namespace ? [] : [cleared]);
162
+ return failure('state', options().namespace ? [] : [cleared], transaction.nx);
163
163
  if (options().namespace && !(await sessions.bindingActive()))
164
- return failure('unavailable', [cleared]);
164
+ return failure('unavailable', [cleared], transaction.nx);
165
165
  // A silent pass the issuer refused (login_required, interaction_required,
166
166
  // consent_required, or any other error) is not a failure: sign in the normal
167
167
  // way, once. `done` is already set, so `/start` will not go silent again.
@@ -181,7 +181,7 @@ export function createAuth(input) {
181
181
  session = await sessionCookie(tokens, requestHeaders);
182
182
  }
183
183
  catch (error) {
184
- return failure(reasonOf(error), [cleared]);
184
+ return failure(reasonOf(error), [cleared], transaction.nx);
185
185
  }
186
186
  await replacePreviousSession(cookieHeader);
187
187
  return {
@@ -539,6 +539,20 @@ export function createAuth(input) {
539
539
  emit({ type: 'invite', email: input.email, userId: result.userId, invited: result.invited });
540
540
  return result;
541
541
  };
542
+ /**
543
+ * An admin's reset for somebody in this app's own organisation whom
544
+ * `requestPasswordReset` cannot otherwise get back in -- nobody at the
545
+ * mailbox a personal address or the login address itself would reach.
546
+ * Refused for a user outside this app's organisation, exactly as
547
+ * `setPersonActive` is. The password is a one-time one: the person must
548
+ * choose their own at next sign-in, same as an invite's. Audited with the
549
+ * target's id and never the password itself.
550
+ */
551
+ const adminResetPassword = async (userId) => {
552
+ const result = await broker().adminResetPassword(userId);
553
+ emit({ type: 'adminResetPassword', userId: result.userId });
554
+ return result;
555
+ };
542
556
  const resetPassword = async ({ userId, code, password, next, }, requestHeaders) => {
543
557
  try {
544
558
  const flow = await serverFlow();
@@ -891,9 +905,13 @@ export function createAuth(input) {
891
905
  cookies,
892
906
  };
893
907
  };
894
- const failure = (reason, cookies) => {
908
+ /** `next`, when it is a safe same-origin path, rides on the error URL so the error page can offer a retry to it. */
909
+ const failure = (reason, cookies, next) => {
895
910
  const url = new URL(options().onError, options().appUrl);
896
911
  url.searchParams.set('auth_error', reason);
912
+ const retry = safeNextPath(next, '');
913
+ if (retry)
914
+ url.searchParams.set('next', retry);
897
915
  return { location: url.href, cookies };
898
916
  };
899
917
  /** `failure`'s location on its own, for a caller that redirects rather than answering. */
@@ -927,6 +945,7 @@ export function createAuth(input) {
927
945
  errorUrl,
928
946
  people,
929
947
  setPersonActive,
948
+ adminResetPassword,
930
949
  revokeSessions,
931
950
  sessions: listSessions,
932
951
  endSession,
package/dist/broker.d.ts CHANGED
@@ -21,6 +21,12 @@ export interface NewUser {
21
21
  * `password_change_required`, then `setNewPassword`).
22
22
  */
23
23
  changeRequired?: boolean;
24
+ /**
25
+ * Where this person's reset links go instead of their own login address --
26
+ * for somebody who cannot yet open the mailbox that address names. See
27
+ * `Auth['adminResetPassword']` for the person this does not cover.
28
+ */
29
+ personalEmail?: string;
24
30
  }
25
31
  /** One of the organisation's people, as the sign-in service reports them. */
26
32
  export interface Person {
@@ -64,6 +70,11 @@ export declare class Broker {
64
70
  userId: string;
65
71
  state: string;
66
72
  }>;
73
+ /** An admin's reset for somebody requestPasswordReset cannot get back in: see `Auth['adminResetPassword']`. */
74
+ adminResetPassword(userId: string): Promise<{
75
+ userId: string;
76
+ password: string;
77
+ }>;
67
78
  authRequest(id: string): Promise<AuthRequest>;
68
79
  signIn(input: {
69
80
  authRequestId: string;
@@ -119,9 +130,11 @@ export declare class Broker {
119
130
  givenName: string;
120
131
  familyName: string;
121
132
  next: string;
133
+ personalEmail?: string;
122
134
  }): Promise<{
123
135
  userId: string;
124
136
  invited: boolean;
137
+ personalEmailStored: boolean;
125
138
  }>;
126
139
  sendLink(email: string, next: string): Promise<unknown>;
127
140
  sendReset(email: string, next: string): Promise<unknown>;
package/dist/broker.js CHANGED
@@ -67,6 +67,10 @@ export class Broker {
67
67
  setPersonActive(userId, active) {
68
68
  return this.call('POST', active ? '/people/reactivate' : '/people/deactivate', { userId });
69
69
  }
70
+ /** An admin's reset for somebody requestPasswordReset cannot get back in: see `Auth['adminResetPassword']`. */
71
+ adminResetPassword(userId) {
72
+ return this.call('POST', '/people/reset-password', { userId });
73
+ }
70
74
  authRequest(id) {
71
75
  return this.call('GET', '/auth-request', { id });
72
76
  }
package/dist/config.d.ts CHANGED
@@ -19,6 +19,11 @@ export type AuthEvent = {
19
19
  email: string;
20
20
  userId: string;
21
21
  invited: boolean;
22
+ }
23
+ /** Never the password: an audit record of who was reset, not a copy of the credential. */
24
+ | {
25
+ type: 'adminResetPassword';
26
+ userId: string;
22
27
  };
23
28
  /**
24
29
  * Why `accessToken` refused: no session (no cookie in cookie mode, no live
package/dist/next.d.ts CHANGED
@@ -95,14 +95,18 @@ export interface NextAuth {
95
95
  givenName: string;
96
96
  familyName: string;
97
97
  next?: string;
98
+ personalEmail?: string;
98
99
  }) => Promise<{
99
100
  userId: string;
100
101
  invited: boolean;
102
+ personalEmailStored: boolean;
101
103
  }>;
102
104
  /** The organisation's people, for an operator surface. Scoped to this app's organisation by the service. */
103
105
  people: Auth['people'];
104
106
  /** Deactivate or reactivate somebody in this app's organisation. */
105
107
  setPersonActive: Auth['setPersonActive'];
108
+ /** An admin's reset for somebody in this app's organisation whom requestPasswordReset cannot otherwise get back in. */
109
+ adminResetPassword: Auth['adminResetPassword'];
106
110
  /** The devices page: the signed-in person's own live sessions, newest first. Namespace mode only. */
107
111
  sessions: () => Promise<SessionRecord[]>;
108
112
  /**
package/dist/next.js CHANGED
@@ -272,6 +272,7 @@ export function nextAuth(options) {
272
272
  invite: auth.invite,
273
273
  people: auth.people,
274
274
  setPersonActive: auth.setPersonActive,
275
+ adminResetPassword: auth.adminResetPassword,
275
276
  sessions: sessionsList,
276
277
  endSession,
277
278
  resetPassword,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/auth",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Sign in against a self-hosted ZITADEL issuer: a server session for a Next.js app, and a browser client with silent single sign-on across subdomains.",
5
5
  "repository": {
6
6
  "type": "git",