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

GEWIS / sudosos-backend / 15664172639

15 Jun 2025 02:23PM UTC coverage: 85.087% (-0.007%) from 85.094%
15664172639

Pull #471

github

web-flow
Merge 2cfe2a825 into 5d7267112
Pull Request #471: Add `authentication` docs

1281 of 1565 branches covered (81.85%)

Branch coverage included in aggregate %.

1 of 2 new or added lines in 1 file covered. (50.0%)

33 existing lines in 3 files now uncovered.

6929 of 8084 relevant lines covered (85.71%)

1081.38 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 authentication
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:
56
 * - Verifying user authentications.
57
 * - 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');
2✔
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);
2✔
80
    this.logger.level = process.env.LOG_LEVEL;
2✔
81
    this.tokenHandler = tokenHandler;
2✔
82
  }
83

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

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

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

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

168
    return true;
4✔
169
  }
170

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

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

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

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

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

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

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

238
      res.json(result);
4✔
239
    };
240
  }
241

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

502

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

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

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

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

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

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

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

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

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

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