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

GEWIS / sudosos-backend / 20690997091

04 Jan 2026 09:40AM UTC coverage: 89.701% (-0.1%) from 89.81%
20690997091

Pull #640

github

web-flow
Merge 3f181cd77 into b62eef7ad
Pull Request #640: feat: notification abstraction

1596 of 1909 branches covered (83.6%)

Branch coverage included in aggregate %.

334 of 364 new or added lines in 22 files covered. (91.76%)

16 existing lines in 4 files now uncovered.

8289 of 9111 relevant lines covered (90.98%)

1036.73 hits per line

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

86.76
/src/controller/authentication-controller.ts
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
 */
20

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

27
import { Request, Response } from 'express';
28
import log4js, { Logger } from 'log4js';
2✔
29
import BaseController, { BaseControllerOptions } from './base-controller';
2✔
30
import Policy from './policy';
31
import User, { UserType } from '../entity/user/user';
2✔
32
import AuthenticationMockRequest from './request/authentication-mock-request';
33
import TokenHandler from '../authentication/token-handler';
34
import AuthenticationService, { AuthenticationContext } from '../service/authentication-service';
2✔
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';
2✔
40
import AuthenticationPinRequest from './request/authentication-pin-request';
41
import LocalAuthenticator from '../entity/authenticator/local-authenticator';
2✔
42
import ResetLocalRequest from './request/reset-local-request';
43
import AuthenticationResetTokenRequest from './request/authentication-reset-token-request';
44
import AuthenticationEanRequest from './request/authentication-ean-request';
45
import EanAuthenticator from '../entity/authenticator/ean-authenticator';
2✔
46
import AuthenticationNfcRequest from './request/authentication-nfc-request';
47
import NfcAuthenticator from '../entity/authenticator/nfc-authenticator';
2✔
48
import AuthenticationKeyRequest from './request/authentication-key-request';
49
import KeyAuthenticator from '../entity/authenticator/key-authenticator';
2✔
50
import { AppDataSource } from '../database/database';
2✔
51
import { NotificationTypes } from '../notifications/notification-types';
2✔
52
import Notifier, { WelcomeWithResetOptions } from '../notifications';
2✔
53
import UserService from '../service/user-service';
2✔
54

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

64
  /**
65
   * Reference to the token handler of the application.
66
   */
67
  protected tokenHandler: TokenHandler;
68

69
  /**
70
   * Creates a new authentication controller instance.
71
   * @param options - The options passed to the base controller.
72
   * @param tokenHandler - The token handler for creating signed tokens.
73
   */
74
  public constructor(
75
    options: BaseControllerOptions,
76
    tokenHandler: TokenHandler,
77
  ) {
78
    super(options);
3✔
79
    this.logger.level = process.env.LOG_LEVEL;
3✔
80
    this.tokenHandler = tokenHandler;
3✔
81
  }
82

83
  /**
84
   * @inheritdoc
85
   */
86
  public getPolicy(): Policy {
87
    return {
3✔
88
      '/public': {
89
        GET: {
90
          policy: async () => true,
1✔
91
          handler: this.getJWTPublicKey.bind(this),
92
        },
93
      },
94
      '/mock': {
95
        POST: {
96
          body: { modelName: 'AuthenticationMockRequest' },
97
          policy: AuthenticationController.canPerformMock.bind(this),
98
          handler: this.mockLogin.bind(this),
99
        },
100
      },
101
      '/LDAP': {
102
        POST: {
103
          body: { modelName: 'AuthenticationLDAPRequest' },
104
          policy: async () => true,
2✔
105
          handler: this.LDAPLogin.bind(this),
106
        },
107
      },
108
      '/pin': {
109
        POST: {
110
          body: { modelName: 'AuthenticationPinRequest' },
111
          policy: async () => true,
7✔
112
          handler: this.PINLogin.bind(this),
113
        },
114
      },
115
      '/nfc': {
116
        POST: {
117
          body: { modelName: 'AuthenticationNfcRequest' },
118
          policy: async () => true,
5✔
119
          handler: this.nfcLogin.bind(this),
120
        },
121
      },
122
      '/key': {
123
        POST: {
124
          body: { modelName: 'AuthenticationKeyRequest' },
125
          policy: async () => true,
4✔
126
          handler: this.keyLogin.bind(this),
127
        },
128
      },
129
      '/local': {
130
        POST: {
131
          body: { modelName: 'AuthenticationLocalRequest' },
132
          policy: async () => true,
5✔
133
          handler: this.LocalLogin.bind(this),
134
          restrictions: { availableDuringMaintenance: true },
135
        },
136
        PUT: {
137
          body: { modelName: 'AuthenticationResetTokenRequest' },
138
          policy: async () => true,
5✔
139
          handler: this.resetLocalUsingToken.bind(this),
140
        },
141
      },
142
      '/local/reset': {
143
        POST: {
144
          body: { modelName: 'ResetLocalRequest' },
145
          policy: async () => true,
2✔
146
          handler: this.createResetToken.bind(this),
147
        },
148
      },
149
      '/ean': {
150
        POST: {
151
          body: { modelName: 'AuthenticationEanRequest' },
152
          policy: async () => true,
2✔
153
          handler: this.eanLogin.bind(this),
154
        },
155
      },
156
    };
157
  }
158

159
  /**
160
   * Validates that the request is authorized by the policy.
161
   * @param req - The incoming request.
162
   */
