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

GEWIS / sudosos-backend / 25753937432

12 May 2026 09:17AM UTC coverage: 88.117% (-1.0%) from 89.089%
25753937432

push

github

web-flow
chore(deps): fix missing dependencies for running docs:dev (#911)

3925 of 4574 branches covered (85.81%)

Branch coverage included in aggregate %.

20093 of 22683 relevant lines covered (88.58%)

1125.83 hits per line

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

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

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

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

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

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

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

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

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

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

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

148
    return true;
4✔
149
  }
4✔
150

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

309
      const context: AuthenticationContext = {
3✔
310
        roleManager: this.roleManager,
3✔
311
        tokenHandler: this.tokenHandler,
3✔
312
      };
3✔
313

314
      const result = await new AuthenticationService().HashAuthentication(body.password,
3✔
315
        localAuthenticator, context);
3✔
316

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

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

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

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

354
      if (AuthenticationService.isTokenExpired(resetToken)) {
5✔
355
        res.status(403).json({
1✔
356
          message: 'Token expired.',
1✔
357
        });
1✔
358
        return;
1✔
359
      }
1✔
360

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

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

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

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

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

427
      if (!user) {
6✔
428
        res.status(403).json({
1✔
429
          message: 'Invalid credentials.',
1✔
430
        });
1✔
431
        return;
1✔
432
      }
1✔
433

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

445
      const context: AuthenticationContext = {
4✔
446
        roleManager: this.roleManager,
4✔
447
        tokenHandler: this.tokenHandler,
4✔
448
      };
4✔
449

450
      const result = await new AuthenticationService().HashAuthentication(body.key,
4✔
451
        keyAuthenticator, context);
4✔
452

453
      if (!result) {
6✔
454
        res.status(403).json({
1✔
455
          message: 'Invalid credentials.',
1✔
456
        });
1✔
457
        return;
1✔
458
      }
1✔
459

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

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

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

© 2026 Coveralls, Inc