@stamhoofd/backend 2.143.1 → 2.145.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/package.json +19 -19
- package/src/crons/fake-settlements.test.ts +13 -11
- package/src/crons/fake-settlements.ts +22 -38
- package/src/crons/mollie-chargebacks.ts +31 -29
- package/src/crons/mollie-refunds.ts +61 -59
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +16 -5
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +210 -4
- package/src/endpoints/auth/CreateTokenEndpoint.ts +9 -20
- package/src/endpoints/auth/DeleteTokenEndpoint.test.ts +29 -0
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +2 -1
- package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -11
- package/src/endpoints/auth/MFA.security.test.ts +9 -7
- package/src/endpoints/auth/MFA.test.ts +42 -15
- package/src/endpoints/auth/PatchUserEndpoint.ts +3 -2
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +5 -4
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +4 -3
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +6 -5
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +5 -4
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +5 -4
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -1
- package/src/endpoints/global/files/UploadFile.test.ts +2 -1
- package/src/endpoints/global/files/UploadFile.ts +53 -25
- package/src/endpoints/global/files/UploadImage.test.ts +2 -1
- package/src/endpoints/global/files/UploadImage.ts +32 -6
- package/src/endpoints/global/files/upload-security.test.ts +3 -2
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +4 -3
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +26 -25
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +57 -56
- package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.test.ts +4 -3
- package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +6 -4
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +6 -6
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +9 -8
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +14 -13
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +24 -23
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +4 -3
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +20 -19
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +4 -3
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +0 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +23 -22
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -4
- package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +4 -3
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +7 -6
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +9 -8
- package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +6 -5
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +16 -15
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +2 -1
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -3
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -5
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +94 -8
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +18 -18
- package/src/helpers/ApplicationFeeInvoicer.ts +1 -1
- package/src/helpers/Context.ts +14 -21
- package/src/helpers/ContextStorage.ts +5 -0
- package/src/helpers/MollieSettlementSync.test.ts +94 -41
- package/src/helpers/MollieSettlementSync.ts +174 -78
- package/src/helpers/StripeSettlementSync.test.ts +0 -85
- package/src/helpers/StripeSettlementSync.ts +47 -70
- package/src/helpers/TwoFactorHelper.ts +21 -22
- package/src/helpers/streamForBreakdown.ts +1 -1
- package/src/services/AdminSessionService.ts +5 -3
- package/src/services/InvoiceService.ts +2 -2
- package/src/services/MollieService.ts +168 -126
- package/src/services/PaymentService.ts +2 -2
- package/src/services/SSOService.test.ts +25 -1
- package/src/services/SSOService.ts +17 -6
- package/src/services/SessionService.test.ts +319 -0
- package/src/services/SessionService.ts +357 -0
- package/src/services/SettlementService.test.ts +13 -85
- package/src/services/SettlementService.ts +5 -113
- package/src/services/WebshopCrowdfundingService.test.ts +4 -3
- package/src/sql-filters/balance-items.ts +10 -0
- package/src/sql-filters/registrations.ts +59 -1
- package/tests/actions/patchUserMember.ts +2 -1
- package/tests/e2e/api-rate-limits.test.ts +2 -1
- package/tests/e2e/bundle-discounts.test.ts +4 -3
- package/tests/e2e/charge-members.test.ts +6 -5
- package/tests/e2e/documents.test.ts +4 -3
- package/tests/e2e/private-files.test.ts +9 -8
- package/tests/e2e/register.test.ts +4 -3
- package/tests/e2e/stock.test.ts +4 -3
- package/tests/e2e/tickets.test.ts +4 -3
- package/tests/helpers/ExportSlice.ts +2 -1
- package/tests/helpers/MollieMocker.ts +173 -38
- package/tests/init/initAdmin.ts +2 -1
- package/tests/init/initPlatformAdmin.ts +2 -1
- package/src/helpers/ViesHelper.test.ts +0 -401
- package/src/helpers/ViesHelper.ts +0 -215
- package/src/services/PeppolDirectoryService.test.ts +0 -139
- package/src/services/PeppolDirectoryService.ts +0 -111
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import {
|
|
3
|
-
import { Token
|
|
2
|
+
import type { Organization } from '@stamhoofd/models';
|
|
3
|
+
import { OrganizationFactory, Token, User, UserFactory, UserSession } from '@stamhoofd/models';
|
|
4
|
+
import { SESSION_DURATIONS } from '@stamhoofd/models/constants/sessions.js';
|
|
5
|
+
import { PermissionLevel, Permissions, SessionClientType, Token as TokenStruct } from '@stamhoofd/structures';
|
|
6
|
+
import { SessionService } from '../../services/SessionService.js';
|
|
4
7
|
|
|
5
8
|
import { testServer } from '../../../tests/helpers/TestServer.js';
|
|
6
9
|
import { CreateTokenEndpoint } from './CreateTokenEndpoint.js';
|
|
@@ -41,7 +44,7 @@ describe('Endpoint.CreateToken', () => {
|
|
|
41
44
|
// Also check UTF8 passwords
|
|
42
45
|
const password = '54😂test👌🏾86s&é';
|
|
43
46
|
const user = await new UserFactory({ organization, password }).create();
|
|
44
|
-
const token = await
|
|
47
|
+
const token = await SessionService.createSession(user);
|
|
45
48
|
|
|
46
49
|
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
47
50
|
grant_type: 'refresh_token',
|
|
@@ -94,7 +97,7 @@ describe('Endpoint.CreateToken', () => {
|
|
|
94
97
|
test('refreshing a token marks the user as active again', async () => {
|
|
95
98
|
const organization = await new OrganizationFactory({}).create();
|
|
96
99
|
const user = await new UserFactory({ organization, password }).create();
|
|
97
|
-
const token = await
|
|
100
|
+
const token = await SessionService.createSession(user);
|
|
98
101
|
|
|
99
102
|
const monthsAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
100
103
|
monthsAgo.setMilliseconds(0);
|
|
@@ -125,4 +128,207 @@ describe('Endpoint.CreateToken', () => {
|
|
|
125
128
|
expect(await getLastActiveAt(user)).toBeNull();
|
|
126
129
|
});
|
|
127
130
|
});
|
|
131
|
+
|
|
132
|
+
describe('session length', () => {
|
|
133
|
+
const password = 'test-password-1234';
|
|
134
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
135
|
+
|
|
136
|
+
function expectValidFor(date: Date, duration: number) {
|
|
137
|
+
expect(date.getTime()).toBeGreaterThan(Date.now() + duration - 5000);
|
|
138
|
+
expect(date.getTime()).toBeLessThanOrEqual(Date.now() + duration);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function login(organization: Organization, user: User, platform: string | null = null): Promise<Token> {
|
|
142
|
+
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
143
|
+
grant_type: 'password',
|
|
144
|
+
username: user.email,
|
|
145
|
+
password,
|
|
146
|
+
});
|
|
147
|
+
if (platform) {
|
|
148
|
+
r.headers['x-platform'] = platform;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const response = await testServer.test(endpoint, r);
|
|
152
|
+
if (!(response.body instanceof TokenStruct)) {
|
|
153
|
+
throw new Error('Expected TokenStruct');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const token = await Token.getByAccessToken(response.body.accessToken);
|
|
157
|
+
expect(token).toBeDefined();
|
|
158
|
+
return token!;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function refresh(organization: Organization, refreshToken: string): Promise<Token> {
|
|
162
|
+
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
163
|
+
grant_type: 'refresh_token',
|
|
164
|
+
refresh_token: refreshToken,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const response = await testServer.test(endpoint, r);
|
|
168
|
+
if (!(response.body instanceof TokenStruct)) {
|
|
169
|
+
throw new Error('Expected TokenStruct');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const token = await Token.getByAccessToken(response.body.accessToken);
|
|
173
|
+
expect(token).toBeDefined();
|
|
174
|
+
return token!;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function getSession(token: Token): Promise<UserSession> {
|
|
178
|
+
const session = await UserSession.getByID(token.sessionId);
|
|
179
|
+
if (!session) throw new Error('Expected user session');
|
|
180
|
+
return session;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
test('an administrator that signs in in a browser gets a limited session', async () => {
|
|
184
|
+
const organization = await new OrganizationFactory({}).create();
|
|
185
|
+
const admin = await new UserFactory({
|
|
186
|
+
organization,
|
|
187
|
+
password,
|
|
188
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
189
|
+
}).create();
|
|
190
|
+
|
|
191
|
+
const token = await login(organization, admin, 'web');
|
|
192
|
+
|
|
193
|
+
expect((await getSession(token)).clientType).toBe(SessionClientType.Browser);
|
|
194
|
+
expectValidFor(token.refreshTokenValidUntil, SESSION_DURATIONS.admin[SessionClientType.Browser].refreshToken);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('an administrator that signs in in the native app has no maximum session length', async () => {
|
|
198
|
+
const organization = await new OrganizationFactory({}).create();
|
|
199
|
+
const admin = await new UserFactory({
|
|
200
|
+
organization,
|
|
201
|
+
password,
|
|
202
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
203
|
+
}).create();
|
|
204
|
+
|
|
205
|
+
const token = await login(organization, admin, 'ios');
|
|
206
|
+
|
|
207
|
+
expect((await getSession(token)).clientType).toBe(SessionClientType.iOS);
|
|
208
|
+
expectValidFor(token.refreshTokenValidUntil, SESSION_DURATIONS.admin[SessionClientType.iOS].refreshToken);
|
|
209
|
+
|
|
210
|
+
const browserSessionDuration = SESSION_DURATIONS.admin[SessionClientType.Browser].session;
|
|
211
|
+
if (browserSessionDuration === null) {
|
|
212
|
+
throw new Error('Expected browser administrator sessions to have a maximum length');
|
|
213
|
+
}
|
|
214
|
+
const userSession = await getSession(token);
|
|
215
|
+
userSession.startedAt = new Date(Date.now() - 2 * browserSessionDuration);
|
|
216
|
+
await userSession.save();
|
|
217
|
+
|
|
218
|
+
const renewed = await refresh(organization, token.refreshToken);
|
|
219
|
+
expectValidFor(renewed.refreshTokenValidUntil, SESSION_DURATIONS.admin[SessionClientType.iOS].refreshToken);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('renewing the same active token twice revokes the first replacement', async () => {
|
|
223
|
+
const organization = await new OrganizationFactory({}).create();
|
|
224
|
+
const user = await new UserFactory({ organization, password }).create();
|
|
225
|
+
const original = await login(organization, user);
|
|
226
|
+
|
|
227
|
+
const firstReplacement = await refresh(organization, original.refreshToken);
|
|
228
|
+
const secondReplacement = await refresh(organization, original.refreshToken);
|
|
229
|
+
|
|
230
|
+
expect(secondReplacement.sessionId).toBe(original.sessionId);
|
|
231
|
+
expect(await Token.getByAccessToken(firstReplacement.accessToken)).toBeUndefined();
|
|
232
|
+
expect(await Token.getByAccessToken(original.accessToken)).toBeDefined();
|
|
233
|
+
expect(await Token.getByAccessToken(secondReplacement.accessToken)).toBeDefined();
|
|
234
|
+
|
|
235
|
+
const sessionTokens = await Token.where({ sessionId: original.sessionId });
|
|
236
|
+
expect(sessionTokens.map(token => token.id).sort()).toEqual([original.id, secondReplacement.id].sort());
|
|
237
|
+
|
|
238
|
+
const forkRequest = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
239
|
+
grant_type: 'refresh_token',
|
|
240
|
+
refresh_token: firstReplacement.refreshToken,
|
|
241
|
+
});
|
|
242
|
+
await expect(testServer.test(endpoint, forkRequest)).rejects.toMatchObject({ code: 'invalid_refresh_token' });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('an SSO handoff refresh token can only be exchanged once', async () => {
|
|
246
|
+
const organization = await new OrganizationFactory({}).create();
|
|
247
|
+
const user = await new UserFactory({ organization }).create();
|
|
248
|
+
const handoff = await SessionService.createSSOHandoff(user);
|
|
249
|
+
|
|
250
|
+
const sessionToken = await refresh(organization, handoff.refreshToken);
|
|
251
|
+
expect(sessionToken.sessionId).toBe(handoff.sessionId);
|
|
252
|
+
|
|
253
|
+
await expect(refresh(organization, handoff.refreshToken)).rejects.toMatchObject({
|
|
254
|
+
code: 'invalid_refresh_token',
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('concurrent exchanges cannot fork an SSO handoff', async () => {
|
|
259
|
+
const organization = await new OrganizationFactory({}).create();
|
|
260
|
+
const user = await new UserFactory({ organization }).create();
|
|
261
|
+
const handoff = await SessionService.createSSOHandoff(user);
|
|
262
|
+
|
|
263
|
+
const exchanges = await Promise.allSettled([
|
|
264
|
+
refresh(organization, handoff.refreshToken),
|
|
265
|
+
refresh(organization, handoff.refreshToken),
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
expect(exchanges.filter(result => result.status === 'fulfilled')).toHaveLength(1);
|
|
269
|
+
const rejected = exchanges.filter(result => result.status === 'rejected');
|
|
270
|
+
expect(rejected).toHaveLength(1);
|
|
271
|
+
expect(rejected[0].reason).toMatchObject({ code: 'invalid_refresh_token' });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('renewing an access token does not extend the session past its maximum length', async () => {
|
|
275
|
+
const organization = await new OrganizationFactory({}).create();
|
|
276
|
+
const admin = await new UserFactory({
|
|
277
|
+
organization,
|
|
278
|
+
password,
|
|
279
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
280
|
+
}).create();
|
|
281
|
+
|
|
282
|
+
const token = await login(organization, admin, 'web');
|
|
283
|
+
|
|
284
|
+
const sessionDuration = SESSION_DURATIONS.admin[SessionClientType.Browser].session;
|
|
285
|
+
if (sessionDuration === null) {
|
|
286
|
+
throw new Error('Expected browser administrator sessions to have a maximum length');
|
|
287
|
+
}
|
|
288
|
+
const sessionStartedAt = new Date(Date.now() - sessionDuration + DAY);
|
|
289
|
+
sessionStartedAt.setMilliseconds(0);
|
|
290
|
+
const userSession = await getSession(token);
|
|
291
|
+
userSession.startedAt = sessionStartedAt;
|
|
292
|
+
await userSession.save();
|
|
293
|
+
|
|
294
|
+
const renewed = await refresh(organization, token.refreshToken);
|
|
295
|
+
|
|
296
|
+
expect((await getSession(renewed)).startedAt).toEqual(sessionStartedAt);
|
|
297
|
+
expect(renewed.refreshTokenValidUntil.getTime()).toBeLessThanOrEqual(sessionStartedAt.getTime() + sessionDuration);
|
|
298
|
+
expect(renewed.refreshTokenValidUntil.getTime()).toBeGreaterThan(Date.now());
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('a session that reached its maximum length cannot be renewed', async () => {
|
|
302
|
+
const organization = await new OrganizationFactory({}).create();
|
|
303
|
+
const admin = await new UserFactory({
|
|
304
|
+
organization,
|
|
305
|
+
password,
|
|
306
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
307
|
+
}).create();
|
|
308
|
+
|
|
309
|
+
const token = await login(organization, admin, 'web');
|
|
310
|
+
const sessionDuration = SESSION_DURATIONS.admin[SessionClientType.Browser].session;
|
|
311
|
+
if (sessionDuration === null) {
|
|
312
|
+
throw new Error('Expected browser administrator sessions to have a maximum length');
|
|
313
|
+
}
|
|
314
|
+
const userSession = await getSession(token);
|
|
315
|
+
userSession.startedAt = new Date(Date.now() - sessionDuration - DAY);
|
|
316
|
+
await userSession.save();
|
|
317
|
+
token.refreshTokenValidUntil = new Date(Date.now() + DAY);
|
|
318
|
+
await token.save();
|
|
319
|
+
|
|
320
|
+
const otherSession = await login(organization, admin, 'web');
|
|
321
|
+
|
|
322
|
+
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
323
|
+
grant_type: 'refresh_token',
|
|
324
|
+
refresh_token: token.refreshToken,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await expect(testServer.test(endpoint, r)).rejects.toMatchObject({ code: 'invalid_refresh_token' });
|
|
328
|
+
expect(await Token.getByAccessToken(otherSession.accessToken)).toBeDefined();
|
|
329
|
+
|
|
330
|
+
await expect(testServer.test(endpoint, r)).rejects.toMatchObject({ code: 'invalid_refresh_token' });
|
|
331
|
+
expect(await Token.getByAccessToken(otherSession.accessToken)).toBeDefined();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
128
334
|
});
|
|
@@ -3,13 +3,14 @@ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
4
|
import { EmailVerificationCode, MFATOTP, MFAToken, PasswordToken, Platform, Token, User, WebauthnCredential } from '@stamhoofd/models';
|
|
5
5
|
import type { ChallengeGrantStruct, MFAGrantStruct, PasswordGrantStruct, PasswordTokenGrantStruct, RefreshTokenGrantStruct, RequestChallengeGrantStruct } from '@stamhoofd/structures';
|
|
6
|
-
import { CreateTokenStruct, LoginMethod, MFAMethodType, SignupResponse, Token as TokenStruct } from '@stamhoofd/structures';
|
|
6
|
+
import { CreateTokenStruct, LoginMethod, MFAMethodType, SessionLoginMethod, SignupResponse, Token as TokenStruct } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import { Context } from '../../helpers/Context.js';
|
|
9
9
|
import { RecoveryCodeHelper } from '../../helpers/RecoveryCodeHelper.js';
|
|
10
10
|
import { TOTPHelper } from '../../helpers/TOTPHelper.js';
|
|
11
11
|
import { mfaVerificationRateLimiter, TwoFactorHelper } from '../../helpers/TwoFactorHelper.js';
|
|
12
12
|
import { WebauthnHelper } from '../../helpers/WebauthnHelper.js';
|
|
13
|
+
import { SessionService } from '../../services/SessionService.js';
|
|
13
14
|
import { VerificationCodeService } from '../../services/VerificationCodeService.js';
|
|
14
15
|
|
|
15
16
|
type Params = Record<string, never>;
|
|
@@ -45,7 +46,7 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
45
46
|
|
|
46
47
|
switch (request.body.grantType) {
|
|
47
48
|
case 'refresh_token': {
|
|
48
|
-
const oldToken = await
|
|
49
|
+
const oldToken = await SessionService.getByRefreshToken(request.body.refreshToken);
|
|
49
50
|
if (!oldToken) {
|
|
50
51
|
throw new SimpleError({
|
|
51
52
|
code: 'invalid_refresh_token',
|
|
@@ -63,19 +64,7 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
const token = await Token.createToken(oldToken.user);
|
|
68
|
-
|
|
69
|
-
// In the rare event our response doesn't reach the client anymore, we don't want the client to sign out...
|
|
70
|
-
// So we allow a small rotation overlap period
|
|
71
|
-
const leeway = 60 * 1000;
|
|
72
|
-
oldToken.refreshTokenValidUntil = new Date(Math.min(oldToken.refreshTokenValidUntil.getTime(), Date.now() + leeway));
|
|
73
|
-
|
|
74
|
-
// Invalidate the corresponding access token
|
|
75
|
-
oldToken.accessTokenValidUntil = new Date(Date.now() - 60 * 60 * 1000);
|
|
76
|
-
|
|
77
|
-
// Do not delete the old one, only expire it fast so it will get deleted in the future
|
|
78
|
-
await oldToken.save();
|
|
67
|
+
const token = await SessionService.rotateSession(oldToken);
|
|
79
68
|
|
|
80
69
|
if (!token) {
|
|
81
70
|
throw new SimpleError({
|
|
@@ -150,9 +139,9 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
150
139
|
// Second factor / forced enrollment: block the session until the user
|
|
151
140
|
// completes (or first sets up) a second factor. Shared with every other
|
|
152
141
|
// grant that mints a session from a single primary credential.
|
|
153
|
-
await TwoFactorHelper.assertSecondFactorOrThrow(user, organization, request.request.getVersion(), { loginMethod:
|
|
142
|
+
await TwoFactorHelper.assertSecondFactorOrThrow(user, organization, request.request.getVersion(), { loginMethod: SessionLoginMethod.Password, i18n: request.i18n });
|
|
154
143
|
|
|
155
|
-
const token = await
|
|
144
|
+
const token = await SessionService.createSession(user, { loginMethod: SessionLoginMethod.Password, authenticatedAt: new Date() });
|
|
156
145
|
|
|
157
146
|
if (!token) {
|
|
158
147
|
throw new SimpleError({
|
|
@@ -264,7 +253,7 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
264
253
|
});
|
|
265
254
|
}
|
|
266
255
|
|
|
267
|
-
const token = await
|
|
256
|
+
const token = await SessionService.createSession(user, { loginMethod: mfaToken.loginMethod, authenticatedAt: new Date() });
|
|
268
257
|
await user.markActive();
|
|
269
258
|
|
|
270
259
|
const st = new TokenStruct(token);
|
|
@@ -314,10 +303,10 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
314
303
|
//
|
|
315
304
|
// The link was emailed to the account, so this is also how a long-inactive
|
|
316
305
|
// admin confirms their email address and gets to enroll after all.
|
|
317
|
-
await TwoFactorHelper.assertSecondFactorOrThrow(passwordToken.user, organization, request.request.getVersion(), { loginMethod:
|
|
306
|
+
await TwoFactorHelper.assertSecondFactorOrThrow(passwordToken.user, organization, request.request.getVersion(), { loginMethod: SessionLoginMethod.Email, i18n: request.i18n, allowTemporarySession: true });
|
|
318
307
|
|
|
319
308
|
// Important to create a new token before adjusting the old token
|
|
320
|
-
const token = await
|
|
309
|
+
const token = await SessionService.createSession(passwordToken.user, { loginMethod: SessionLoginMethod.Email, authenticatedAt: new Date() });
|
|
321
310
|
|
|
322
311
|
// TODO: make token short lived until renewal
|
|
323
312
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { OrganizationFactory, Token, UserFactory, UserSession } from '@stamhoofd/models';
|
|
3
|
+
|
|
4
|
+
import { testServer } from '../../../tests/helpers/TestServer.js';
|
|
5
|
+
import { SessionService } from '../../services/SessionService.js';
|
|
6
|
+
import { DeleteTokenEndpoint } from './DeleteTokenEndpoint.js';
|
|
7
|
+
|
|
8
|
+
describe('Endpoint.DeleteToken', () => {
|
|
9
|
+
const endpoint = new DeleteTokenEndpoint();
|
|
10
|
+
|
|
11
|
+
test('ends the session and deletes every token in it', async () => {
|
|
12
|
+
const organization = await new OrganizationFactory({}).create();
|
|
13
|
+
const user = await new UserFactory({ organization }).create();
|
|
14
|
+
const original = await SessionService.createSession(user);
|
|
15
|
+
const renewed = await SessionService.rotateSession(original);
|
|
16
|
+
const otherSession = await SessionService.createSession(user);
|
|
17
|
+
|
|
18
|
+
const request = Request.buildJson('DELETE', '/oauth/token', organization.getApiHost());
|
|
19
|
+
request.headers.authorization = 'Bearer ' + original.accessToken;
|
|
20
|
+
await testServer.test(endpoint, request);
|
|
21
|
+
|
|
22
|
+
expect(await UserSession.getByID(original.sessionId)).toBeUndefined();
|
|
23
|
+
expect(await Token.getByAccessToken(original.accessToken, true)).toBeUndefined();
|
|
24
|
+
expect(await Token.getByAccessToken(renewed.accessToken, true)).toBeUndefined();
|
|
25
|
+
expect(await UserSession.getByID(otherSession.sessionId)).toBeDefined();
|
|
26
|
+
expect(await Token.getByAccessToken(otherSession.accessToken, true)).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
});
|
|
@@ -2,6 +2,7 @@ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
|
2
2
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
|
|
4
4
|
import { Context } from '../../helpers/Context.js';
|
|
5
|
+
import { SessionService } from '../../services/SessionService.js';
|
|
5
6
|
|
|
6
7
|
type Params = Record<string, never>;
|
|
7
8
|
type Query = undefined;
|
|
@@ -25,7 +26,7 @@ export class DeleteTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
25
26
|
async handle(_: DecodedRequest<Params, Query, Body>) {
|
|
26
27
|
await Context.setOptionalOrganizationScope();
|
|
27
28
|
const { token } = await Context.authenticate({ allowWithoutAccount: true });
|
|
28
|
-
await
|
|
29
|
+
await SessionService.endSession(token);
|
|
29
30
|
|
|
30
31
|
return new Response(undefined);
|
|
31
32
|
}
|
|
@@ -3,6 +3,7 @@ import { MemberFactory, OrganizationFactory, Token, UserFactory } from '@stamhoo
|
|
|
3
3
|
|
|
4
4
|
import { MFATestHelper } from '../../../tests/helpers/MFATestHelper.js';
|
|
5
5
|
import { testServer } from '../../../tests/helpers/TestServer.js';
|
|
6
|
+
import { SessionService } from '../../services/SessionService.js';
|
|
6
7
|
import { GetUserEndpoint } from './GetUserEndpoint.js';
|
|
7
8
|
import { STExpect } from '@stamhoofd/test-utils';
|
|
8
9
|
|
|
@@ -13,7 +14,7 @@ describe('Endpoint.GetUser', () => {
|
|
|
13
14
|
test('Request user details when signed in', async () => {
|
|
14
15
|
const organization = await new OrganizationFactory({}).create();
|
|
15
16
|
const user = await new UserFactory({ organization }).create();
|
|
16
|
-
const token = await
|
|
17
|
+
const token = await SessionService.createSession(user);
|
|
17
18
|
|
|
18
19
|
const r = Request.buildJson('GET', '/v1/user', organization.getApiHost());
|
|
19
20
|
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
@@ -23,13 +24,32 @@ describe('Endpoint.GetUser', () => {
|
|
|
23
24
|
expect(response.body.id).toEqual(user.id);
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
test('using a renewed token retires the previous active token', async () => {
|
|
28
|
+
const organization = await new OrganizationFactory({}).create();
|
|
29
|
+
const user = await new UserFactory({ organization }).create();
|
|
30
|
+
const original = await SessionService.createSession(user);
|
|
31
|
+
const replacement = await SessionService.rotateSession(original);
|
|
32
|
+
|
|
33
|
+
const originalRequest = Request.buildJson('GET', '/v1/user', organization.getApiHost());
|
|
34
|
+
originalRequest.headers.authorization = 'Bearer ' + original.accessToken;
|
|
35
|
+
await testServer.test(endpoint, originalRequest);
|
|
36
|
+
|
|
37
|
+
const replacementRequest = Request.buildJson('GET', '/v1/user', organization.getApiHost());
|
|
38
|
+
replacementRequest.headers.authorization = 'Bearer ' + replacement.accessToken;
|
|
39
|
+
await testServer.test(endpoint, replacementRequest);
|
|
40
|
+
|
|
41
|
+
await expect(testServer.test(endpoint, originalRequest)).rejects.toThrow(STExpect.simpleError({
|
|
42
|
+
code: 'invalid_access_token',
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
|
|
26
46
|
test('Two-factor authentication is reported on the user and on the accounts of a member', async () => {
|
|
27
47
|
const organization = await new OrganizationFactory({}).create();
|
|
28
48
|
const user = await new UserFactory({ organization }).create();
|
|
29
49
|
await new MemberFactory({ organization, user }).create();
|
|
30
50
|
await MFATestHelper.addConfirmedTOTP(user);
|
|
31
51
|
|
|
32
|
-
const token = await
|
|
52
|
+
const token = await SessionService.createSession(user);
|
|
33
53
|
const r = Request.buildJson('GET', '/v1/user', organization.getApiHost());
|
|
34
54
|
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
35
55
|
|
|
@@ -44,7 +64,7 @@ describe('Endpoint.GetUser', () => {
|
|
|
44
64
|
test('A user without a second factor is not reported as having two-factor authentication', async () => {
|
|
45
65
|
const organization = await new OrganizationFactory({}).create();
|
|
46
66
|
const user = await new UserFactory({ organization }).create();
|
|
47
|
-
const token = await
|
|
67
|
+
const token = await SessionService.createSession(user);
|
|
48
68
|
|
|
49
69
|
const r = Request.buildJson('GET', '/v1/user', organization.getApiHost());
|
|
50
70
|
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
@@ -62,7 +82,7 @@ describe('Endpoint.GetUser', () => {
|
|
|
62
82
|
test('Request user details with invalid token is not working', async () => {
|
|
63
83
|
const organization = await new OrganizationFactory({}).create();
|
|
64
84
|
const user = await new UserFactory({ organization }).create();
|
|
65
|
-
const token = await
|
|
85
|
+
const token = await SessionService.createSession(user);
|
|
66
86
|
|
|
67
87
|
const r = Request.buildJson('GET', '/v1/user', organization.getApiHost());
|
|
68
88
|
r.headers.authorization = 'Bearer ' + token.accessToken + 'd';
|
|
@@ -73,7 +93,9 @@ describe('Endpoint.GetUser', () => {
|
|
|
73
93
|
test('Request user details with expired token is not working', async () => {
|
|
74
94
|
const organization = await new OrganizationFactory({}).create();
|
|
75
95
|
const user = await new UserFactory({ organization }).create();
|
|
76
|
-
const token = await
|
|
96
|
+
const token = await SessionService.createSession(user);
|
|
97
|
+
token.accessTokenValidUntil = new Date(Date.now() - 1000);
|
|
98
|
+
await token.save();
|
|
77
99
|
|
|
78
100
|
const r = Request.buildJson('GET', '/v1/user', organization.getApiHost());
|
|
79
101
|
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
@@ -84,7 +106,7 @@ describe('Endpoint.GetUser', () => {
|
|
|
84
106
|
test('Can request details of an API user by token without knowing the organization scope', async () => {
|
|
85
107
|
const organization = await new OrganizationFactory({}).create();
|
|
86
108
|
const user = await new UserFactory({ organization, apiUser: true }).create();
|
|
87
|
-
const token = await
|
|
109
|
+
const token = await SessionService.createSession(user);
|
|
88
110
|
|
|
89
111
|
const r = Request.buildJson('GET', '/v1/user');
|
|
90
112
|
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
@@ -98,13 +120,13 @@ describe('Endpoint.GetUser', () => {
|
|
|
98
120
|
test('Cannot request details of an normal user by token without knowing the organization scope', async () => {
|
|
99
121
|
const organization = await new OrganizationFactory({}).create();
|
|
100
122
|
const user = await new UserFactory({ organization }).create();
|
|
101
|
-
const token = await
|
|
123
|
+
const token = await SessionService.createSession(user);
|
|
102
124
|
|
|
103
125
|
const r = Request.buildJson('GET', '/v1/user');
|
|
104
126
|
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
105
127
|
|
|
106
128
|
await expect(testServer.test(endpoint, r)).rejects.toThrow(STExpect.simpleError({
|
|
107
|
-
code: 'invalid_access_token'
|
|
129
|
+
code: 'invalid_access_token',
|
|
108
130
|
}));
|
|
109
131
|
});
|
|
110
132
|
|
|
@@ -112,14 +134,13 @@ describe('Endpoint.GetUser', () => {
|
|
|
112
134
|
const wrongOrganization = await new OrganizationFactory({}).create();
|
|
113
135
|
const organization = await new OrganizationFactory({}).create();
|
|
114
136
|
const user = await new UserFactory({ organization, apiUser: true }).create();
|
|
115
|
-
const token = await
|
|
137
|
+
const token = await SessionService.createSession(user);
|
|
116
138
|
|
|
117
139
|
const r = Request.buildJson('GET', '/v1/user', wrongOrganization.getApiHost());
|
|
118
140
|
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
119
141
|
|
|
120
142
|
await expect(testServer.test(endpoint, r)).rejects.toThrow(STExpect.simpleError({
|
|
121
|
-
code: 'invalid_access_token'
|
|
143
|
+
code: 'invalid_access_token',
|
|
122
144
|
}));
|
|
123
145
|
});
|
|
124
|
-
|
|
125
146
|
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import type { SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
import { isSimpleError, isSimpleErrors } from '@simonbackx/simple-errors';
|
|
4
|
+
import type { Organization } from '@stamhoofd/models';
|
|
5
|
+
import { EmailVerificationCode, MFARecoveryCode, MFATOTP, MFAToken, OrganizationFactory, PasswordToken, Token, User, UserFactory, WebauthnCredential } from '@stamhoofd/models';
|
|
4
6
|
import { NewUser, PermissionLevel, Permissions, Token as TokenStruct } from '@stamhoofd/structures';
|
|
5
7
|
import { TestUtils } from '@stamhoofd/test-utils';
|
|
6
8
|
import crypto from 'crypto';
|
|
7
9
|
import { authenticator } from 'otplib';
|
|
10
|
+
import { SessionService } from '../../services/SessionService.js';
|
|
8
11
|
|
|
9
12
|
import { MFATestHelper } from '../../../tests/helpers/MFATestHelper.js';
|
|
10
13
|
import { testServer } from '../../../tests/helpers/TestServer.js';
|
|
@@ -49,8 +52,7 @@ function firstError(e: unknown): SimpleError {
|
|
|
49
52
|
async function captureError(promise: Promise<unknown>): Promise<SimpleError> {
|
|
50
53
|
try {
|
|
51
54
|
await promise;
|
|
52
|
-
}
|
|
53
|
-
catch (e) {
|
|
55
|
+
} catch (e) {
|
|
54
56
|
return firstError(e);
|
|
55
57
|
}
|
|
56
58
|
throw new Error('Expected the request to be rejected, but it succeeded');
|
|
@@ -79,7 +81,7 @@ function withSetup(request: Request, setupToken: string) {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
async function freshToken(user: User): Promise<Token> {
|
|
82
|
-
return await
|
|
84
|
+
return await SessionService.createSession(user, { authenticatedAt: new Date() });
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
async function addConfirmedTOTP(user: User): Promise<{ id: string; secret: string }> {
|
|
@@ -649,9 +651,9 @@ describe('MFA security', () => {
|
|
|
649
651
|
// separately and are not browser sessions.
|
|
650
652
|
const organization = await new OrganizationFactory({}).create();
|
|
651
653
|
const apiUser = await new UserFactory({ organization, apiUser: true }).create();
|
|
652
|
-
const apiToken = await
|
|
654
|
+
const apiToken = await SessionService.createSession(apiUser);
|
|
653
655
|
|
|
654
|
-
expect(await
|
|
656
|
+
expect(await SessionService.deleteOtherSessions({ userId: apiUser.id, keepAccessToken: null })).toBe(0);
|
|
655
657
|
expect(await Token.getByAccessToken(apiToken.accessToken, true)).toBeDefined();
|
|
656
658
|
});
|
|
657
659
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { Endpoint
|
|
2
|
-
import {
|
|
1
|
+
import type { Endpoint } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import type { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import { isSimpleError, isSimpleErrors } from '@simonbackx/simple-errors';
|
|
3
5
|
import { EmailMocker } from '@stamhoofd/email';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
+
import type { Organization } from '@stamhoofd/models';
|
|
7
|
+
import { AuditLog, EmailTemplateFactory, EmailVerificationCode, MFARecoveryCode, MFATOTP, MFAToken, PasswordToken, Platform, Token, User, UserFactory, OrganizationFactory, UserSession, WebauthnChallenge, WebauthnCredential } from '@stamhoofd/models';
|
|
8
|
+
import { AuditLogReplacementType, AuditLogType, EmailTemplateType, MFAMethodType, PermissionLevel, Permissions, SessionLoginMethod, Token as TokenStruct } from '@stamhoofd/structures';
|
|
6
9
|
import { authenticator } from 'otplib';
|
|
7
10
|
import crypto from 'crypto';
|
|
11
|
+
import { SessionService } from '../../services/SessionService.js';
|
|
8
12
|
|
|
9
13
|
import { MFATestHelper } from '../../../tests/helpers/MFATestHelper.js';
|
|
10
14
|
import { testServer } from '../../../tests/helpers/TestServer.js';
|
|
@@ -43,8 +47,7 @@ function firstError(e: unknown): SimpleError {
|
|
|
43
47
|
async function captureError(promise: Promise<unknown>): Promise<SimpleError> {
|
|
44
48
|
try {
|
|
45
49
|
await promise;
|
|
46
|
-
}
|
|
47
|
-
catch (e) {
|
|
50
|
+
} catch (e) {
|
|
48
51
|
return firstError(e);
|
|
49
52
|
}
|
|
50
53
|
throw new Error('Expected the request to throw');
|
|
@@ -106,12 +109,12 @@ function bearer(request: Request, token: Token) {
|
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
async function freshToken(user: User): Promise<Token> {
|
|
109
|
-
return await
|
|
112
|
+
return await SessionService.createSession(user, { authenticatedAt: new Date() });
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
async function staleToken(user: User): Promise<Token> {
|
|
113
116
|
// A token as produced by a refresh_token rotation (never authenticatedAt).
|
|
114
|
-
return await
|
|
117
|
+
return await SessionService.createSession(user);
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
async function requireMfa(organization: Organization | null, email: string, password: string): Promise<{ token: string; methods: string[]; webauthnAuthenticationOptions: unknown }> {
|
|
@@ -668,8 +671,7 @@ describe('MFA', () => {
|
|
|
668
671
|
const err = await captureError(testServer.test(tokenEndpoint, passwordLogin(organization, user.email, password)));
|
|
669
672
|
expect(err.code).toBe('require_email_confirmation');
|
|
670
673
|
expect(send).toHaveBeenCalledOnce();
|
|
671
|
-
}
|
|
672
|
-
finally {
|
|
674
|
+
} finally {
|
|
673
675
|
send.mockRestore();
|
|
674
676
|
}
|
|
675
677
|
});
|
|
@@ -690,7 +692,7 @@ describe('MFA', () => {
|
|
|
690
692
|
// The identity provider authenticated the user, so there is nothing to confirm.
|
|
691
693
|
const { organization, user } = await inactiveAdmin();
|
|
692
694
|
|
|
693
|
-
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod:
|
|
695
|
+
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod: SessionLoginMethod.SSO });
|
|
694
696
|
expect(requirement.type).toBe('setup');
|
|
695
697
|
});
|
|
696
698
|
});
|
|
@@ -1414,14 +1416,14 @@ describe('MFA', () => {
|
|
|
1414
1416
|
const { organization, user } = await adminOfOrgRequiringTwoFactor({ withPassword: false });
|
|
1415
1417
|
await addConfirmedTOTP(user);
|
|
1416
1418
|
|
|
1417
|
-
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod:
|
|
1419
|
+
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod: SessionLoginMethod.SSO });
|
|
1418
1420
|
expect(requirement.type).toBe('challenge');
|
|
1419
1421
|
});
|
|
1420
1422
|
|
|
1421
1423
|
test('an SSO-only account is not forced to enroll: the provider is the second factor', async () => {
|
|
1422
1424
|
const { organization, user } = await adminOfOrgRequiringTwoFactor({ withPassword: false });
|
|
1423
1425
|
|
|
1424
|
-
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod:
|
|
1426
|
+
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod: SessionLoginMethod.SSO });
|
|
1425
1427
|
expect(requirement.type).toBe('none');
|
|
1426
1428
|
});
|
|
1427
1429
|
|
|
@@ -1430,16 +1432,41 @@ describe('MFA', () => {
|
|
|
1430
1432
|
// requirement is not satisfied by signing in through SSO.
|
|
1431
1433
|
const { organization, user } = await adminOfOrgRequiringTwoFactor({ withPassword: true });
|
|
1432
1434
|
|
|
1433
|
-
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod:
|
|
1435
|
+
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod: SessionLoginMethod.SSO });
|
|
1434
1436
|
expect(requirement.type).toBe('setup');
|
|
1435
1437
|
});
|
|
1436
1438
|
|
|
1437
1439
|
test('a password login is always forced to enroll', async () => {
|
|
1438
1440
|
const { organization, user } = await adminOfOrgRequiringTwoFactor({ withPassword: true });
|
|
1439
1441
|
|
|
1440
|
-
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod:
|
|
1442
|
+
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod: SessionLoginMethod.Password });
|
|
1441
1443
|
expect(requirement.type).toBe('setup');
|
|
1442
1444
|
});
|
|
1445
|
+
|
|
1446
|
+
test('the session of an SSO login that passed a second factor is an SSO session', async () => {
|
|
1447
|
+
// The session is only created after the second factor, so the challenge has to
|
|
1448
|
+
// remember how the user got there: it decides how long the session may live.
|
|
1449
|
+
const { organization, user } = await adminOfOrgRequiringTwoFactor({ withPassword: false });
|
|
1450
|
+
const { secret } = await addConfirmedTOTP(user);
|
|
1451
|
+
|
|
1452
|
+
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod: SessionLoginMethod.SSO });
|
|
1453
|
+
if (requirement.type !== 'challenge') {
|
|
1454
|
+
throw new Error('Expected a challenge');
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const response = await testServer.test(tokenEndpoint, mfaGrant(organization, {
|
|
1458
|
+
mfa_token: requirement.challenge.token,
|
|
1459
|
+
method: 'TOTP',
|
|
1460
|
+
code: authenticator.generate(secret),
|
|
1461
|
+
}));
|
|
1462
|
+
if (!(response.body instanceof TokenStruct)) {
|
|
1463
|
+
throw new Error('Expected TokenStruct');
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const token = await Token.getByAccessToken(response.body.accessToken);
|
|
1467
|
+
expect((await UserSession.getByID(token!.sessionId))!.loginMethod).toBe(SessionLoginMethod.SSO);
|
|
1468
|
+
expect(token!.refreshTokenValidUntil.getTime()).toBeLessThanOrEqual(Date.now() + 3 * 60 * 60 * 1000);
|
|
1469
|
+
});
|
|
1443
1470
|
});
|
|
1444
1471
|
|
|
1445
1472
|
// -----------------------------------------------------------------------
|