• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GEWIS / sudosos-backend / 27283082186

10 Jun 2026 02:13PM UTC coverage: 91.996% (+0.04%) from 91.956%
27283082186

push

github

web-flow
chore(deps): bump bullmq from 5.77.6 to 5.78.0 (#949)

Bumps [bullmq](https://github.com/taskforcesh/bullmq) from 5.77.6 to 5.78.0.
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v5.77.6...v5.78.0)

---
updated-dependencies:
- dependency-name: bullmq
  dependency-version: 5.78.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

4199 of 4800 branches covered (87.48%)

Branch coverage included in aggregate %.

21593 of 23236 relevant lines covered (92.93%)

847.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.83
/src/controller/authentication-controller.ts
1
/**
1✔
2
 *  SudoSOS back-end API service.
3
 *  Copyright (C) 2026 Study association GEWIS
4
 *
5
 *  This program is free software: you can redistribute it and/or modify
6
 *  it under the terms of the GNU Affero General Public License as published
7
 *  by the Free Software Foundation, either version 3 of the License, or
8
 *  (at your option) any later version.
9
 *
10
 *  This program is distributed in the hope that it will be useful,
11
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 *  GNU Affero General Public License for more details.
14
 *
15
 *  You should have received a copy of the GNU Affero General Public License
16
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
 *
18
 *  @license
19
 */
1✔
20

21
/**
1✔
22
 * This is the module page of authentication-controller.
23
 *
24
 * @module internal/controllers
25
 */
1✔
26

27
import { Request, Response } from 'express';
28
import log4js, { Logger } from 'log4js';
29
import BaseController, { BaseControllerOptions } from './base-controller';
30
import Policy from './policy';
31
import User, { UserType } from '../entity/user/user';
32
import AuthenticationMockRequest from './request/authentication-mock-request';
33
import TokenHandler from '../authentication/token-handler';
34
import AuthenticationService, { AuthenticationContext } from '../service/authentication-service';
35
import AuthenticationLDAPRequest from './request/authentication-ldap-request';
36
import RoleManager from '../rbac/role-manager';
37
import { LDAPUser } from '../helpers/ad';
38
import AuthenticationLocalRequest from './request/authentication-local-request';
39
import PinAuthenticator from '../entity/authenticator/pin-authenticator';
40
import LocalAuthenticator from '../entity/authenticator/local-authenticator';
41
import ResetLocalRequest from './request/reset-local-request';
42
import AuthenticationResetTokenRequest from './request/authentication-reset-token-request';
43
import AuthenticationKeyRequest from './request/authentication-key-request';
44
import KeyAuthenticator from '../entity/authenticator/key-authenticator';
45
import { AppDataSource } from '../database/database';
46
import { NotificationTypes } from '../notifications/notification-types';
47
import Notifier, { WelcomeWithResetOptions } from '../notifications';
48
import UserService from '../service/user-service';
49
import Config from '../config';
50

51
/**
1✔
52
 * The authentication controller is responsible for verifying user authentications and handing out json web tokens.
53
 */
1✔
54
export default class AuthenticationController extends BaseController {
1✔
55
  /**
1✔
56
   * Reference to the logger instance.
57
   */
1✔
58
  private logger: Logger = log4js.getLogger('AuthenticationController');
1✔
59

60
  /**
61
   * Reference to the token handler of the application.
62
   */
63
  protected tokenHandler: TokenHandler;
64

65
  /**
1✔
66
   * Creates a new authentication controller instance.
67
   * @param options - The options passed to the base controller.
68
   * @param tokenHandler - The token handler for creating signed tokens.
69
   */
1✔
70
  public constructor(
1✔
71
    options: BaseControllerOptions,
3✔
72
    tokenHandler: TokenHandler,
3✔
73
  ) {
3✔
74
    super(options);
3✔
75
    this.configureLogger(this.logger);
3✔
76
    this.tokenHandler = tokenHandler;
3✔
77
  }
3✔
78

79
  /**
1✔
80
   * @inheritdoc
81
   */
1✔
82
  public getPolicy(): Policy {
1✔
83
    return {
3✔
84
      '/public': {
3✔
85
        GET: {
3✔
86
          policy: async () => true,
3✔
87
          handler: this.getJWTPublicKey.bind(this),
3✔
88
        },
3✔
89
      },
3✔
90
      '/mock': {
3✔
91
        POST: {
3✔
92
          body: { modelName: 'AuthenticationMockRequest' },
3✔
93
          policy: AuthenticationController.canPerformMock.bind(this),
3✔
94
          handler: this.mockLogin.bind(this),
3✔
95
        },
3✔
96
      },
3✔
97
      '/LDAP': {
3✔
98
        POST: {
3✔
99
          body: { modelName: 'AuthenticationLDAPRequest' },
3✔
100
          policy: async () => true,
3✔
101
          handler: this.LDAPLogin.bind(this),
3✔
102
        },
3✔
103
      },
3✔
104
      '/key': {
3✔
105
        POST: {
3✔
106
          body: { modelName: 'AuthenticationKeyRequest' },
3✔
107
          policy: async () => true,
3✔
108
          handler: this.keyLogin.bind(this),
3✔
109
        },
3✔
110
      },
3✔
111
      '/local': {
3✔
112
        POST: {
3✔
113
          body: { modelName: 'AuthenticationLocalRequest' },
3✔
114
          policy: async () => true,
3✔
115
          handler: this.LocalLogin.bind(this),
3✔
116
          restrictions: { availableDuringMaintenance: true },
3✔
117
        },
3✔
118
        PUT: {
3✔
119
          body: { modelName: 'AuthenticationResetTokenRequest' },
3✔
120
          policy: async () => true,
3✔
121
          handler: this.resetLocalUsingToken.bind(this),
3✔
122
        },
3✔
123
      },
3✔
124
      '/local/reset': {
3✔
125
        POST: {
3✔
126
          body: { modelName: 'ResetLocalRequest' },
3✔
127
          policy: async () => true,
3✔
128
          handler: this.createResetToken.bind(this),
3✔
129
        },
3✔
130
      },
3✔
131
    };
3✔
132
  }
3✔
133

134
  /**
1✔
135
   * Validates that the request is authorized by the policy.
136
   * @param req - The incoming request.
137
   */
1✔
138
  static async canPerformMock(req: Request): Promise<boolean> {
1✔
139
    const body = req.body as AuthenticationMockRequest;
6✔
140

141
    // Only allow in development setups
6✔
142
    if (!Config.get().app.isDevelopment && !Config.get().app.isTest) return false;
6✔
143

144
    // Check the existence of the user
5✔
145
    const user = await User.findOne(UserService.getOptions({ id: body.userId }));
5✔
146
    if (!user) return false;
6✔
147

148
    return true;
4✔
149
  }
4✔
150

151
  /**
1✔
152
   * GET /authentication/public
153
   * @summary Get the JWT public key used by SudoSOS
154
   * @operationId getJWTPublicKey
155
   * @tags authenticate - Operations of authentication controller
156
   * @returns {string} 200 - Public key
157
   */
1✔
158
  public async getJWTPublicKey(req: Request, res: Response): Promise<void> {
1✔
159
    this.logger.trace('Get JWT public key by IP', req.ip);
1✔
160

161
    try {
1✔
162
      const publicKey = this.tokenHandler.getOptions().publicKey;
1✔
163
      res.json(publicKey);
1✔
164
    } catch (error) {
1!
165
      this.logger.error('Could not get JWT public key:', error);
×
166
      res.status(500).json('Internal server error.');
×
167
    }
×
168
  }
1✔
169

170
  /**
1✔
171
   * Construct a login function for PIN.
172
   * This was done such that it is easily adaptable.
173
   * @param roleManager
174
   * @param tokenHandler
175
   * @param pin - Provided PIN code
176
   * @param userId - Provided User
177
   * @param posId - Optional POS identifier (only used by secure endpoints)
178
   */
1✔
179
  public static PINLoginConstructor(roleManager: RoleManager, tokenHandler: TokenHandler,
1✔
180
    pin: string, userId: number, posId?: number) {
9✔
181
    return async (req: Request, res: Response) => {
9✔
182
      const user = await User.findOne(UserService.getOptions({ id: userId }));
9✔
183

184
      if (!user) {
9✔
185
        res.status(403).json({
1✔
186
          message: 'Invalid credentials.',
1✔
187
        });
1✔
188
        return;
1✔
189
      }
1✔
190

191
      const pinAuthenticator = await PinAuthenticator.findOne({ where: { user: { id: user.id } }, relations: {
8✔
192
        user: true,
8✔
193
      } });
8✔
194
      if (!pinAuthenticator) {
9✔
195
        res.status(403).json({
2✔
196
          message: 'Invalid credentials.',
2✔
197
        });
2✔
198
        return;
2✔
199
      }
2✔
200
      const context: AuthenticationContext = {
6✔
201
        roleManager,
6✔
202
        tokenHandler,
6✔
203
      };
6✔
204

205
      const result = await new AuthenticationService().HashAuthentication(pin,
6✔
206
        pinAuthenticator, context, posId);
6✔
207

208
      if (!result) {
9✔
209
        res.status(403).json({
2✔
210
          message: 'Invalid credentials.',
2✔
211
        });
2✔
212
        return;
2✔
213
      }
2✔
214

215
      res.json(AuthenticationService.asAuthenticationResponse(result.user, result.roles, result.organs, result.token));
4✔
216
    };
4✔
217
  }
9✔
218

219
  /**
1✔
220
   * POST /authentication/LDAP
221
   * @summary LDAP login and hand out token
222
   * If user has never signed in before this also creates an account.
223
   * @operationId ldapAuthentication
224
   * @tags authenticate - Operations of authentication controller
225
   * @param {AuthenticationLDAPRequest} request.body.required - The LDAP login.
226
   * @return {AuthenticationResponse} 200 - The created json web token.
227
   * @return {string} 400 - Validation error.
228
   * @return {string} 403 - Authentication error.
229
   */
1✔
230
  public async LDAPLogin(req: Request, res: Response): Promise<void> {
1✔
231
    const body = req.body as AuthenticationLDAPRequest;
2✔
232
    this.logger.trace('LDAP authentication for user', body.accountName);
2✔
233

234
    try {
2✔
235
      await AppDataSource.transaction(async (manager) => {
2✔
236
        const service = new AuthenticationService(manager);
2✔
237
        await AuthenticationController.LDAPLoginConstructor(this.roleManager, this.tokenHandler, service.createUserAndBind.bind(service))(req, res);
2✔
238
      });
2✔
239
    } catch (error) {
2!
240
      this.logger.error('Could not authenticate using LDAP:', error);
×
241
      res.status(500).json('Internal server error.');
×
242
    }
×
243
  }
2✔
244

245
  /**
1✔
246
   * Constructor for the LDAP function to make it easily adaptable.
247
   */
1✔
248
  public static LDAPLoginConstructor(roleManager: RoleManager, tokenHandler: TokenHandler,
1✔
249
    onNewUser: (ADUser: LDAPUser) => Promise<User>) {
4✔
250
    return async (req: Request, res: Response) => {
4✔
251
      const service = new AuthenticationService();
4✔
252
      const body = req.body as AuthenticationLDAPRequest;
4✔
253
      const user = await service.LDAPAuthentication(
4✔
254
        body.accountName, body.password, onNewUser,
4✔
255
      );
256

257
      // If user is undefined something went wrong.
4✔
258
      if (!user) {
4✔
259
        res.status(403).json({
2✔
260
          message: 'Invalid credentials.',
2✔
261
        });
2✔
262
        return;
2✔
263
      }
2✔
264

265
      const context: AuthenticationContext = {
2✔
266
        roleManager,
2✔
267
        tokenHandler,
2✔
268
      };
2✔
269

270
      // AD login gives full access.
2✔
271
      const result = await service.getSaltedToken({ user, context });
2✔
272
      res.json(AuthenticationService.asAuthenticationResponse(result.user, result.roles, result.organs, result.token));
2✔
273
    };
2✔
274
  }
4✔
275

276
  /**
1✔
277
   * POST /authentication/local
278
   * @summary Local login and hand out token
279
   * @operationId localAuthentication
280
   * @tags authenticate - Operations of authentication controller
281
   * @param {AuthenticationLocalRequest} request.body.required - The local login.
282
   * @return {AuthenticationResponse} 200 - The created json web token.
283
   * @return {string} 400 - Validation error.
284
   * @return {string} 403 - Authentication error.
285
   */
1✔
286
  public async LocalLogin(req: Request, res: Response): Promise<void> {
1✔
287
    const body = req.body as AuthenticationLocalRequest;
5✔
288
    this.logger.trace('Local authentication for user', body.accountMail);
5✔
289

290
    try {
5✔
291
      // Email-based lookup is specific to local auth; UserService.getOptions does not support email filter
5✔
292
      const user = await User.findOne({
5✔
293
        where: { email: body.accountMail, deleted: false },
5✔
294
      });
5✔
295

296
      if (!user) {
5✔
297
        res.status(403).json({
1✔
298
          message: 'Invalid credentials.',
1✔
299
        });
1✔
300
        return;
1✔
301
      }
1✔
302

303
      const localAuthenticator = await LocalAuthenticator.findOne({ where: { user: { id: user.id } }, relations: {
4✔
304
        user: true,
4✔
305
      } });
4✔
306
      if (!localAuthenticator) {
5✔
307
        res.status(403).json({
1✔
308
          message: 'Invalid credentials.',
1✔
309
        });
1✔
310
        return;
1✔
311
      }
1✔
312

313
      const context: AuthenticationContext = {
3✔
314
        roleManager: this.roleManager,
3✔
315
        tokenHandler: this.tokenHandler,
3✔
316
      };
3✔
317

318
      const result = await new AuthenticationService().HashAuthentication(body.password,
3✔
319
        localAuthenticator, context);
3✔
320

321
      if (!result) {
5✔
322
        res.status(403).json({
1✔
323
          message: 'Invalid credentials.',
1✔
324
        });
1✔
325
        return;
1✔
326
      }
1✔
327

328
      res.json(AuthenticationService.asAuthenticationResponse(result.user, result.roles, result.organs, result.token));
2✔
329
    } catch (error) {
5!
330
      this.logger.error('Could not authenticate using Local:', error);
×
331
      res.status(500).json('Internal server error.');
×
332
    }
×
333
  }
5✔
334

335
  /**
1✔
336
   * PUT /authentication/local
337
   * @summary Reset local authentication using the provided token
338
   * @operationId resetLocalWithToken
339
   * @tags authenticate - Operations of authentication controller
340
   * @param {AuthenticationResetTokenRequest} request.body.required - The reset token.
341
   * @return 204 - Successfully reset
342
   * @return {string} 403 - Authentication error.
343
   */
1✔
344
  public async resetLocalUsingToken(req: Request, res: Response): Promise<void> {
1✔
345
    const body = req.body as AuthenticationResetTokenRequest;
5✔
346
    this.logger.trace('Reset using token for user', body.accountMail);
5✔
347

348
    try {
5✔
349
      const service = new AuthenticationService();
5✔
350
      const resetToken = await service.isResetTokenRequestValid(body);
5✔
351
      if (!resetToken) {
5✔
352
        res.status(403).json({
3✔
353
          message: 'Invalid request.',
3✔
354
        });
3✔
355
        return;
3✔
356
      }
3✔
357

358
      if (AuthenticationService.isTokenExpired(resetToken)) {
5✔
359
        res.status(403).json({
1✔
360
          message: 'Token expired.',
1✔
361
        });
1✔
362
        return;
1✔
363
      }
1✔
364

365
      await service.resetLocalUsingToken(resetToken, body.token, body.password);
1✔
366
      res.status(204).send();
1✔
367
      return;
1✔
368
    } catch (error) {
5!
369
      this.logger.error('Could not reset using token:', error);
×
370
      res.status(500).json('Internal server error.');
×
371
    }
×
372
  }
5✔
373

374
  /**
1✔
375
   * POST /authentication/local/reset
376
   * @summary Creates a reset token for the local authentication
377
   * @operationId resetLocal
378
   * @tags authenticate - Operations of authentication controller
379
   * @param {ResetLocalRequest} request.body.required - The reset info.
380
   * @return 204 - Creation success
381
   */
1✔
382
  public async createResetToken(req: Request, res: Response): Promise<void> {
1✔
383
    const body = req.body as ResetLocalRequest;
2✔
384
    this.logger.trace('Reset request for user', body.accountMail);
2✔
385
    try {
2✔
386
      // Email-based lookup is specific to local auth; UserService.getOptions does not support email filter
2✔
387
      const user = await User.findOne({
2✔
388
        where: { email: body.accountMail, deleted: false, type: UserType.LOCAL_USER },
2✔
389
      });
2✔
390
      // If the user does not exist we simply return a success code as to not leak info.
2✔
391
      if (!user) {
2✔
392
        res.status(204).send();
2✔
393
        return;
2✔
394
      }
2!
395

396
      const resetTokenInfo = await new AuthenticationService().createResetToken(user);
×
397
      await Notifier.getInstance().notify({
×
398
        type: NotificationTypes.PasswordReset,
×
399
        userId: user.id,
×
400
        params: new WelcomeWithResetOptions(
×
401
          user.email,
×
402
          resetTokenInfo,
×
403
        ),
404
      });
×
405
      // send email with link.
×
406
      res.status(204).send();
×
407
      return;
×
408
    } catch (error) {
×
409
      this.logger.error('Could not create reset token:', error);
×
410
      res.status(500).json('Internal server error.');
×
411
    }
×
412
  }
2✔
413

414
  /**
1✔
415
   * POST /authentication/key
416
   * @summary Key login and hand out token.
417
   * @operationId keyAuthentication
418
   * @tags authenticate - Operations of authentication controller
419
   * @param {AuthenticationKeyRequest} request.body.required - The key login.
420
   * @return {AuthenticationResponse} 200 - The created json web token.
421
   * @return {string} 400 - Validation error.
422
   * @return {string} 403 - Authentication error.
423
   */
1✔
424
  public async keyLogin(req: Request, res: Response): Promise<void> {
1✔
425
    const body = req.body as AuthenticationKeyRequest;
6✔
426
    this.logger.trace('key authentication for user', body.userId);
6✔
427

428
    try {
6✔
429
      const user = await User.findOne(UserService.getOptions({ id: body.userId, allowPos: true }));
6✔
430

431
      if (!user) {
6✔
432
        res.status(403).json({
1✔
433
          message: 'Invalid credentials.',
1✔
434
        });
1✔
435
        return;
1✔
436
      }
1✔
437

438
      const keyAuthenticator = await KeyAuthenticator.findOne({
5✔
439
        where: { user: { id: body.userId } },
5✔
440
        relations: UserService.getRelations<KeyAuthenticator>({ pos: true }),
5✔
441
      });
5✔
442
      if (!keyAuthenticator) {
6✔
443
        res.status(403).json({
1✔
444
          message: 'Invalid credentials.',
1✔
445
        });
1✔
446
        return;
1✔
447
      }
1✔
448

449
      const context: AuthenticationContext = {
4✔
450
        roleManager: this.roleManager,
4✔
451
        tokenHandler: this.tokenHandler,
4✔
452
      };
4✔
453

454
      const result = await new AuthenticationService().HashAuthentication(body.key,
4✔
455
        keyAuthenticator, context);
4✔
456

457
      if (!result) {
6✔
458
        res.status(403).json({
1✔
459
          message: 'Invalid credentials.',
1✔
460
        });
1✔
461
        return;
1✔
462
      }
1✔
463

464
      res.json(AuthenticationService.asAuthenticationResponse(result.user, result.roles, result.organs, result.token));
3✔
465
    } catch (error) {
6!
466
      this.logger.error('Could not authenticate using key:', error);
×
467
      res.status(500).json('Internal server error.');
×
468
    }
×
469
  }
6✔
470

471
  /**
1✔
472
   * POST /authentication/mock
473
   * @summary Mock login and hand out token.
474
   * @operationId mockAuthentication
475
   * @tags authenticate - Operations of authentication controller
476
   * @param {AuthenticationMockRequest} request.body.required - The mock login.
477
   * @return {AuthenticationResponse} 200 - The created json web token.
478
   * @return {string} 400 - Validation error.
479
   */
1✔
480
  public async mockLogin(req: Request, res: Response): Promise<void> {
1✔
481
    const body = req.body as AuthenticationMockRequest;
4✔
482
    this.logger.trace('Mock authentication for user', body.userId);
4✔
483

484
    try {
4✔
485
      const userOptions = UserService.getOptions({ id: body.userId });
4✔
486
      const user = await User.findOne(userOptions);
4✔
487
      if (!user) {
4!
488
        res.status(404).json('User not found.');
×
489
        return;
×
490
      }
×
491
      const result = await new AuthenticationService().getSaltedToken({
4✔
492
        user,
4✔
493
        context: { tokenHandler: this.tokenHandler, roleManager: this.roleManager },
4✔
494
        salt: body.nonce,
4✔
495
      });
4✔
496
      res.json(AuthenticationService.asAuthenticationResponse(result.user, result.roles, result.organs, result.token));
4✔
497
    } catch (error) {
4!
498
      this.logger.error('Could not create token:', error);
×
499
      res.status(500).json('Internal server error.');
×
500
    }
×
501
  }
4✔
502
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc