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

GEWIS / sudosos-backend / 19908327267

03 Dec 2025 08:47PM UTC coverage: 89.574% (-0.2%) from 89.778%
19908327267

Pull #640

github

web-flow
Merge a34badee5 into 017e7a4eb
Pull Request #640: feat: notification abstraction

1527 of 1814 branches covered (84.18%)

Branch coverage included in aggregate %.

334 of 366 new or added lines in 22 files covered. (91.26%)

22 existing lines in 5 files now uncovered.

8009 of 8832 relevant lines covered (90.68%)

1051.65 hits per line

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

86.83
/src/controller/authentication-controller.ts
1
/**
2
 *  SudoSOS back-end API service.
3
 *  Copyright (C) 2024  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 { NotificationChannels } from '../entity/notifications/user-notification-preference';
2✔
54
import UserService from '../service/user-service';
2✔
55

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

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

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

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

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

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

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

174
    return true;
4✔
175
  }
176

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

545

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

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

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

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

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

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

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

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

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

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