@wtfalch/auth 0.15.0 → 0.19.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 +93 -7
- package/dist/auth.d.ts +58 -3
- package/dist/auth.js +142 -14
- package/dist/broker.d.ts +17 -1
- package/dist/broker.js +11 -2
- package/dist/browser.d.ts +11 -9
- package/dist/browser.js +163 -71
- package/dist/config.d.ts +2 -3
- package/dist/config.js +4 -4
- package/dist/cookies.d.ts +7 -0
- package/dist/cookies.js +14 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -2
- package/dist/names.d.ts +43 -0
- package/dist/names.js +146 -0
- package/dist/names.list.d.ts +19 -0
- package/dist/names.list.js +42 -0
- package/dist/names.reserved.d.ts +18 -0
- package/dist/names.reserved.js +734 -0
- package/dist/next.d.ts +24 -2
- package/dist/next.js +28 -16
- package/dist/oidc.d.ts +2 -1
- package/dist/oidc.js +21 -0
- package/package.json +11 -4
package/dist/next.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { type Auth, type AuthRequest, type ChangePasswordResult, type DeletePasskeyResult, type EmailChangeResult, type GateRules, type Intent, type MaxAge, type NewUser, type Passkey, type PasskeyRegistrationResult, type PasskeyRegistrationStart, type PasskeySignInStart, type Prompt, type QrPollStatus, type ResetError, type SignInError, type SignUpError, type User } from './auth.js';
|
|
2
|
+
import { type Auth, type AuthRequest, type ChangePasswordResult, type DeletePasskeyResult, type EmailChangeResult, type GateRules, type Intent, type MaxAge, type NewUser, type Passkey, type PasskeyRegistrationResult, type PasskeyRegistrationStart, type PasskeySignInStart, type Prompt, type QrPollStatus, type ResetError, type SetNewPasswordError, type SetNewPasswordResult, type SignInError, type SignUpError, type User } from './auth.js';
|
|
3
3
|
import type { AuthEvent, AuthOptions } from './config.js';
|
|
4
4
|
import type { SessionRecord } from './sessions.js';
|
|
5
5
|
type SearchParams = Record<string, string | string[] | undefined>;
|
|
@@ -57,12 +57,31 @@ export interface NextAuth {
|
|
|
57
57
|
password: string;
|
|
58
58
|
}) => Promise<{
|
|
59
59
|
error: SignInError;
|
|
60
|
+
} | {
|
|
61
|
+
error: 'password_change_required';
|
|
62
|
+
changeToken: string;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* For a server action, after `signIn` returned `password_change_required`:
|
|
66
|
+
* sets the person's own password with no old one, signs them in and
|
|
67
|
+
* redirects. Returns the error otherwise.
|
|
68
|
+
*/
|
|
69
|
+
setNewPassword: (input: {
|
|
70
|
+
authRequestId: string;
|
|
71
|
+
changeToken: string;
|
|
72
|
+
newPassword: string;
|
|
73
|
+
}) => Promise<{
|
|
74
|
+
error: SetNewPasswordError;
|
|
60
75
|
}>;
|
|
61
76
|
signUp: (input: {
|
|
62
77
|
authRequestId: string;
|
|
63
78
|
} & NewUser) => Promise<{
|
|
64
79
|
error: SignUpError;
|
|
65
80
|
message: string;
|
|
81
|
+
} | {
|
|
82
|
+
error: 'password_change_required';
|
|
83
|
+
changeToken: string;
|
|
84
|
+
message: string;
|
|
66
85
|
}>;
|
|
67
86
|
/** Emails a fresh verification link. Never says whether the address exists. */
|
|
68
87
|
resendVerification: (email: string, next?: string) => Promise<void>;
|
|
@@ -138,6 +157,9 @@ export interface NextAuth {
|
|
|
138
157
|
credential: unknown;
|
|
139
158
|
}) => Promise<{
|
|
140
159
|
error: SignInError;
|
|
160
|
+
} | {
|
|
161
|
+
error: 'password_change_required';
|
|
162
|
+
changeToken: string;
|
|
141
163
|
}>;
|
|
142
164
|
/**
|
|
143
165
|
* Starts a QR sign-in from a server action or route handler, setting the
|
|
@@ -157,4 +179,4 @@ export interface NextAuth {
|
|
|
157
179
|
}>;
|
|
158
180
|
}
|
|
159
181
|
export declare function nextAuth(options: AuthOptions): NextAuth;
|
|
160
|
-
export type { Auth, AuthEvent, AuthOptions, AuthRequest, ChangePasswordResult, DeletePasskeyResult, EmailChangeResult, GateRules, Intent, MaxAge, NewUser, Passkey, PasskeyRegistrationResult, PasskeyRegistrationStart, PasskeySignInStart, Prompt, QrPollStatus, SessionRecord, User, };
|
|
182
|
+
export type { Auth, AuthEvent, AuthOptions, AuthRequest, ChangePasswordResult, DeletePasskeyResult, EmailChangeResult, GateRules, Intent, MaxAge, NewUser, Passkey, PasskeyRegistrationResult, PasskeyRegistrationStart, PasskeySignInStart, SetNewPasswordError, SetNewPasswordResult, Prompt, QrPollStatus, SessionRecord, User, };
|
package/dist/next.js
CHANGED
|
@@ -138,10 +138,7 @@ export function nextAuth(options) {
|
|
|
138
138
|
const request = await requestFromCookies();
|
|
139
139
|
const { location, cookies: set } = await auth.complete(callbackUrl, request.headers.get('cookie'), request.headers);
|
|
140
140
|
for (const c of set) {
|
|
141
|
-
|
|
142
|
-
store.set(c.name, c.value, c.attributes);
|
|
143
|
-
else
|
|
144
|
-
store.delete({ name: c.name, path: '/' });
|
|
141
|
+
store.set(c.name, c.value, c.attributes);
|
|
145
142
|
}
|
|
146
143
|
const url = new URL(location);
|
|
147
144
|
redirect(`${url.pathname}${url.search}`);
|
|
@@ -184,6 +181,9 @@ export function nextAuth(options) {
|
|
|
184
181
|
redirect(result.redirectTo);
|
|
185
182
|
return finish(result.redirectTo);
|
|
186
183
|
}
|
|
184
|
+
if (result.error === 'password_change_required') {
|
|
185
|
+
return { error: result.error, changeToken: result.changeToken };
|
|
186
|
+
}
|
|
187
187
|
return { error: result.error };
|
|
188
188
|
};
|
|
189
189
|
const resetPassword = async (input) => {
|
|
@@ -192,10 +192,7 @@ export function nextAuth(options) {
|
|
|
192
192
|
return { error: result.error, message: result.message };
|
|
193
193
|
const store = await cookies();
|
|
194
194
|
for (const c of result.cookies) {
|
|
195
|
-
|
|
196
|
-
store.set(c.name, c.value, c.attributes);
|
|
197
|
-
else
|
|
198
|
-
store.delete({ name: c.name, path: '/' });
|
|
195
|
+
store.set(c.name, c.value, c.attributes);
|
|
199
196
|
}
|
|
200
197
|
if (result.hosted)
|
|
201
198
|
redirect(result.location);
|
|
@@ -209,6 +206,23 @@ export function nextAuth(options) {
|
|
|
209
206
|
redirect(result.redirectTo);
|
|
210
207
|
return finish(result.redirectTo);
|
|
211
208
|
}
|
|
209
|
+
if (result.error === 'password_change_required') {
|
|
210
|
+
return { error: result.error, changeToken: result.changeToken };
|
|
211
|
+
}
|
|
212
|
+
return { error: result.error };
|
|
213
|
+
};
|
|
214
|
+
const setNewPassword = async (input) => {
|
|
215
|
+
// Named fields: a Server Action's argument is whatever the client sent.
|
|
216
|
+
const result = await auth.setNewPassword({
|
|
217
|
+
authRequestId: String(input?.authRequestId),
|
|
218
|
+
changeToken: String(input?.changeToken),
|
|
219
|
+
newPassword: String(input?.newPassword),
|
|
220
|
+
});
|
|
221
|
+
if (result.ok) {
|
|
222
|
+
if (result.hosted)
|
|
223
|
+
redirect(result.redirectTo);
|
|
224
|
+
return finish(result.redirectTo);
|
|
225
|
+
}
|
|
212
226
|
return { error: result.error };
|
|
213
227
|
};
|
|
214
228
|
const signUp = async (input) => {
|
|
@@ -218,16 +232,16 @@ export function nextAuth(options) {
|
|
|
218
232
|
redirect(result.redirectTo);
|
|
219
233
|
return finish(result.redirectTo);
|
|
220
234
|
}
|
|
235
|
+
if (result.error === 'password_change_required') {
|
|
236
|
+
return { error: result.error, changeToken: result.changeToken, message: result.message };
|
|
237
|
+
}
|
|
221
238
|
return { error: result.error, message: result.message };
|
|
222
239
|
};
|
|
223
240
|
const startQrSignIn = async () => {
|
|
224
241
|
const { cookies: set, ...start } = await auth.startQrSignIn();
|
|
225
242
|
const store = await cookies();
|
|
226
243
|
for (const c of set) {
|
|
227
|
-
|
|
228
|
-
store.set(c.name, c.value, c.attributes);
|
|
229
|
-
else
|
|
230
|
-
store.delete({ name: c.name, path: '/' });
|
|
244
|
+
store.set(c.name, c.value, c.attributes);
|
|
231
245
|
}
|
|
232
246
|
return start;
|
|
233
247
|
};
|
|
@@ -235,10 +249,7 @@ export function nextAuth(options) {
|
|
|
235
249
|
const { status, reason, cookies: set } = await auth.pollQrSignIn(await requestFromCookies());
|
|
236
250
|
const store = await cookies();
|
|
237
251
|
for (const c of set) {
|
|
238
|
-
|
|
239
|
-
store.set(c.name, c.value, c.attributes);
|
|
240
|
-
else
|
|
241
|
-
store.delete({ name: c.name, path: '/' });
|
|
252
|
+
store.set(c.name, c.value, c.attributes);
|
|
242
253
|
}
|
|
243
254
|
return reason ? { status, reason } : { status };
|
|
244
255
|
};
|
|
@@ -253,6 +264,7 @@ export function nextAuth(options) {
|
|
|
253
264
|
requireFreshUser,
|
|
254
265
|
authRequest,
|
|
255
266
|
signIn,
|
|
267
|
+
setNewPassword,
|
|
256
268
|
signUp,
|
|
257
269
|
resendVerification: auth.resendVerification,
|
|
258
270
|
requestPasswordReset: auth.requestPasswordReset,
|
package/dist/oidc.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare class Oidc {
|
|
|
44
44
|
get redirectUri(): URL;
|
|
45
45
|
authorizationUrl(state: string, nonce: string, codeVerifier: string, { create, prompt, maxAge, }?: {
|
|
46
46
|
create?: boolean;
|
|
47
|
-
prompt?: 'select_account' | 'login';
|
|
47
|
+
prompt?: 'select_account' | 'login' | 'none';
|
|
48
48
|
maxAge?: number;
|
|
49
49
|
}): Promise<URL>;
|
|
50
50
|
exchange(callbackParams: URLSearchParams, transaction: {
|
|
@@ -61,6 +61,7 @@ export declare class Oidc {
|
|
|
61
61
|
endSessionUrl(idToken: string, postLogoutRedirectUri: string): Promise<URL | null>;
|
|
62
62
|
private tokensFrom;
|
|
63
63
|
verify(idToken: string): Promise<jose.JWTPayload>;
|
|
64
|
+
private assertIdentityClaims;
|
|
64
65
|
private assertOrganization;
|
|
65
66
|
}
|
|
66
67
|
export type AuthErrorReason = 'expired' | 'state' | 'denied' | 'exchange' | 'organization' | 'cookie' | 'request' | 'account_changed' | 'unavailable'
|
package/dist/oidc.js
CHANGED
|
@@ -147,6 +147,7 @@ export class Oidc {
|
|
|
147
147
|
if (!response.id_token || !claims) {
|
|
148
148
|
throw new AuthError('exchange', 'the issuer returned no id token');
|
|
149
149
|
}
|
|
150
|
+
this.assertIdentityClaims(claims);
|
|
150
151
|
this.assertOrganization(claims);
|
|
151
152
|
const expiresIn = response.expiresIn();
|
|
152
153
|
return {
|
|
@@ -163,6 +164,7 @@ export class Oidc {
|
|
|
163
164
|
audience: this.options.clientId,
|
|
164
165
|
algorithms: ['RS256'],
|
|
165
166
|
clockTolerance: 60,
|
|
167
|
+
requiredClaims: ['exp', 'iat', 'sub'],
|
|
166
168
|
});
|
|
167
169
|
// ZITADEL puts the project in aud as well, so azp has to name us. Same rule openid-client applies on exchange.
|
|
168
170
|
if (Array.isArray(payload.aud) &&
|
|
@@ -170,9 +172,28 @@ export class Oidc {
|
|
|
170
172
|
payload.azp !== this.options.clientId) {
|
|
171
173
|
throw new AuthError('exchange', 'the token was authorised for another party');
|
|
172
174
|
}
|
|
175
|
+
this.assertIdentityClaims(payload);
|
|
173
176
|
this.assertOrganization(payload);
|
|
174
177
|
return payload;
|
|
175
178
|
}
|
|
179
|
+
// Both grant responses and stored/device tokens become a User. Keep their
|
|
180
|
+
// identity and time shapes consistent, including optional freshness claims.
|
|
181
|
+
assertIdentityClaims(claims) {
|
|
182
|
+
if (typeof claims.sub !== 'string' || claims.sub.length === 0) {
|
|
183
|
+
throw new AuthError('exchange', 'the id token has no valid subject');
|
|
184
|
+
}
|
|
185
|
+
for (const name of ['exp', 'iat']) {
|
|
186
|
+
if (typeof claims[name] !== 'number' || !Number.isFinite(claims[name])) {
|
|
187
|
+
throw new AuthError('exchange', `the id token has no valid ${name}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (Object.hasOwn(claims, 'auth_time') &&
|
|
191
|
+
(typeof claims.auth_time !== 'number' ||
|
|
192
|
+
!Number.isFinite(claims.auth_time) ||
|
|
193
|
+
claims.auth_time < 0)) {
|
|
194
|
+
throw new AuthError('exchange', 'the id token has no valid auth_time');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
176
197
|
assertOrganization(claims) {
|
|
177
198
|
if (claims[ORG_CLAIM] !== this.options.organizationId) {
|
|
178
199
|
throw new AuthError('organization', `the token's organisation ${String(claims[ORG_CLAIM])} is not this app's`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/auth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sign in against
|
|
3
|
+
"version": "0.19.0",
|
|
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",
|
|
7
7
|
"url": "git+https://github.com/wtfalch/auth.git",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"types": "./dist/namespaces.d.ts",
|
|
31
31
|
"default": "./dist/namespaces.js"
|
|
32
32
|
},
|
|
33
|
+
"./names": {
|
|
34
|
+
"types": "./dist/names.d.ts",
|
|
35
|
+
"default": "./dist/names.js"
|
|
36
|
+
},
|
|
33
37
|
"./namespace-server": {
|
|
34
38
|
"types": "./dist/namespace-server.d.ts",
|
|
35
39
|
"default": "./dist/namespace-server.js"
|
|
@@ -51,11 +55,14 @@
|
|
|
51
55
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
52
56
|
"prepack": "pnpm build",
|
|
53
57
|
"typecheck": "tsc --noEmit",
|
|
54
|
-
"test": "vitest run"
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"test:cookie-browser": "vitest run --config vitest.cookie-browser.config.ts"
|
|
55
60
|
},
|
|
56
61
|
"dependencies": {
|
|
57
62
|
"jose": "^6.2.10",
|
|
58
|
-
"
|
|
63
|
+
"obscenity": "^0.4.6",
|
|
64
|
+
"openid-client": "6.8.7",
|
|
65
|
+
"reserved-usernames": "^1.1.6"
|
|
59
66
|
},
|
|
60
67
|
"peerDependencies": {
|
|
61
68
|
"next": ">=15.0.0"
|