163
  static async canPerformMock(req: Request): Promise<boolean> {
164
    const body = req.body as AuthenticationMockRequest;
6✔
165

166
    // Only allow in development setups
167
    if (process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test') return false;
6✔
168

169
    // Check the existence of the user
170
    const user = await User.findOne({ where: { id: body.userId } });
5✔
171
    if (!user) return false;
5✔
172

173
    return true;
4✔
174
  }
175

176
  /**
177
   * GET /authentication/public
178
   * @summary Get the JWT public key used by SudoSOS
179
   * @operationId getJWTPublicKey
180
   * @tags authenticate - Operations of authentication controller
181
   * @returns {string} 200 - Public key
182
   */
183
  public async getJWTPublicKey(req: Request, res: Response): Promise<void> {
184
    this.logger.trace('Get JWT public key by IP', req.ip);
1✔
185

186
    try {
1✔
187
      const publicKey = this.tokenHandler.getOptions().publicKey;
1✔
188
      res.json(publicKey);
1✔
189
    } catch (error) {
190
      this.logger.error('Could not get JWT public key:', error);
×
191
      res.status(500).json('Internal server error.');
×
192
    }
193
  }
194

195
  /**
196
   * POST /authentication/pin
197
   * @summary PIN login and hand out token
198
   * @operationId pinAuthentication
199
   * @tags authenticate - Operations of authentication controller
200
   * @param {AuthenticationPinRequest} request.body.required - The PIN login.
201
   * @return {AuthenticationResponse} 200 - The created json web token.
202
   * @return {string} 400 - Validation error.
203
   * @return {string} 403 - Authentication error.
204
   */
205
  public async PINLogin(req: Request, res: Response): Promise<void> {
206
    const body = req.body as AuthenticationPinRequest;
7✔
207
    this.logger.trace('PIN authentication for user', body.userId);
7✔
208

209
    try {
7✔
210
      await (AuthenticationController.PINLoginConstructor(this.roleManager,
7✔
211
        this.tokenHandler, body.pin, body.userId))(req, res);
212
    } catch (error) {
213
      this.logger.error('Could not authenticate using PIN:', error);
×
214
      res.status(500).json('Internal server error.');
×
215
    }
216
  }
217

218
  /**
219
   * Construct a login function for PIN.
220
   * This was done such that it is easily adaptable.
221
   * @param roleManager
222
   * @param tokenHandler
223
   * @param pin - Provided PIN code
224
   * @param userId - Provided User
225
   * @param posId - Optional POS identifier (only used by secure endpoints)
226
   * @constructor
227
   */
228
  public static PINLoginConstructor(roleManager: RoleManager, tokenHandler: TokenHandler,
229
    pin: string, userId: number, posId?: number) {
230
    return async (req: Request, res: Response) => {
18✔
231
      const user = await User.findOne({
18✔
232
        where: { id: userId, deleted: false },
233
      });
234

235
      if (!user) {
18✔
236
        res.status(403).json({
2✔
237
          message: 'Invalid credentials.',
238
        });
239
        return;
2✔
240
      }
241

242
      const pinAuthenticator = await PinAuthenticator.findOne({ where: { user: { id: user.id } }, relations: ['user'] });
16✔
243
      if (!pinAuthenticator) {
16✔
244
        res.status(403).json({
3✔
245
          message: 'Invalid credentials.',
246
        });
247
        return;
3✔
248
      }
249
      const context: AuthenticationContext = {
13✔
250
        roleManager,
251
        tokenHandler,
252
      };
253

254
      const result = await new AuthenticationService().HashAuthentication(pin,
13✔
255
        pinAuthenticator, context, posId);
256

257
      if (!result) {
13✔
258
        res.status(403).json({
4✔
259
          message: 'Invalid credentials.',
260
        });
261
      }
262

263
      res.json(result);
13✔
264
    };
265
  }
266

267
  /**
268
   * POST /authentication/LDAP
269
   * @summary LDAP login and hand out token
270
   * If user has never signed in before this also creates an account.
271
   * @operationId ldapAuthentication
272
   * @tags authenticate - Operations of authentication controller
273
   * @param {AuthenticationLDAPRequest} request.body.required - The LDAP login.
274
   * @return {AuthenticationResponse} 200 - The created json web token.
275
   * @return {string} 400 - Validation error.
276
   * @return {string} 403 - Authentication error.
277
   */
278
  public async LDAPLogin(req: Request, res: Response): Promise<void> {
279
    const body = req.body as AuthenticationLDAPRequest;
2✔
280
    this.logger.trace('LDAP authentication for user', body.accountName);
2✔
281

282
    try {
2✔
283
      await AppDataSource.transaction(async (manager) => {
2✔
284
        const service = new AuthenticationService(manager);
2✔
285
        await AuthenticationController.LDAPLoginConstructor(this.roleManager, this.tokenHandler, service.createUserAndBind.bind(service))(req, res);
2✔
286
      });
287
    } catch (error) {
288
      this.logger.error('Could not authenticate using LDAP:', error);
×
289
      res.status(500).json('Internal server error.');
×
290
    }
291
  }
292

293
  /**
294
   * Constructor for the LDAP function to make it easily adaptable.
295
   * @constructor
296
   */
297
  public static LDAPLoginConstructor(roleManager: RoleManager, tokenHandler: TokenHandler,
298
    onNewUser: (ADUser: LDAPUser) => Promise<User>) {
299
    return async (req: Request, res: Response) => {
4✔
300
      const service = new AuthenticationService();
4✔
301
      const body = req.body as AuthenticationLDAPRequest;
4✔
302
      const user = await service.LDAPAuthentication(
4✔
303
        body.accountName, body.password, onNewUser,
304
      );
305

306
      // If user is undefined something went wrong.
307
      if (!user) {
4✔
308
        res.status(403).json({
2✔
309
          message: 'Invalid credentials.',
310
        });
311
        return;
2✔
312
      }
313

314
      const context: AuthenticationContext = {
2✔
315
        roleManager,
316
        tokenHandler,
317
      };
318

319
      // AD login gives full access.
320
      const token = await service.getSaltedToken({ user, context });
2✔
321
      res.json(token);
2✔
322
    };
323
  }
324

325
  /**
326
   * POST /authentication/local
327
   * @summary Local login and hand out token
328
   * @operationId localAuthentication
329
   * @tags authenticate - Operations of authentication controller
330
   * @param {AuthenticationLocalRequest} request.body.required - The local login.
331
   * @return {AuthenticationResponse} 200 - The created json web token.
332
   * @return {string} 400 - Validation error.
333
   * @return {string} 403 - Authentication error.
334
   */
335
  public async LocalLogin(req: Request, res: Response): Promise<void> {
336
    const body = req.body as AuthenticationLocalRequest;
5✔
337
    this.logger.trace('Local authentication for user', body.accountMail);
5✔
338

339
    try {
5✔
340
      const user = await User.findOne({
5✔
341
        where: { email: body.accountMail, deleted: false },
342
      });
343

344
      if (!user) {
5✔
345
        res.status(403).json({
1✔
346
          message: 'Invalid credentials.',
347
        });
348
        return;
1✔
349
      }
350

351
      const localAuthenticator = await LocalAuthenticator.findOne({ where: { user: { id: user.id } }, relations: ['user'] });
4✔
352
      if (!localAuthenticator) {
4✔
353
        res.status(403).json({
1✔
354
          message: 'Invalid credentials.',
355
        });
356
        return;
1✔
357
      }
358

359
      const context: AuthenticationContext = {
3✔
360
        roleManager: this.roleManager,
361
        tokenHandler: this.tokenHandler,
362
      };
363

364
      const result = await new AuthenticationService().HashAuthentication(body.password,
3✔
365
        localAuthenticator, context);
366

367
      if (!result) {
3✔
368
        res.status(403).json({
1✔
369
          message: 'Invalid credentials.',
370
        });
371
      }
372

373
      res.json(result);
3✔
374
    } catch (error) {
375
      this.logger.error('Could not authenticate using Local:', error);
×
376
      res.status(500).json('Internal server error.');
×
377
    }
378
  }
379

380
  /**
381
   * PUT /authentication/local
382
   * @summary Reset local authentication using the provided token
383
   * @operationId resetLocalWithToken
384
   * @tags authenticate - Operations of authentication controller
385
   * @param {AuthenticationResetTokenRequest} request.body.required - The reset token.
386
   * @return 204 - Successfully reset
387
   * @return {string} 403 - Authentication error.
388
   */
389
  public async resetLocalUsingToken(req: Request, res: Response): Promise<void> {
390
    const body = req.body as AuthenticationResetTokenRequest;
5✔
391
    this.logger.trace('Reset using token for user', body.accountMail);
5✔
392

393
    try {
5✔
394
      const service = new AuthenticationService();
5✔
395
      const resetToken = await service.isResetTokenRequestValid(body);
5✔
396
      if (!resetToken) {
5✔
397
        res.status(403).json({
3✔
398
          message: 'Invalid request.',
399
        });
400
        return;
3✔
401
      }
402

403
      if (AuthenticationService.isTokenExpired(resetToken)) {
2✔
404
        res.status(403).json({
1✔
405
          message: 'Token expired.',
406
        });
407
        return;
1✔
408
      }
409

410
      await service.resetLocalUsingToken(resetToken, body.token, body.password);
1✔
411
      res.status(204).send();
1✔
412
      return;
1✔
413
    } catch (error) {
414
      this.logger.error('Could not reset using token:', error);
×
415
      res.status(500).json('Internal server error.');
×
416
    }
417
  }
418

419
  /**
420
   * POST /authentication/local/reset
421
   * @summary Creates a reset token for the local authentication
422
   * @operationId resetLocal
423
   * @tags authenticate - Operations of authentication controller
424
   * @param {ResetLocalRequest} request.body.required - The reset info.
425
   * @return 204 - Creation success
426
   */
427
  public async createResetToken(req: Request, res: Response): Promise<void> {
428
    const body = req.body as ResetLocalRequest;
2✔
429
    this.logger.trace('Reset request for user', body.accountMail);
2✔
430
    try {
2✔
431
      const user = await User.findOne({
2✔
432
        where: { email: body.accountMail, deleted: false, type: UserType.LOCAL_USER },
433
      });
434
      // If the user does not exist we simply return a success code as to not leak info.
435
      if (!user) {
2✔
436
        res.status(204).send();
2✔
437
        return;
2✔
438
      }
439

440
      const resetTokenInfo = await new AuthenticationService().createResetToken(user);
×
NEW
441
      await Notifier.getInstance().notify({
×
442
        type: NotificationTypes.PasswordReset,
443
        userId: user.id,
444
        params: new WelcomeWithResetOptions(
445
          user.email,
446
          resetTokenInfo,
447
        ),
448
      });
449
      // send email with link.
450
      res.status(204).send();
×
451
      return;
×
452
    } catch (error) {
453
      this.logger.error('Could not create reset token:', error);
×
454
      res.status(500).json('Internal server error.');
×
455
    }
456
  }
457

458
  /**
459
   * POST /authentication/nfc
460
   * @summary NFC login and hand out token
461
   * @operationId nfcAuthentication
462
   * @tags authenticate - Operations of authentication controller
463
   * @param {AuthenticationNfcRequest} request.body.required - The NFC login.
464
   * @return {AuthenticationResponse} 200 - The created json web token.
465
   * @return {string} 403 - Authentication error.
466
   */
467
  public async nfcLogin(req: Request, res: Response): Promise<void> {
468
    const body = req.body as AuthenticationNfcRequest;
5✔
469
    this.logger.trace('Atempted NFC authentication with NFC length, ', body.nfcCode.length);
5✔
470

471
    try {
5✔
472
      const authenticator = await NfcAuthenticator.findOne({
5✔
473
        where: { nfcCode: body.nfcCode },
474
        relations: UserService.getRelations<NfcAuthenticator>(),
475
      });
476
      if (authenticator == null || authenticator.user == null) {
5✔
477
        res.status(403).json({
2✔
478
          message: 'Invalid credentials.',
479
        });
480
        return;
2✔
481
      }
482

483
      const context: AuthenticationContext = {
3✔
484
        roleManager: this.roleManager,
485
        tokenHandler: this.tokenHandler,
486
      };
487

488
      this.logger.trace('Succesfull NFC authentication for user ', authenticator.user);
3✔
489

490
      const token = await new AuthenticationService().getSaltedToken({
3✔
491
        user: authenticator.user,
492
        context,
493
      });
494
      res.json(token);
3✔
495
    } catch (error) {
496
      this.logger.error('Could not authenticate using NFC:', error);
×
497
      res.status(500).json('Internal server error.');
×
498
    }
499
  }
500

501
  /**
502
   * POST /authentication/ean
503
   * @summary EAN login and hand out token
504
   * @operationId eanAuthentication
505
   * @tags authenticate - Operations of authentication controller
506
   * @param {AuthenticationEanRequest} request.body.required - The EAN login.
507
   * @return {AuthenticationResponse} 200 - The created json web token.
508
   * @return {string} 403 - Authentication error.
509
   */
510
  public async eanLogin(req: Request, res: Response): Promise<void> {
511
    const body = req.body as AuthenticationEanRequest;
2✔
512
    this.logger.trace('EAN authentication for ean', body.eanCode);
2✔
513

514
    try {
2✔
515
      const { eanCode } = body;
2✔
516
      const authenticator = await EanAuthenticator.findOne({
2✔
517
        where: { eanCode },
518
        relations: UserService.getRelations<EanAuthenticator>(),
519
      });
520
      if (authenticator == null || authenticator.user == null) {
2✔
521
        res.status(403).json({
1✔
522
          message: 'Invalid credentials.',
523
        });
524
        return;
1✔
525
      }
526

527
      const context: AuthenticationContext = {
1✔
528
        roleManager: this.roleManager,
529
        tokenHandler: this.tokenHandler,
530
      };
531

532
      const token = await new AuthenticationService().getSaltedToken({
1✔
533
        user: authenticator.user,
534
        context,
535
      });
536
      res.json(token);
1✔
537
    } catch (error) {
538
      this.logger.error('Could not authenticate using EAN:', error);
×
539
      res.status(500).json('Internal server error.');
×
540
    }
541
  }
542

543

544
  /**
545
   * POST /authentication/key
546
   * @summary Key login and hand out token.
547
   * @operationId keyAuthentication
548
   * @tags authenticate - Operations of authentication controller
549
   * @param {AuthenticationKeyRequest} request.body.required - The key login.
550
   * @return {AuthenticationResponse} 200 - The created json web token.
551
   * @return {string} 400 - Validation error.
552
   * @return {string} 403 - Authentication error.
553
   */
554
  public async keyLogin(req: Request, res: Response): Promise<void> {
555
    const body = req.body as AuthenticationKeyRequest;
4✔
556
    this.logger.trace('key authentication for user', body.userId);
4✔
557

558
    try {
4✔
559
      const user = await User.findOne({
4✔
560
        where: { id: body.userId, deleted: false },
561
      });
562

563
      if (!user) {
4✔
564
        res.status(403).json({
1✔
565
          message: 'Invalid credentials.',
566
        });
567
        return;
1✔
568
      }
569

570
      const keyAuthenticator = await KeyAuthenticator.findOne({
3✔
571
        where: { user: { id: body.userId } },
572
        relations: UserService.getRelations<KeyAuthenticator>({ pos: true }),
573
      });
574
      if (!keyAuthenticator) {
3✔
575
        res.status(403).json({
1✔
576
          message: 'Invalid credentials.',
577
        });
578
        return;
1✔
579
      }
580

581
      const context: AuthenticationContext = {
2✔
582
        roleManager: this.roleManager,
583
        tokenHandler: this.tokenHandler,
584
      };
585

586
      const result = await new AuthenticationService().HashAuthentication(body.key,
2✔
587
        keyAuthenticator, context);
588

589
      if (!result) {
2✔
590
        res.status(403).json({
1✔
591
          message: 'Invalid credentials.',
592
        });
593
      }
594

595
      res.json(result);
2✔
596
    } catch (error) {
597
      this.logger.error('Could not authenticate using key:', error);
×
598
      res.status(500).json('Internal server error.');
×
599
    }
600
  }
601

602
  /**
603
   * POST /authentication/mock
604
   * @summary Mock login and hand out token.
605
   * @operationId mockAuthentication
606
   * @tags authenticate - Operations of authentication controller
607
   * @param {AuthenticationMockRequest} request.body.required - The mock login.
608
   * @return {AuthenticationResponse} 200 - The created json web token.
609
   * @return {string} 400 - Validation error.
610
   */
611
  public async mockLogin(req: Request, res: Response): Promise<void> {
612
    const body = req.body as AuthenticationMockRequest;
4✔
613
    this.logger.trace('Mock authentication for user', body.userId);
4✔
614

615
    try {
4✔
616
      const userOptions = UserService.getOptions({ id: body.userId });
4✔
617
      const user = await User.findOne(userOptions);
4✔
618
      if (!user) {
4!
619
        res.status(404).json('User not found.');
×
620
        return;
×
621
      }
622
      const response = await new AuthenticationService().getSaltedToken({
4✔
623
        user,
624
        context: { tokenHandler: this.tokenHandler, roleManager: this.roleManager },
625
        salt: body.nonce,
626
      });
627
      res.json(response);
4✔
628
    } catch (error) {
629
      this.logger.error('Could not create token:', error);
×
630
      res.status(500).json('Internal server error.');
×
631
    }
632
  }
633
}
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