@stamhoofd/backend 2.70.0 → 2.72.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/audit-logs/OrganizationLogger.ts +1 -1
- package/src/audit-logs/PlatformLogger.ts +1 -0
- package/src/endpoints/auth/CreateTokenEndpoint.ts +11 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +26 -2
- package/src/endpoints/auth/OpenIDConnectCallbackEndpoint.ts +4 -25
- package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +9 -75
- package/src/endpoints/auth/PatchUserEndpoint.ts +67 -3
- package/src/endpoints/auth/SignupEndpoint.ts +1 -1
- package/src/endpoints/global/members/GetMembersEndpoint.ts +5 -3
- package/src/endpoints/global/sso/GetSSOEndpoint.ts +18 -7
- package/src/endpoints/global/sso/SetSSOEndpoint.ts +9 -24
- package/src/helpers/AdminPermissionChecker.ts +4 -3
- package/src/helpers/AuthenticatedStructures.ts +2 -0
- package/src/helpers/Context.ts +23 -5
- package/src/helpers/CookieHelper.ts +9 -4
- package/src/services/SSOService.ts +569 -0
- package/src/helpers/OpenIDConnectHelper.ts +0 -295
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { DecodedRequest, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
import { Organization, Platform, Token, User, Webshop } from '@stamhoofd/models';
|
|
4
|
+
import { LoginMethod, LoginProviderType, OpenIDClientConfiguration, StartOpenIDFlowStruct, Token as TokenStruct } from '@stamhoofd/structures';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import { generators, Issuer } from 'openid-client';
|
|
7
|
+
import { Context } from '../helpers/Context';
|
|
8
|
+
|
|
9
|
+
import { CookieHelper, ObjectWithHeaders } from '../helpers/CookieHelper';
|
|
10
|
+
|
|
11
|
+
async function randomBytes(size: number): Promise<Buffer> {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
crypto.randomBytes(size, (err: Error | null, buf: Buffer) => {
|
|
14
|
+
if (err) {
|
|
15
|
+
reject(err);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
resolve(buf);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type SSOSessionContext = {
|
|
24
|
+
expires: Date;
|
|
25
|
+
code_verifier: string;
|
|
26
|
+
state: string;
|
|
27
|
+
nonce: string;
|
|
28
|
+
redirectUri: string;
|
|
29
|
+
spaState: string;
|
|
30
|
+
providerType: LoginProviderType;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Link this method to this existing user and don't create a new token
|
|
34
|
+
*/
|
|
35
|
+
userId?: string | null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export class SSOService {
|
|
39
|
+
provider: LoginProviderType;
|
|
40
|
+
platform: Platform;
|
|
41
|
+
organization: Organization | null;
|
|
42
|
+
user: User | null = null;
|
|
43
|
+
|
|
44
|
+
static sessionStorage = new Map<string, SSOSessionContext>();
|
|
45
|
+
|
|
46
|
+
constructor(data: { provider: LoginProviderType; platform: Platform; organization?: Organization | null; user?: User | null }) {
|
|
47
|
+
this.provider = data.provider;
|
|
48
|
+
this.platform = data.platform;
|
|
49
|
+
this.organization = data.organization ?? null;
|
|
50
|
+
this.user = data.user ?? null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This is the redirectUri we'll use towards the provider - but we store a different internal redirectUri in the session to allow more flexibility
|
|
55
|
+
* with the multiple domains we can redirect to.
|
|
56
|
+
*/
|
|
57
|
+
get externalRedirectUri() {
|
|
58
|
+
if (this.configuration.redirectUri) {
|
|
59
|
+
return this.configuration.redirectUri;
|
|
60
|
+
}
|
|
61
|
+
// todo: we might need a special url for the app here
|
|
62
|
+
|
|
63
|
+
if (!this.organization) {
|
|
64
|
+
return 'https://' + STAMHOOFD.domains.api + '/openid/callback';
|
|
65
|
+
}
|
|
66
|
+
return 'https://' + this.organization.id + '.' + STAMHOOFD.domains.api + '/openid/callback';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get defaultRedirectUri() {
|
|
70
|
+
// Host should match correctly
|
|
71
|
+
let redirectUri = 'https://' + STAMHOOFD.domains.dashboard;
|
|
72
|
+
|
|
73
|
+
if (this.organization) {
|
|
74
|
+
redirectUri = 'https://' + this.organization.getHost();
|
|
75
|
+
}
|
|
76
|
+
return redirectUri;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static async fromContext(provider: LoginProviderType) {
|
|
80
|
+
const organization = Context.organization;
|
|
81
|
+
const platform = await Platform.getShared();
|
|
82
|
+
|
|
83
|
+
const service = new SSOService({ provider, platform, organization, user: Context.user });
|
|
84
|
+
service.validate();
|
|
85
|
+
|
|
86
|
+
return service;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get configuration() {
|
|
90
|
+
let configuration: OpenIDClientConfiguration | null = null;
|
|
91
|
+
|
|
92
|
+
if (this.provider === LoginProviderType.SSO) {
|
|
93
|
+
if (this.organization) {
|
|
94
|
+
configuration = this.organization.serverMeta.ssoConfiguration ?? OpenIDClientConfiguration.create({});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
configuration = this.platform.serverConfig.ssoConfiguration ?? OpenIDClientConfiguration.create({});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (this.provider === LoginProviderType.Google) {
|
|
101
|
+
if (this.organization) {
|
|
102
|
+
// Not supported yet
|
|
103
|
+
configuration = null;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
configuration = this.platform.serverConfig.googleConfiguration ?? OpenIDClientConfiguration.create({});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!configuration) {
|
|
111
|
+
throw new SimpleError({
|
|
112
|
+
code: 'invalid_client',
|
|
113
|
+
message: 'SSO not configured',
|
|
114
|
+
statusCode: 400,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return configuration;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get loginConfiguration() {
|
|
121
|
+
if (this.organization) {
|
|
122
|
+
throw new SimpleError({
|
|
123
|
+
code: 'invalid_client',
|
|
124
|
+
message: 'Login configuration not yet supported for organization users',
|
|
125
|
+
statusCode: 400,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const loginConfiguration = this.platform.config.loginMethods.get(this.provider as unknown as LoginMethod);
|
|
130
|
+
if (!loginConfiguration) {
|
|
131
|
+
throw new SimpleError({
|
|
132
|
+
code: 'invalid_client',
|
|
133
|
+
message: 'SSO not configured (correctly)',
|
|
134
|
+
statusCode: 400,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return loginConfiguration;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
validate() {
|
|
142
|
+
// Validate configuration exists
|
|
143
|
+
const _ = this.configuration;
|
|
144
|
+
const __ = this.loginConfiguration;
|
|
145
|
+
|
|
146
|
+
if (this.user) {
|
|
147
|
+
this.validateEmail(this.user.email);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
validateEmail(email: string) {
|
|
152
|
+
// Validate configuration
|
|
153
|
+
const loginConfiguration = this.loginConfiguration;
|
|
154
|
+
|
|
155
|
+
if (!loginConfiguration.isEnabledForEmail(email)) {
|
|
156
|
+
throw new SimpleError({
|
|
157
|
+
code: 'invalid_user',
|
|
158
|
+
message: 'User not allowed to use this login method',
|
|
159
|
+
human: 'Je kan deze inlogmethode niet gebruiken',
|
|
160
|
+
statusCode: 400,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async setConfiguration(configuration: OpenIDClientConfiguration) {
|
|
166
|
+
if (this.provider === LoginProviderType.SSO) {
|
|
167
|
+
if (this.organization) {
|
|
168
|
+
this.organization.serverMeta.ssoConfiguration = configuration;
|
|
169
|
+
await this.getClient();
|
|
170
|
+
await this.organization.save();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
this.platform.serverConfig.ssoConfiguration = configuration;
|
|
175
|
+
await this.getClient();
|
|
176
|
+
await this.platform.save();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (this.provider === LoginProviderType.Google) {
|
|
181
|
+
if (!this.organization) {
|
|
182
|
+
this.platform.serverConfig.googleConfiguration = configuration;
|
|
183
|
+
await this.getClient();
|
|
184
|
+
await this.platform.save();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
throw new SimpleError({
|
|
190
|
+
code: 'invalid_client',
|
|
191
|
+
message: 'SSO not supported here',
|
|
192
|
+
statusCode: 400,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async getClient() {
|
|
197
|
+
const issuer = await Issuer.discover(this.configuration.issuer);
|
|
198
|
+
const client = new issuer.Client({
|
|
199
|
+
client_id: this.configuration.clientId,
|
|
200
|
+
client_secret: this.configuration.clientSecret,
|
|
201
|
+
redirect_uris: [this.externalRedirectUri],
|
|
202
|
+
response_types: ['code'],
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Todo: in the future we can add a cache here
|
|
206
|
+
|
|
207
|
+
return client;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static async storeSession(response: Response<any>, data: SSOSessionContext) {
|
|
211
|
+
const sessionId = (await randomBytes(192)).toString('base64');
|
|
212
|
+
|
|
213
|
+
// Delete expired sessions
|
|
214
|
+
for (const [key, value] of this.sessionStorage) {
|
|
215
|
+
if (value.expires < new Date()) {
|
|
216
|
+
this.sessionStorage.delete(key);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.sessionStorage.set(sessionId, data);
|
|
221
|
+
|
|
222
|
+
// Store
|
|
223
|
+
CookieHelper.setCookie(response, 'oid_session_id', sessionId, {
|
|
224
|
+
httpOnly: true,
|
|
225
|
+
secure: STAMHOOFD.environment !== 'development',
|
|
226
|
+
expires: data.expires,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
static getSession(request: ObjectWithHeaders): SSOSessionContext | null {
|
|
231
|
+
const sessionId = CookieHelper.getCookie(request, 'oid_session_id');
|
|
232
|
+
if (!sessionId) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const session = this.sessionStorage.get(sessionId);
|
|
237
|
+
if (!session) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (session.expires < new Date()) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return session;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async validateAndStartAuthCodeFlow(data: StartOpenIDFlowStruct) {
|
|
249
|
+
// Host should match correctly
|
|
250
|
+
let redirectUri = this.defaultRedirectUri;
|
|
251
|
+
|
|
252
|
+
// todo: also support the app as redirect uri using app schemes (could be required for mobile apps)
|
|
253
|
+
const webshopId = data.webshopId;
|
|
254
|
+
|
|
255
|
+
if (webshopId) {
|
|
256
|
+
if (!this.organization) {
|
|
257
|
+
throw new SimpleError({
|
|
258
|
+
code: 'invalid_organization',
|
|
259
|
+
message: 'Organization required when specifying webshopId',
|
|
260
|
+
statusCode: 400,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const webshop = await Webshop.getByID(webshopId);
|
|
265
|
+
if (!webshop || webshop.organizationId !== this.organization.id) {
|
|
266
|
+
throw new SimpleError({
|
|
267
|
+
code: 'invalid_webshop',
|
|
268
|
+
message: 'Invalid webshop',
|
|
269
|
+
statusCode: 400,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
redirectUri = 'https://' + webshop.setRelation(Webshop.organization, this.organization).getHost();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (data.redirectUri) {
|
|
276
|
+
try {
|
|
277
|
+
const allowedHost = new URL(redirectUri);
|
|
278
|
+
const givenUrl = new URL(data.redirectUri);
|
|
279
|
+
|
|
280
|
+
if (allowedHost.host === givenUrl.host && givenUrl.protocol === 'https:') {
|
|
281
|
+
redirectUri = givenUrl.href;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
throw new SimpleError({
|
|
285
|
+
code: 'redirect_uri_not_allowed',
|
|
286
|
+
message: 'Redirect uri not allowed',
|
|
287
|
+
field: 'redirectUri',
|
|
288
|
+
statusCode: 400,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
throw new SimpleError({
|
|
294
|
+
code: 'invalid_redirect_uri',
|
|
295
|
+
message: 'Invalid redirect uri',
|
|
296
|
+
field: 'redirectUri',
|
|
297
|
+
statusCode: 400,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (data.spaState.length < 10) {
|
|
303
|
+
throw new SimpleError({
|
|
304
|
+
code: 'invalid_state',
|
|
305
|
+
message: 'Invalid state',
|
|
306
|
+
statusCode: 400,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return await this.startAuthCodeFlow(redirectUri, data.spaState, data.prompt);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async startAuthCodeFlow(redirectUri: string, spaState: string, prompt: string | null = null): Promise<Response<undefined>> {
|
|
314
|
+
const code_verifier = generators.codeVerifier();
|
|
315
|
+
const state = generators.state();
|
|
316
|
+
const nonce = generators.nonce();
|
|
317
|
+
const code_challenge = generators.codeChallenge(code_verifier);
|
|
318
|
+
const expires = new Date(Date.now() + 1000 * 60 * 15);
|
|
319
|
+
|
|
320
|
+
const session: SSOSessionContext = {
|
|
321
|
+
expires,
|
|
322
|
+
code_verifier,
|
|
323
|
+
state,
|
|
324
|
+
nonce,
|
|
325
|
+
redirectUri,
|
|
326
|
+
spaState,
|
|
327
|
+
providerType: this.provider,
|
|
328
|
+
userId: Context.user?.id ?? null,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const response = new Response(undefined);
|
|
333
|
+
|
|
334
|
+
const client = await this.getClient();
|
|
335
|
+
await SSOService.storeSession(response, session);
|
|
336
|
+
|
|
337
|
+
const scopes = ['openid', 'email', 'profile'];
|
|
338
|
+
|
|
339
|
+
if (this.provider === LoginProviderType.SSO) {
|
|
340
|
+
// Google doesn't support this scope
|
|
341
|
+
scopes.push('offline_access');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const redirect = client.authorizationUrl({
|
|
345
|
+
scope: scopes.join(' '),
|
|
346
|
+
code_challenge,
|
|
347
|
+
code_challenge_method: 'S256',
|
|
348
|
+
response_mode: 'form_post',
|
|
349
|
+
response_type: 'code',
|
|
350
|
+
state,
|
|
351
|
+
nonce,
|
|
352
|
+
prompt: prompt ?? undefined,
|
|
353
|
+
login_hint: this.user?.email ?? undefined,
|
|
354
|
+
redirect_uri: this.externalRedirectUri,
|
|
355
|
+
|
|
356
|
+
// Google has this instead of the offline_access scope
|
|
357
|
+
access_type: this.provider === LoginProviderType.Google ? 'offline' : undefined,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
response.headers['location'] = redirect;
|
|
361
|
+
response.status = 302;
|
|
362
|
+
|
|
363
|
+
return response;
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
const message = (isSimpleError(e) || isSimpleErrors(e) ? e.getHuman() : 'Er ging iets mis.');
|
|
367
|
+
console.error('Error in openID callback', e);
|
|
368
|
+
return SSOServiceWithSession.getErrorRedirectResponse(session, message);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export class SSOServiceWithSession {
|
|
374
|
+
session: SSOSessionContext;
|
|
375
|
+
service: SSOService;
|
|
376
|
+
request: DecodedRequest<any, any, any>;
|
|
377
|
+
|
|
378
|
+
constructor(session: SSOSessionContext, service: SSOService, request: DecodedRequest<any, any, any>) {
|
|
379
|
+
this.session = session;
|
|
380
|
+
this.service = service;
|
|
381
|
+
this.request = request;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
static async fromSession(request: DecodedRequest<any, any, any>): Promise<SSOServiceWithSession> {
|
|
385
|
+
const session = SSOService.getSession(request);
|
|
386
|
+
|
|
387
|
+
if (!session) {
|
|
388
|
+
throw new Error('Missing session');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const service = await SSOService.fromContext(session.providerType);
|
|
392
|
+
|
|
393
|
+
return new SSOServiceWithSession(session, service, request);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async callback(): Promise<Response<undefined>> {
|
|
397
|
+
const session = SSOService.getSession(Context.request);
|
|
398
|
+
|
|
399
|
+
if (!session) {
|
|
400
|
+
throw new Error('Missing session');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const response = new Response(undefined);
|
|
405
|
+
const client = await this.service.getClient();
|
|
406
|
+
|
|
407
|
+
const tokenSet = await client.callback(this.service.externalRedirectUri, this.request.body as Record<string, unknown>, {
|
|
408
|
+
code_verifier: session.code_verifier,
|
|
409
|
+
state: session.state,
|
|
410
|
+
nonce: session.nonce,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
console.log('received and validated tokens %j', tokenSet);
|
|
414
|
+
|
|
415
|
+
const claims = tokenSet.claims();
|
|
416
|
+
console.log('validated ID Token claims %j', claims);
|
|
417
|
+
|
|
418
|
+
if (!claims.name) {
|
|
419
|
+
throw new SimpleError({
|
|
420
|
+
code: 'invalid_user',
|
|
421
|
+
message: 'Missing name',
|
|
422
|
+
statusCode: 400,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let firstName = claims.name.split(' ')[0];
|
|
427
|
+
let lastName = claims.name.split(' ').slice(1).join(' ');
|
|
428
|
+
|
|
429
|
+
// Get from API
|
|
430
|
+
if (tokenSet.access_token) {
|
|
431
|
+
const userinfo = await client.userinfo(tokenSet.access_token);
|
|
432
|
+
console.log('userinfo', userinfo);
|
|
433
|
+
|
|
434
|
+
if (userinfo.given_name) {
|
|
435
|
+
console.log('userinfo given_name', userinfo.given_name);
|
|
436
|
+
firstName = userinfo.given_name;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (userinfo.family_name) {
|
|
440
|
+
console.log('userinfo family_name', userinfo.family_name);
|
|
441
|
+
lastName = userinfo.family_name;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (tokenSet.refresh_token) {
|
|
446
|
+
console.log('OK. Refresh token received!');
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
console.log('No refresh token');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!claims.email) {
|
|
453
|
+
throw new SimpleError({
|
|
454
|
+
code: 'invalid_user',
|
|
455
|
+
message: 'Missing email address',
|
|
456
|
+
statusCode: 400,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (!claims.sub) {
|
|
461
|
+
throw new SimpleError({
|
|
462
|
+
code: 'invalid_user',
|
|
463
|
+
message: 'Missing sub',
|
|
464
|
+
statusCode: 400,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this.service.validateEmail(claims.email);
|
|
469
|
+
|
|
470
|
+
// Get user from database
|
|
471
|
+
let user = await User.getForRegister(this.service.organization?.id ?? null, claims.email);
|
|
472
|
+
if (!user) {
|
|
473
|
+
if (this.session.userId) {
|
|
474
|
+
throw new SimpleError({
|
|
475
|
+
code: 'invalid_user',
|
|
476
|
+
message: 'User not found: please log in with the same email address as the user you are trying to link',
|
|
477
|
+
human: 'Je moet inloggen met hetzelfde e-mailadres als het account dat je probeert te koppelen',
|
|
478
|
+
statusCode: 404,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Create a new user
|
|
483
|
+
user = await User.registerSSO(this.service.organization, {
|
|
484
|
+
id: undefined,
|
|
485
|
+
email: claims.email,
|
|
486
|
+
firstName,
|
|
487
|
+
lastName,
|
|
488
|
+
type: session.providerType,
|
|
489
|
+
sub: claims.sub,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (!user) {
|
|
493
|
+
throw new SimpleError({
|
|
494
|
+
code: 'invalid_user',
|
|
495
|
+
message: 'Failed to create user',
|
|
496
|
+
statusCode: 500,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
if (this.session.userId && user.id !== this.session.userId) {
|
|
502
|
+
throw new SimpleError({
|
|
503
|
+
code: 'invalid_user',
|
|
504
|
+
message: 'User or email mismatch',
|
|
505
|
+
statusCode: 400,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Update name
|
|
510
|
+
if (!user.firstName || !user.hasPasswordBasedAccount()) {
|
|
511
|
+
user.firstName = firstName;
|
|
512
|
+
}
|
|
513
|
+
if (!user.lastName || !user.hasPasswordBasedAccount()) {
|
|
514
|
+
user.lastName = lastName;
|
|
515
|
+
}
|
|
516
|
+
user.linkLoginProvider(this.service.provider, claims.sub, !!this.session.userId);
|
|
517
|
+
await user.save();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Redirect back
|
|
521
|
+
const redirectUri = new URL(session.redirectUri);
|
|
522
|
+
|
|
523
|
+
if (!this.session.userId) {
|
|
524
|
+
const token = await Token.createExpiredToken(user);
|
|
525
|
+
|
|
526
|
+
if (!token) {
|
|
527
|
+
throw new SimpleError({
|
|
528
|
+
code: 'error',
|
|
529
|
+
message: 'Could not generate token',
|
|
530
|
+
human: 'Er ging iets mis bij het aanmelden',
|
|
531
|
+
statusCode: 500,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const st = new TokenStruct(token);
|
|
536
|
+
redirectUri.searchParams.set('oid_rt', st.refreshToken);
|
|
537
|
+
redirectUri.searchParams.set('s', session.spaState);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
redirectUri.searchParams.set('s', session.spaState);
|
|
541
|
+
redirectUri.searchParams.set('msg', 'Je account is succesvol gekoppeld');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
response.headers['location'] = redirectUri.toString();
|
|
545
|
+
response.status = 302;
|
|
546
|
+
|
|
547
|
+
return response;
|
|
548
|
+
}
|
|
549
|
+
catch (e) {
|
|
550
|
+
const message = (isSimpleError(e) || isSimpleErrors(e) ? e.getHuman() : 'Er ging iets mis.');
|
|
551
|
+
console.error('Error in openID callback', e);
|
|
552
|
+
return SSOServiceWithSession.getErrorRedirectResponse(session, message);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
static getErrorRedirectResponse(session: SSOSessionContext, errorMessage: string) {
|
|
557
|
+
const response = new Response(undefined);
|
|
558
|
+
|
|
559
|
+
// Redirect back to webshop
|
|
560
|
+
const redirectUri = new URL(session.redirectUri);
|
|
561
|
+
redirectUri.searchParams.set('s', session.spaState);
|
|
562
|
+
redirectUri.searchParams.set('error', errorMessage);
|
|
563
|
+
|
|
564
|
+
response.headers['location'] = redirectUri.toString();
|
|
565
|
+
response.status = 302;
|
|
566
|
+
|
|
567
|
+
return response;
|
|
568
|
+
}
|
|
569
|
+
}
|