@proteinjs/user-server 1.8.0 → 1.9.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 +29 -0
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +3 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/src/authentication/DbSessionStore.d.ts +3 -2
- package/dist/src/authentication/DbSessionStore.d.ts.map +1 -1
- package/dist/src/authentication/DbSessionStore.js +3 -2
- package/dist/src/authentication/DbSessionStore.js.map +1 -1
- package/dist/src/authentication/establishSession.d.ts +30 -0
- package/dist/src/authentication/establishSession.d.ts.map +1 -0
- package/dist/src/authentication/establishSession.js +83 -0
- package/dist/src/authentication/establishSession.js.map +1 -0
- package/dist/src/routes/devLogin.d.ts.map +1 -1
- package/dist/src/routes/devLogin.js +6 -12
- package/dist/src/routes/devLogin.js.map +1 -1
- package/dist/src/routes/login.d.ts.map +1 -1
- package/dist/src/routes/login.js +2 -3
- package/dist/src/routes/login.js.map +1 -1
- package/dist/src/routes/logout.d.ts +10 -0
- package/dist/src/routes/logout.d.ts.map +1 -1
- package/dist/src/routes/logout.js +13 -32
- package/dist/src/routes/logout.js.map +1 -1
- package/dist/src/routes/signup.d.ts +19 -0
- package/dist/src/routes/signup.d.ts.map +1 -0
- package/dist/src/routes/signup.js +102 -0
- package/dist/src/routes/signup.js.map +1 -0
- package/dist/src/routes/validateResetPasswordToken.d.ts.map +1 -1
- package/dist/src/routes/validateResetPasswordToken.js +5 -1
- package/dist/src/routes/validateResetPasswordToken.js.map +1 -1
- package/dist/src/services/AccessInvite.d.ts.map +1 -1
- package/dist/src/services/AccessInvite.js +32 -15
- package/dist/src/services/AccessInvite.js.map +1 -1
- package/dist/src/services/Signup.d.ts +18 -1
- package/dist/src/services/Signup.d.ts.map +1 -1
- package/dist/src/services/Signup.js +8 -2
- package/dist/src/services/Signup.js.map +1 -1
- package/dist/test/AccountDeletion.integration.test.js +24 -16
- package/dist/test/AccountDeletion.integration.test.js.map +1 -1
- package/dist/test/DevLogin.test.js +15 -20
- package/dist/test/DevLogin.test.js.map +1 -1
- package/dist/test/LogoutRoute.test.d.ts +2 -0
- package/dist/test/LogoutRoute.test.d.ts.map +1 -0
- package/dist/test/LogoutRoute.test.js +81 -0
- package/dist/test/LogoutRoute.test.js.map +1 -0
- package/dist/test/Signup.test.js +4 -2
- package/dist/test/Signup.test.js.map +1 -1
- package/dist/test/SignupRoute.test.d.ts +2 -0
- package/dist/test/SignupRoute.test.d.ts.map +1 -0
- package/dist/test/SignupRoute.test.js +338 -0
- package/dist/test/SignupRoute.test.js.map +1 -0
- package/dist/test/ValidateResetToken.test.d.ts +2 -0
- package/dist/test/ValidateResetToken.test.d.ts.map +1 -0
- package/dist/test/ValidateResetToken.test.js +170 -0
- package/dist/test/ValidateResetToken.test.js.map +1 -0
- package/dist/test/passportSessionHarness.d.ts +31 -0
- package/dist/test/passportSessionHarness.d.ts.map +1 -0
- package/dist/test/passportSessionHarness.js +107 -0
- package/dist/test/passportSessionHarness.js.map +1 -0
- package/generated/index.ts +3 -1
- package/package.json +11 -9
- package/src/authentication/DbSessionStore.ts +3 -2
- package/src/authentication/establishSession.ts +33 -0
- package/src/routes/devLogin.ts +4 -6
- package/src/routes/login.ts +2 -3
- package/src/routes/logout.ts +13 -18
- package/src/routes/signup.ts +55 -0
- package/src/routes/validateResetPasswordToken.ts +5 -1
- package/src/services/AccessInvite.ts +36 -12
- package/src/services/Signup.ts +21 -2
- package/test/AccountDeletion.integration.test.ts +15 -11
- package/test/DevLogin.test.ts +11 -14
- package/test/LogoutRoute.test.ts +33 -0
- package/test/Signup.test.ts +4 -2
- package/test/SignupRoute.test.ts +219 -0
- package/test/ValidateResetToken.test.ts +86 -0
- package/test/passportSessionHarness.ts +58 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
import { getDbAsSystem } from '@proteinjs/db';
|
|
3
|
+
import { tables } from '@proteinjs/user';
|
|
4
|
+
import { validateResetPasswordToken } from '../src/routes/validateResetPasswordToken';
|
|
5
|
+
import { UserServerTestEnvironment } from './UserServerTestEnvironment';
|
|
6
|
+
|
|
7
|
+
const testEnv = new UserServerTestEnvironment();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `GET /user/validate-reset-token`. Beyond the valid/expired/unknown verdicts, the valid
|
|
11
|
+
* response carries the account EMAIL: the reset page renders it as the read-only
|
|
12
|
+
* `autocomplete="username"` field so password managers can associate the updated password
|
|
13
|
+
* with the stored credential. The email only ever rides a VALID token's response — the
|
|
14
|
+
* token was delivered to that very inbox, so it reveals nothing the holder doesn't know —
|
|
15
|
+
* while invalid/expired verdicts stay email-free (no account-probing oracle).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type RouteOutcome = { status?: number; body?: any };
|
|
19
|
+
|
|
20
|
+
const invokeValidate = async (query: Record<string, unknown>): Promise<RouteOutcome> => {
|
|
21
|
+
const outcome: RouteOutcome = {};
|
|
22
|
+
const response = {
|
|
23
|
+
status(code: number) {
|
|
24
|
+
outcome.status = code;
|
|
25
|
+
return this;
|
|
26
|
+
},
|
|
27
|
+
send(body?: unknown) {
|
|
28
|
+
outcome.body = body;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
await validateResetPasswordToken.onRequest({ query } as never, response as never);
|
|
32
|
+
return outcome;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const armResetToken = async (email: string, token: string, expiration: moment.Moment) => {
|
|
36
|
+
const user = await testEnv.createUser({ name: 'Reset User', email });
|
|
37
|
+
await getDbAsSystem().update(tables.User, {
|
|
38
|
+
id: user.id,
|
|
39
|
+
passwordResetToken: token,
|
|
40
|
+
passwordResetTokenExpiration: expiration,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe('validateResetPasswordToken route', () => {
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
await testEnv.beforeAll();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterAll(async () => {
|
|
50
|
+
await testEnv.afterAll();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('a valid token resolves isValid WITH the account email (the reset form identifier)', async () => {
|
|
54
|
+
await armResetToken('reset-valid@test.local', 'tok-valid-1', moment().add(1, 'hour'));
|
|
55
|
+
|
|
56
|
+
const outcome = await invokeValidate({ token: 'tok-valid-1' });
|
|
57
|
+
|
|
58
|
+
expect(outcome.status).toBe(200);
|
|
59
|
+
expect(outcome.body).toEqual({ isValid: true, email: 'reset-valid@test.local' });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('an expired token resolves invalid and leaks no email', async () => {
|
|
63
|
+
await armResetToken('reset-expired@test.local', 'tok-expired-1', moment().subtract(1, 'minute'));
|
|
64
|
+
|
|
65
|
+
const outcome = await invokeValidate({ token: 'tok-expired-1' });
|
|
66
|
+
|
|
67
|
+
expect(outcome.status).toBe(200);
|
|
68
|
+
expect(outcome.body.isValid).toBe(false);
|
|
69
|
+
expect(outcome.body.email).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('an unknown token resolves invalid and leaks no email', async () => {
|
|
73
|
+
const outcome = await invokeValidate({ token: 'tok-never-issued' });
|
|
74
|
+
|
|
75
|
+
expect(outcome.status).toBe(200);
|
|
76
|
+
expect(outcome.body.isValid).toBe(false);
|
|
77
|
+
expect(outcome.body.email).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('a missing token is a 400', async () => {
|
|
81
|
+
const outcome = await invokeValidate({});
|
|
82
|
+
|
|
83
|
+
expect(outcome.status).toBe(400);
|
|
84
|
+
expect(outcome.body.isValid).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import passport from 'passport';
|
|
2
|
+
|
|
3
|
+
/** Session-lifecycle events in call order — the session-establishment contract under test. */
|
|
4
|
+
export type SessionEvent = 'regenerate' | 'login' | 'save';
|
|
5
|
+
|
|
6
|
+
export type PassportRequest = {
|
|
7
|
+
/** Express-shaped request whose `login`/`logout` are REAL passport machinery. */
|
|
8
|
+
request: any;
|
|
9
|
+
/**
|
|
10
|
+
* 'regenerate' / 'login' / 'save' in call order. The full contract of session establishment:
|
|
11
|
+
* regenerate FIRST (a fresh session id on privilege change — session fixation), then the bind
|
|
12
|
+
* of the account onto the fresh session, then save (the row must commit before the response).
|
|
13
|
+
*/
|
|
14
|
+
events: SessionEvent[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds a request whose `request.login` / `request.logout` are the GENUINE passport 0.6
|
|
19
|
+
* machinery, wired through `authenticator.initialize()` — the same middleware the server runs —
|
|
20
|
+
* over an express-session-shaped session mock.
|
|
21
|
+
*
|
|
22
|
+
* Why real passport and not a hand-mocked `login`: since passport 0.6 (the CVE-2022-25896
|
|
23
|
+
* session-fixation fix), the regenerate and the commit-before-response save live INSIDE
|
|
24
|
+
* `request.login` (SessionManager.logIn: regenerate → serializeUser → bind → save). A mocked
|
|
25
|
+
* `login` would assert nothing about either — the door suites must drive the real owner to
|
|
26
|
+
* prove the doors inherit the contract.
|
|
27
|
+
*
|
|
28
|
+
* serializeUser mirrors @proteinjs/server's identity pass-through (the session stores the
|
|
29
|
+
* account email directly), instrumented to record the bind ('login') between 'regenerate' and
|
|
30
|
+
* 'save'. The session mock replicates the one express-session behavior passport depends on:
|
|
31
|
+
* `regenerate` REPLACES `request.session` with a fresh session (new id, no passport state), so
|
|
32
|
+
* the bind provably lands on the post-regeneration session.
|
|
33
|
+
*/
|
|
34
|
+
export const createPassportRequest = async (extra: Record<string, unknown> = {}): Promise<PassportRequest> => {
|
|
35
|
+
const events: SessionEvent[] = [];
|
|
36
|
+
const authenticator = new passport.Passport();
|
|
37
|
+
authenticator.serializeUser((user: unknown, done: (err: unknown, id?: unknown) => void) => {
|
|
38
|
+
events.push('login');
|
|
39
|
+
done(null, user);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const request: any = { ...extra };
|
|
43
|
+
const makeSession = (): any => ({
|
|
44
|
+
regenerate: (done: (err?: unknown) => void) => {
|
|
45
|
+
events.push('regenerate');
|
|
46
|
+
request.session = makeSession();
|
|
47
|
+
done();
|
|
48
|
+
},
|
|
49
|
+
save: (done: (err?: unknown) => void) => {
|
|
50
|
+
events.push('save');
|
|
51
|
+
done();
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
request.session = makeSession();
|
|
55
|
+
|
|
56
|
+
await new Promise<void>((resolve) => authenticator.initialize()(request, {} as never, () => resolve()));
|
|
57
|
+
return { request, events };
|
|
58
|
+
};
|