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

GEWIS / sudosos-backend / 18949526732

28 Oct 2025 08:59AM UTC coverage: 89.883% (-0.004%) from 89.887%
18949526732

push

github

web-flow
refactor: remove old deprecated member-authenticator.ts and move to organ membership (#618)

1373 of 1631 branches covered (84.18%)

Branch coverage included in aggregate %.

28 of 29 new or added lines in 6 files covered. (96.55%)

84 existing lines in 11 files now uncovered.

7218 of 7927 relevant lines covered (91.06%)

1112.1 hits per line

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

88.08
/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 Mailer from '../mailer';
2✔
47
import PasswordReset from '../mailer/messages/password-reset';
2✔
48
import AuthenticationNfcRequest from './request/authentication-nfc-request';
49
import NfcAuthenticator from '../entity/authenticator/nfc-authenticator';
2✔
50
import AuthenticationKeyRequest from './request/authentication-key-request';
51
import KeyAuthenticator from '../entity/authenticator/key-authenticator';
2✔
52
import { AppDataSource } from '../database/database';
2✔
53

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

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

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

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

152
  /**
153
   * Validates that the request is authorized by the policy.
154
   * @param req - The incoming request.
155
   */
156
  static async canPerformMock(req: Request): Promise<boolean> {
157
    const body = req.body as AuthenticationMockRequest;
6✔
158

159
    // Only allow in development setups
160
    if (process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test') return false;
6✔
161

162
    // Check the existence of the user
163
    const user = await User.findOne({ where: { id: body.userId } });
5✔
164
    if (!user) return false;
5✔
165

166
    return true;
4✔
167
  }
168

169
  /**
170
   * POST /authentication/pin
171
   * @summary PIN login and hand out token
172
   * @operationId pinAuthentication
173
   * @tags authenticate - Operations of authentication controller
174
   * @param {AuthenticationPinRequest} request.body.required - The PIN login.
175
   * @return {AuthenticationResponse} 200 - The created json web token.
176
   * @return {string} 400 - Validation error.
177
   * @return {string} 403 - Authentication error.
178
   */
179
  public async PINLogin(req: Request, res: Response): Promise<void> {
180
    const body = req.body as AuthenticationPinRequest;
4✔
181
    this.logger.trace('PIN authentication for user', body.userId);
4✔
182

183
    try {
4✔
184
      await (AuthenticationController.PINLoginConstructor(this.roleManager,
4✔
185
        this.tokenHandler, body.pin, body.userId))(req, res);
186
    } catch (error) {
UNCOV
187
      this.logger.error('Could not authenticate using PIN:', error);
×
UNCOV
188
      res.status(500).json('Internal server error.');
×
189
    }
190
  }
191

192
  /**
193
   * Construct a login function for PIN.
194
   * This was done such that it is easily adaptable.
195
   * @param roleManager
196
   * @param tokenHandler
197
   * @param pin - Provided PIN code
198
   * @param userId - Provided User
199
   * @constructor
200
   */
201
  public static PINLoginConstructor(roleManager: RoleManager, tokenHandler: TokenHandler,
202
    pin: string, userId: number) {
203
    return async (req: Request, res: Response) => {
6✔
204
      const user = await User.findOne({
6✔
205
        where: { id: userId, deleted: false },
206
      });
207

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

215
      const pinAuthenticator = await PinAuthenticator.findOne({ where: { user: { id: user.id } }, relations: ['user'] });
5✔
216
      if (!pinAuthenticator) {
5✔
217
        res.status(403).json({
1✔
218
          message: 'Invalid credentials.',
219
        });
220
        return;
1✔
221
      }
222
      const context: AuthenticationContext = {
4✔
223
        roleManager,
224
        tokenHandler,
225
      };
226

227
      const result = await new AuthenticationService().HashAuthentication(pin,
4✔
228
        pinAuthenticator, context, true);
229

230
      if (!result) {
4✔
231
        res.status(403).json({
2✔
232
          message: 'Invalid credentials.',
233
        });
234
      }
235

236
      res.json(result);
4✔
237
    };
238
  }
239

240
  /**
241
   * POST /authentication/LDAP
242
   * @summary LDAP login and hand out token
243
   * If user has never signed in before this also creates an account.
244
   * @operationId ldapAuthentication
245
   * @tags authenticate - Operations of authentication controller
246
   * @param {AuthenticationLDAPRequest} request.body.required - The LDAP login.
247
   * @return {AuthenticationResponse} 200 - The created json web token.
248
   * @return {string} 400 - Validation error.
249
   * @return {string} 403 - Authentication error.
250
   */
251
  public async LDAPLogin(req: Request, res: Response): Promise<void> {
252
    const body = req.body as AuthenticationLDAPRequest;
2✔
253
    this.logger.trace('LDAP authentication for user', body.accountName);
2✔
254

255
    try {
2✔
256
      await AppDataSource.transaction(async (manager) => {
2✔
257
        const service = new AuthenticationService(manager);
2✔
258
        await AuthenticationController.LDAPLoginConstructor(this.roleManager, this.tokenHandler, service.createUserAndBind.bind(service))(req, res);
2✔
259
      });
260
    } catch (error) {
UNCOV
261
      this.logger.error('Could not authenticate using LDAP:', error);
×
UNCOV
262
      res.status(500).json('Internal server error.');
×
263
    }
264
  }
265

266
  /**
267
   * Constructor for the LDAP function to make it easily adaptable.
268
   * @constructor
269
   */
270
  public static LDAPLoginConstructor(roleManager: RoleManager, tokenHandler: TokenHandler,
271
    onNewUser: (ADUser: LDAPUser) => Promise<User>) {
272
    return async (req: Request, res: Response) => {
4✔
273
      const service = new AuthenticationService();
4✔
274
      const body = req.body as AuthenticationLDAPRequest;
4✔
275
      const user = await service.LDAPAuthentication(
4✔
276
        body.accountName, body.password, onNewUser,
277
      );
278

279
      // If user is undefined something went wrong.
280
      if (!user) {
4✔
281
        res.status(403).json({
2✔
282
          message: 'Invalid credentials.',
283
        });
284
        return;
2✔
285
      }
286

287
      const context: AuthenticationContext = {
2✔
288
        roleManager,
289
        tokenHandler,
290
      };
291

292
      // AD login gives full access.
293
      const token = await service.getSaltedToken(user, context, false);
2✔
294
      res.json(token);
2✔
295
    };
296
  }
297

298
  /**
299
   * POST /authentication/local
300
   * @summary Local login and hand out token
301
   * @operationId localAuthentication
302
   * @tags authenticate - Operations of authentication controller
303
   * @param {AuthenticationLocalRequest} request.body.required - The local login.
304
   * @return {AuthenticationResponse} 200 - The created json web token.
305
   * @return {string} 400 - Validation error.
306
   * @return {string} 403 - Authentication error.
307
   */
308
  public async LocalLogin(req: Request, res: Response): Promise<void> {
309
    const body = req.body as AuthenticationLocalRequest;
5✔
310
    this.logger.trace('Local authentication for user', body.accountMail);
5✔
311

312
    try {
5✔
313
      const user = await User.findOne({
5✔
314
        where: { email: body.accountMail, deleted: false },
315
      });
316

317
      if (!user) {
5✔
318
        res.status(403).json({
1✔
319
          message: 'Invalid credentials.',
320
        });
321
        return;
1✔
322
      }
323

324
      const localAuthenticator = await LocalAuthenticator.findOne({ where: { user: { id: user.id } }, relations: ['user'] });
4✔
325
      if (!localAuthenticator) {
4✔
326
        res.status(403).json({
1✔
327
          message: 'Invalid credentials.',
328
        });
329
        return;
1✔
330
      }
331

332
      const context: AuthenticationContext = {
3✔
333
        roleManager: this.roleManager,
334
        tokenHandler: this.tokenHandler,
335
      };
336

337
      const result = await new AuthenticationService().HashAuthentication(body.password,
3✔
338
        localAuthenticator, context, false);
339

340
      if (!result) {
3✔
341
        res.status(403).json({
1✔
342
          message: 'Invalid credentials.',
343
        });
344
      }
345

346
      res.json(result);
3✔
347
    } catch (error) {
UNCOV
348
      this.logger.error('Could not authenticate using Local:', error);
×
UNCOV
349
      res.status(500).json('Internal server error.');
×
350
    }
351
  }
352

353
  /**
354
   * PUT /authentication/local
355
   * @summary Reset local authentication using the provided token
356
   * @operationId resetLocalWithToken
357
   * @tags authenticate - Operations of authentication controller
358
   * @param {AuthenticationResetTokenRequest} request.body.required - The reset token.
359
   * @return 204 - Successfully reset
360
   * @return {string} 403 - Authentication error.
361
   */
362
  public async resetLocalUsingToken(req: Request, res: Response): Promise<void> {
363
    const body = req.body as AuthenticationResetTokenRequest;
5✔
364
    this.logger.trace('Reset using token for user', body.accountMail);
5✔
365

366
    try {
5✔
367
      const service = new AuthenticationService();
5✔
368
      const resetToken = await service.isResetTokenRequestValid(body);
5✔
369
      if (!resetToken) {
5✔
370
        res.status(403).json({
3✔
371
          message: 'Invalid request.',
372
        });
373
        return;
3✔
374
      }
375

376
      if (AuthenticationService.isTokenExpired(resetToken)) {
2✔
377
        res.status(403).json({
1✔
378
          message: 'Token expired.',
379
        });
380
        return;
1✔
381
      }
382

383
      await service.resetLocalUsingToken(resetToken, body.token, body.password);
1✔
384
      res.status(204).send();
1✔
385
      return;
1✔
386
    } catch (error) {
UNCOV
387
      this.logger.error('Could not reset using token:', error);
×
UNCOV
388
      res.status(500).json('Internal server error.');
×
389
    }
390
  }
391

392
  /**
393
   * POST /authentication/local/reset
394
   * @summary Creates a reset token for the local authentication
395
   * @operationId resetLocal
396
   * @tags authenticate - Operations of authentication controller
397
   * @param {ResetLocalRequest} request.body.required - The reset info.
398
   * @return 204 - Creation success
399
   */
400
  public async createResetToken(req: Request, res: Response): Promise<void> {
401
    const body = req.body as ResetLocalRequest;
2✔
402
    this.logger.trace('Reset request for user', body.accountMail);
2✔
403
    try {
2✔
404
      const user = await User.findOne({
2✔
405
        where: { email: body.accountMail, deleted: false, type: UserType.LOCAL_USER },
406
      });
407
      // If the user does not exist we simply return a success code as to not leak info.
408
      if (!user) {
2✔
409
        res.status(204).send();
2✔
410
        return;
2✔
411
      }
412

UNCOV
413
      const resetTokenInfo = await new AuthenticationService().createResetToken(user);
×
UNCOV
414
      Mailer.getInstance().send(user, new PasswordReset({ email: user.email, resetTokenInfo }))
×
415
        .then()
416
        .catch((error) => this.logger.error(error));
×
417
      // send email with link.
418
      res.status(204).send();
×
UNCOV
419
      return;
×
420
    } catch (error) {
421
      this.logger.error('Could not create reset token:', error);
×
UNCOV
422
      res.status(500).json('Internal server error.');
×
423
    }
424
  }
425

426
  /**
427
   * POST /authentication/nfc
428
   * @summary NFC login and hand out token
429
   * @operationId nfcAuthentication
430
   * @tags authenticate - Operations of authentication controller
431
   * @param {AuthenticationNfcRequest} request.body.required - The NFC login.
432
   * @return {AuthenticationResponse} 200 - The created json web token.
433
   * @return {string} 403 - Authentication error.
434
   */
435
  public async nfcLogin(req: Request, res: Response): Promise<void> {
436
    const body = req.body as AuthenticationNfcRequest;
3✔
437
    this.logger.trace('Atempted NFC authentication with NFC length, ', body.nfcCode.length);
3✔
438

439
    try {
3✔
440
      const { nfcCode } = body;
3✔
441
      const authenticator = await NfcAuthenticator.findOne({ where: { nfcCode: nfcCode } });
3✔
442
      if (authenticator == null || authenticator.user == null) {
3✔
443
        res.status(403).json({
2✔
444
          message: 'Invalid credentials.',
445
        });
446
        return;
2✔
447
      }
448

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

454
      this.logger.trace('Succesfull NFC authentication for user ', authenticator.user);
1✔
455

456
      const token = await new AuthenticationService().getSaltedToken(authenticator.user, context, true);
1✔
457
      res.json(token);
1✔
458
    } catch (error) {
UNCOV
459
      this.logger.error('Could not authenticate using NFC:', error);
×
UNCOV
460
      res.status(500).json('Internal server error.');
×
461
    }
462
  }
463

464
  /**
465
   * POST /authentication/ean
466
   * @summary EAN login and hand out token
467
   * @operationId eanAuthentication
468
   * @tags authenticate - Operations of authentication controller
469
   * @param {AuthenticationEanRequest} request.body.required - The EAN login.
470
   * @return {AuthenticationResponse} 200 - The created json web token.
471
   * @return {string} 403 - Authentication error.
472
   */
473
  public async eanLogin(req: Request, res: Response): Promise<void> {
474
    const body = req.body as AuthenticationEanRequest;
2✔
475
    this.logger.trace('EAN authentication for ean', body.eanCode);
2✔
476

477
    try {
2✔
478
      const { eanCode } = body;
2✔
479
      const authenticator = await EanAuthenticator.findOne({ where: { eanCode } });
2✔
480
      if (authenticator == null || authenticator.user == null) {
2✔
481
        res.status(403).json({
1✔
482
          message: 'Invalid credentials.',
483
        });
484
        return;
1✔
485
      }
486

487
      const context: AuthenticationContext = {
1✔
488
        roleManager: this.roleManager,
489
        tokenHandler: this.tokenHandler,
490
      };
491

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

500

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

515
    try {
4✔
516
      const user = await User.findOne({
4✔
517
        where: { id: body.userId, deleted: false },
518
      });
519

520
      if (!user) {
4✔
521
        res.status(403).json({
1✔
522
          message: 'Invalid credentials.',
523
        });
524
        return;
1✔
525
      }
526

527
      const keyAuthenticator = await KeyAuthenticator.findOne({ where: { user: { id: body.userId } }, relations: ['user'] });
3✔
528
      if (!keyAuthenticator) {
3✔
529
        res.status(403).json({
1✔
530
          message: 'Invalid credentials.',
531
        });
532
        return;
1✔
533
      }
534

535
      const context: AuthenticationContext = {
2✔
536
        roleManager: this.roleManager,
537
        tokenHandler: this.tokenHandler,
538
      };
539

540
      const result = await new AuthenticationService().HashAuthentication(body.key,
2✔
541
        keyAuthenticator, context, false);
542

543
      if (!result) {
2✔
544
        res.status(403).json({
1✔
545
          message: 'Invalid credentials.',
546
        });
547
      }
548

549
      res.json(result);
2✔
550
    } catch (error) {
UNCOV
551
      this.logger.error('Could not authenticate using key:', error);
×
UNCOV
552
      res.status(500).json('Internal server error.');
×
553
    }
554
  }
555

556
  /**
557
   * POST /authentication/mock
558
   * @summary Mock login and hand out token.
559
   * @operationId mockAuthentication
560
   * @tags authenticate - Operations of authentication controller
561
   * @param {AuthenticationMockRequest} request.body.required - The mock login.
562
   * @return {AuthenticationResponse} 200 - The created json web token.
563
   * @return {string} 400 - Validation error.
564
   */
565
  public async mockLogin(req: Request, res: Response): Promise<void> {
566
    const body = req.body as AuthenticationMockRequest;
4✔
567
    this.logger.trace('Mock authentication for user', body.userId);
4✔
568

569
    try {
4✔
570
      const user = await User.findOne({ where: { id: body.userId } });
4✔
571
      const response = await new AuthenticationService().getSaltedToken(
4✔
572
        user,
573
        { tokenHandler: this.tokenHandler, roleManager: this.roleManager },
574
        false,
575
        body.nonce,
576
      );
577
      res.json(response);
4✔
578
    } catch (error) {
UNCOV
579
      this.logger.error('Could not create token:', error);
×
UNCOV
580
      res.status(500).json('Internal server error.');
×
581
    }
582
  }
583
}
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