@stamhoofd/backend 2.75.1 → 2.76.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 +10 -10
- package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +43 -0
- package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +6 -7
- package/src/endpoints/global/files/UploadFile.ts +1 -1
- package/src/helpers/MemberUserSyncer.ts +90 -86
- package/src/services/FileSignService.ts +1 -1
- package/src/services/SSOService.ts +114 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.76.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"@simonbackx/simple-encoding": "2.20.0",
|
|
38
38
|
"@simonbackx/simple-endpoints": "1.19.0",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.
|
|
42
|
-
"@stamhoofd/email": "2.
|
|
43
|
-
"@stamhoofd/models": "2.
|
|
44
|
-
"@stamhoofd/queues": "2.
|
|
45
|
-
"@stamhoofd/sql": "2.
|
|
46
|
-
"@stamhoofd/structures": "2.
|
|
47
|
-
"@stamhoofd/utility": "2.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.76.0",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.76.0",
|
|
42
|
+
"@stamhoofd/email": "2.76.0",
|
|
43
|
+
"@stamhoofd/models": "2.76.0",
|
|
44
|
+
"@stamhoofd/queues": "2.76.0",
|
|
45
|
+
"@stamhoofd/sql": "2.76.0",
|
|
46
|
+
"@stamhoofd/structures": "2.76.0",
|
|
47
|
+
"@stamhoofd/utility": "2.76.0",
|
|
48
48
|
"archiver": "^7.0.1",
|
|
49
49
|
"aws-sdk": "^2.885.0",
|
|
50
50
|
"axios": "1.6.8",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "85e0625ba777d83d92ad582443df1666955d51f6"
|
|
68
68
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AutoEncoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
|
|
4
|
+
import { Context } from '../../helpers/Context';
|
|
5
|
+
import { SSOService } from '../../services/SSOService';
|
|
6
|
+
import { OpenIDAuthTokenResponse } from '@stamhoofd/structures';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
type Query = undefined;
|
|
10
|
+
type Body = undefined;
|
|
11
|
+
type ResponseBody = OpenIDAuthTokenResponse;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This endpoint does nothing but build a URL to start the OpenID Connect flow.
|
|
15
|
+
* It is used to provide authenticateion data to the url that is temporarily valid (allows to connect an SSO provider to an existing account)
|
|
16
|
+
*/
|
|
17
|
+
export class OpenIDConnectAuthTokenEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
18
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
19
|
+
if (request.method !== 'POST') {
|
|
20
|
+
return [false];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const params = Endpoint.parseParameters(request.url, '/openid/auth-token', {});
|
|
24
|
+
|
|
25
|
+
if (params) {
|
|
26
|
+
return [true, params as Params];
|
|
27
|
+
}
|
|
28
|
+
return [false];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
32
|
+
// Check webshop and/or organization
|
|
33
|
+
await Context.setUserOrganizationScope();
|
|
34
|
+
await Context.authenticate({ allowWithoutAccount: false });
|
|
35
|
+
|
|
36
|
+
// Create a SSO auth token that can only be used once
|
|
37
|
+
const token = await SSOService.createToken();
|
|
38
|
+
|
|
39
|
+
return new Response(OpenIDAuthTokenResponse.create({
|
|
40
|
+
ssoAuthToken: token,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -6,15 +6,15 @@ import { Context } from '../../helpers/Context';
|
|
|
6
6
|
import { SSOService } from '../../services/SSOService';
|
|
7
7
|
|
|
8
8
|
type Params = Record<string, never>;
|
|
9
|
-
type Query =
|
|
10
|
-
type Body =
|
|
9
|
+
type Query = StartOpenIDFlowStruct;
|
|
10
|
+
type Body = undefined;
|
|
11
11
|
type ResponseBody = undefined;
|
|
12
12
|
|
|
13
13
|
export class OpenIDConnectStartEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
-
|
|
14
|
+
queryDecoder = StartOpenIDFlowStruct as Decoder<StartOpenIDFlowStruct>;
|
|
15
15
|
|
|
16
16
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
|
-
if (request.method !== '
|
|
17
|
+
if (request.method !== 'GET') {
|
|
18
18
|
return [false];
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -29,8 +29,7 @@ export class OpenIDConnectStartEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
29
29
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
30
|
// Check webshop and/or organization
|
|
31
31
|
await Context.setUserOrganizationScope();
|
|
32
|
-
await
|
|
33
|
-
|
|
34
|
-
return await service.validateAndStartAuthCodeFlow(request.body);
|
|
32
|
+
const service = await SSOService.fromContext(request.query.provider);
|
|
33
|
+
return await service.validateAndStartAuthCodeFlow(request.query);
|
|
35
34
|
}
|
|
36
35
|
}
|
|
@@ -186,7 +186,7 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
186
186
|
throw new SimpleError({
|
|
187
187
|
code: 'failed_to_sign',
|
|
188
188
|
message: 'Failed to sign file',
|
|
189
|
-
human: $t('
|
|
189
|
+
human: $t('509cdb4f-131a-42a6-b3b1-a63cca231e65'),
|
|
190
190
|
statusCode: 500,
|
|
191
191
|
});
|
|
192
192
|
}
|
|
@@ -5,6 +5,7 @@ import crypto from 'crypto';
|
|
|
5
5
|
import basex from 'base-x';
|
|
6
6
|
import { AuditLogService } from '../services/AuditLogService';
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
9
|
|
|
9
10
|
const ALPHABET = '123456789ABCDEFGHJKMNPQRSTUVWXYZ'; // Note: we removed 0, O, I and l to make it easier for humans
|
|
10
11
|
const customBase = basex(ALPHABET);
|
|
@@ -203,116 +204,119 @@ export class MemberUserSyncerStatic {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
async linkUser(email: string, member: MemberWithRegistrations, asParent: boolean, updateNameIfEqual = true) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
207
|
+
// This needs to happen in the queue to prevent creating duplicate users in the database
|
|
208
|
+
await QueueHandler.schedule('link-user/' + email, async () => {
|
|
209
|
+
console.log('Linking user', email, 'to member', member.id, 'as parent', asParent, 'update name if equal', updateNameIfEqual);
|
|
210
|
+
|
|
211
|
+
let user = member.users.find(u => u.email.toLocaleLowerCase() === email.toLocaleLowerCase()) ?? await User.getForAuthentication(member.organizationId, email, { allowWithoutAccount: true });
|
|
212
|
+
|
|
213
|
+
if (user) {
|
|
214
|
+
// console.log("Giving an existing user access to a member: " + user.id + ' - ' + member.id)
|
|
215
|
+
if (!asParent) {
|
|
216
|
+
if (user.memberId && user.memberId !== member.id) {
|
|
217
|
+
console.error('Found conflicting user with multiple members', user.id, 'members', user.memberId, 'to', member.id);
|
|
218
|
+
|
|
219
|
+
const otherMember = await Member.getWithRegistrations(user.memberId);
|
|
220
|
+
|
|
221
|
+
if (otherMember) {
|
|
222
|
+
if (otherMember.registrations.length > 0 && member.registrations.length === 0) {
|
|
223
|
+
// Choose the other member
|
|
224
|
+
// don't make changes
|
|
225
|
+
console.error('Resolved to current member - no changes made');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const responsibilities = await this.getResponsibilitiesForMembers([otherMember.id, member.id]);
|
|
230
|
+
const responsibilitiesOther = responsibilities.filter(r => r.memberId === otherMember.id);
|
|
231
|
+
const responsibilitiesCurrent = responsibilities.filter(r => r.memberId === member.id);
|
|
232
|
+
|
|
233
|
+
if (responsibilitiesOther.length >= responsibilitiesCurrent.length) {
|
|
234
|
+
console.error('Resolved to current member because of more responsibilities - no changes made');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
224
237
|
}
|
|
238
|
+
}
|
|
225
239
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (responsibilitiesOther.length >= responsibilitiesCurrent.length) {
|
|
231
|
-
console.error('Resolved to current member because of more responsibilities - no changes made');
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
240
|
+
if (updateNameIfEqual) {
|
|
241
|
+
user.firstName = member.details.firstName;
|
|
242
|
+
user.lastName = member.details.lastName;
|
|
234
243
|
}
|
|
244
|
+
user.memberId = member.id;
|
|
245
|
+
await this.updateInheritedPermissions(user);
|
|
235
246
|
}
|
|
247
|
+
else {
|
|
248
|
+
let shouldSave = false;
|
|
249
|
+
|
|
250
|
+
if (!user.firstName && !user.lastName) {
|
|
251
|
+
const parents = member.details.parents.filter(p => p.email === email);
|
|
252
|
+
if (parents.length === 1) {
|
|
253
|
+
if (updateNameIfEqual) {
|
|
254
|
+
user.firstName = parents[0].firstName;
|
|
255
|
+
user.lastName = parents[0].lastName;
|
|
256
|
+
}
|
|
257
|
+
shouldSave = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
236
260
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
261
|
+
if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
|
|
262
|
+
user.firstName = null;
|
|
263
|
+
user.lastName = null;
|
|
264
|
+
shouldSave = true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (user.memberId === member.id) {
|
|
268
|
+
// Unlink: parents are never 'equal' to the member
|
|
269
|
+
user.memberId = null;
|
|
270
|
+
await this.updateInheritedPermissions(user);
|
|
271
|
+
}
|
|
272
|
+
if (shouldSave) {
|
|
273
|
+
await user.save();
|
|
274
|
+
}
|
|
240
275
|
}
|
|
241
|
-
user.memberId = member.id;
|
|
242
|
-
await this.updateInheritedPermissions(user);
|
|
243
276
|
}
|
|
244
277
|
else {
|
|
245
|
-
|
|
278
|
+
// Create a new placeholder user
|
|
279
|
+
user = new User();
|
|
280
|
+
user.organizationId = member.organizationId;
|
|
281
|
+
user.email = email;
|
|
246
282
|
|
|
247
|
-
if (!
|
|
283
|
+
if (!asParent) {
|
|
284
|
+
if (updateNameIfEqual) {
|
|
285
|
+
user.firstName = member.details.firstName;
|
|
286
|
+
user.lastName = member.details.lastName;
|
|
287
|
+
}
|
|
288
|
+
user.memberId = member.id;
|
|
289
|
+
await this.updateInheritedPermissions(user);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
248
292
|
const parents = member.details.parents.filter(p => p.email === email);
|
|
249
293
|
if (parents.length === 1) {
|
|
250
294
|
if (updateNameIfEqual) {
|
|
251
295
|
user.firstName = parents[0].firstName;
|
|
252
296
|
user.lastName = parents[0].lastName;
|
|
253
297
|
}
|
|
254
|
-
shouldSave = true;
|
|
255
298
|
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
|
|
259
|
-
user.firstName = null;
|
|
260
|
-
user.lastName = null;
|
|
261
|
-
shouldSave = true;
|
|
262
|
-
}
|
|
263
299
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
await this.updateInheritedPermissions(user);
|
|
268
|
-
}
|
|
269
|
-
if (shouldSave) {
|
|
270
|
-
await user.save();
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
// Create a new placeholder user
|
|
276
|
-
user = new User();
|
|
277
|
-
user.organizationId = member.organizationId;
|
|
278
|
-
user.email = email;
|
|
279
|
-
|
|
280
|
-
if (!asParent) {
|
|
281
|
-
if (updateNameIfEqual) {
|
|
282
|
-
user.firstName = member.details.firstName;
|
|
283
|
-
user.lastName = member.details.lastName;
|
|
284
|
-
}
|
|
285
|
-
user.memberId = member.id;
|
|
286
|
-
await this.updateInheritedPermissions(user);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
const parents = member.details.parents.filter(p => p.email === email);
|
|
290
|
-
if (parents.length === 1) {
|
|
291
|
-
if (updateNameIfEqual) {
|
|
292
|
-
user.firstName = parents[0].firstName;
|
|
293
|
-
user.lastName = parents[0].lastName;
|
|
300
|
+
if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
|
|
301
|
+
user.firstName = null;
|
|
302
|
+
user.lastName = null;
|
|
294
303
|
}
|
|
295
|
-
}
|
|
296
304
|
|
|
297
|
-
|
|
298
|
-
user.firstName = null;
|
|
299
|
-
user.lastName = null;
|
|
305
|
+
await user.save();
|
|
300
306
|
}
|
|
301
307
|
|
|
302
|
-
|
|
308
|
+
console.log('Created new (placeholder) user that has access to a member: ' + user.id);
|
|
303
309
|
}
|
|
304
310
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (!member.users.find(u => u.id === user.id)) {
|
|
310
|
-
await Member.users.reverse('members').link(user, [member]);
|
|
311
|
-
member.users.push(user);
|
|
311
|
+
// Update model relation to correct response
|
|
312
|
+
if (!member.users.find(u => u.id === user.id)) {
|
|
313
|
+
await Member.users.reverse('members').link(user, [member]);
|
|
314
|
+
member.users.push(user);
|
|
312
315
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
// Update balance of this user, as it could have changed
|
|
317
|
+
await this.updateUserBalance(user.id, member.id);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
316
320
|
}
|
|
317
321
|
|
|
318
322
|
/**
|
|
@@ -175,7 +175,7 @@ export class FileSignService {
|
|
|
175
175
|
throw new SimpleError({
|
|
176
176
|
code: 'invalid_signature',
|
|
177
177
|
message: 'Invalid signature for file',
|
|
178
|
-
human: $t('
|
|
178
|
+
human: $t('479684ab-6c50-4ced-82d7-8245f4f475f4'),
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
return;
|
|
@@ -35,19 +35,71 @@ type SSOSessionContext = {
|
|
|
35
35
|
userId?: string | null;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
export class SSOAuthToken {
|
|
39
|
+
validUntil: Date;
|
|
40
|
+
token: string;
|
|
41
|
+
userId: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
export class SSOService {
|
|
39
45
|
provider: LoginProviderType;
|
|
40
46
|
platform: Platform;
|
|
41
47
|
organization: Organization | null;
|
|
42
|
-
user: User | null = null;
|
|
43
48
|
|
|
44
49
|
static sessionStorage = new Map<string, SSOSessionContext>();
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Maps auth token to user id + expiry information
|
|
53
|
+
*/
|
|
54
|
+
static authTokens = new Map<string, SSOAuthToken>();
|
|
55
|
+
|
|
56
|
+
constructor(data: { provider: LoginProviderType; platform: Platform; organization?: Organization | null }) {
|
|
47
57
|
this.provider = data.provider;
|
|
48
58
|
this.platform = data.platform;
|
|
49
59
|
this.organization = data.organization ?? null;
|
|
50
|
-
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static async clearExpiredTokensOrFromUser(userId: string | null = null) {
|
|
63
|
+
const d = new Date();
|
|
64
|
+
for (const [key, value] of this.authTokens) {
|
|
65
|
+
if (value.userId === userId || value.validUntil < d) {
|
|
66
|
+
this.authTokens.delete(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static async createToken() {
|
|
72
|
+
if (!Context.user) {
|
|
73
|
+
throw new SimpleError({
|
|
74
|
+
code: 'invalid_user',
|
|
75
|
+
message: 'Not signed in',
|
|
76
|
+
statusCode: 401,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const token = new SSOAuthToken();
|
|
81
|
+
token.validUntil = new Date(Date.now() + 1000 * 60 * 5);
|
|
82
|
+
token.token = (await randomBytes(192)).toString('base64').toUpperCase();
|
|
83
|
+
token.userId = Context.user.id;
|
|
84
|
+
|
|
85
|
+
await this.clearExpiredTokensOrFromUser(token.userId);
|
|
86
|
+
this.authTokens.set(token.token, token);
|
|
87
|
+
|
|
88
|
+
return token.token;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static async validateToken(token: string) {
|
|
92
|
+
const authToken = this.authTokens.get(token);
|
|
93
|
+
if (!authToken || authToken.validUntil < new Date()) {
|
|
94
|
+
this.authTokens.delete(token);
|
|
95
|
+
throw new SimpleError({
|
|
96
|
+
code: 'invalid_token',
|
|
97
|
+
message: 'Invalid token',
|
|
98
|
+
statusCode: 401,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
this.authTokens.delete(token);
|
|
102
|
+
return authToken;
|
|
51
103
|
}
|
|
52
104
|
|
|
53
105
|
/**
|
|
@@ -80,7 +132,7 @@ export class SSOService {
|
|
|
80
132
|
const organization = Context.organization;
|
|
81
133
|
const platform = await Platform.getShared();
|
|
82
134
|
|
|
83
|
-
const service = new SSOService({ provider, platform, organization
|
|
135
|
+
const service = new SSOService({ provider, platform, organization });
|
|
84
136
|
service.validateConfiguration();
|
|
85
137
|
|
|
86
138
|
return service;
|
|
@@ -144,12 +196,6 @@ export class SSOService {
|
|
|
144
196
|
const __ = this.loginConfiguration;
|
|
145
197
|
}
|
|
146
198
|
|
|
147
|
-
validateUser() {
|
|
148
|
-
if (this.user) {
|
|
149
|
-
this.validateEmail(this.user.email);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
199
|
validateEmail(email: string) {
|
|
154
200
|
// Validate configuration
|
|
155
201
|
const loginConfiguration = this.loginConfiguration;
|
|
@@ -309,28 +355,77 @@ export class SSOService {
|
|
|
309
355
|
});
|
|
310
356
|
}
|
|
311
357
|
|
|
312
|
-
|
|
358
|
+
let user: User | undefined = undefined;
|
|
359
|
+
|
|
360
|
+
if (data.authToken) {
|
|
361
|
+
const token = await SSOService.validateToken(data.authToken);
|
|
362
|
+
if (token) {
|
|
363
|
+
user = await User.getByID(token.userId);
|
|
364
|
+
|
|
365
|
+
if (!user) {
|
|
366
|
+
throw new SimpleError({
|
|
367
|
+
code: 'invalid_user',
|
|
368
|
+
message: 'User not found',
|
|
369
|
+
statusCode: 404,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this.validateEmail(user.email);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return await this.startAuthCodeFlow(redirectUri, data.spaState, data.prompt, user);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
validateRedirectUri(uri: string) {
|
|
381
|
+
let parsed: URL;
|
|
382
|
+
try {
|
|
383
|
+
parsed = new URL(uri);
|
|
384
|
+
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
throw new SimpleError({
|
|
387
|
+
code: 'invalid_redirect_uri',
|
|
388
|
+
message: 'Invalid redirect uri',
|
|
389
|
+
field: 'redirectUri',
|
|
390
|
+
statusCode: 400,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (parsed.protocol !== 'https:') {
|
|
395
|
+
throw new SimpleError({
|
|
396
|
+
code: 'invalid_redirect_uri',
|
|
397
|
+
message: 'Invalid redirect uri',
|
|
398
|
+
field: 'redirectUri',
|
|
399
|
+
statusCode: 400,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (parsed.host !== STAMHOOFD.domains.dashboard) {
|
|
404
|
+
throw new SimpleError({
|
|
405
|
+
code: 'invalid_redirect_uri',
|
|
406
|
+
message: 'Invalid redirect uri',
|
|
407
|
+
field: 'redirectUri',
|
|
408
|
+
statusCode: 400,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
313
411
|
}
|
|
314
412
|
|
|
315
|
-
async startAuthCodeFlow(redirectUri: string, spaState: string, prompt: string | null = null): Promise<Response<undefined>> {
|
|
413
|
+
async startAuthCodeFlow(redirectUri: string, spaState: string, prompt: string | null = null, user?: User): Promise<Response<undefined>> {
|
|
316
414
|
const code_verifier = generators.codeVerifier();
|
|
317
|
-
const state = generators.state();
|
|
415
|
+
const state = generators.state(); // this is the internal state backend <-> SSO provider
|
|
318
416
|
const nonce = generators.nonce();
|
|
319
417
|
const code_challenge = generators.codeChallenge(code_verifier);
|
|
320
418
|
const expires = new Date(Date.now() + 1000 * 60 * 15);
|
|
321
419
|
|
|
322
|
-
// Validate user id
|
|
323
|
-
this.validateUser();
|
|
324
|
-
|
|
325
420
|
const session: SSOSessionContext = {
|
|
326
421
|
expires,
|
|
327
422
|
code_verifier,
|
|
328
423
|
state,
|
|
329
424
|
nonce,
|
|
330
425
|
redirectUri,
|
|
331
|
-
spaState,
|
|
426
|
+
spaState, // this is the state frontend <-> backend (not backend <-> SSO provider)
|
|
332
427
|
providerType: this.provider,
|
|
333
|
-
userId:
|
|
428
|
+
userId: user?.id ?? null,
|
|
334
429
|
};
|
|
335
430
|
|
|
336
431
|
try {
|
|
@@ -355,7 +450,7 @@ export class SSOService {
|
|
|
355
450
|
state,
|
|
356
451
|
nonce,
|
|
357
452
|
prompt: prompt ?? undefined,
|
|
358
|
-
login_hint:
|
|
453
|
+
login_hint: user?.email ?? undefined,
|
|
359
454
|
redirect_uri: this.externalRedirectUri,
|
|
360
455
|
|
|
361
456
|
// Google has this instead of the offline_access scope
|