• 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

91.59
/src/controller/authentication-secure-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
 * @module internal/controllers
23
 */
1✔
24

25
import { Response } from 'express';
26
import log4js, { Logger } from 'log4js';
27
import BaseController, { BaseControllerOptions } from './base-controller';
28
import Policy from './policy';
29
import { RequestWithToken } from '../middleware/token-middleware';
30
import TokenHandler from '../authentication/token-handler';
31
import User from '../entity/user/user';
32
import PointOfSaleController from './point-of-sale-controller';
33
import PointOfSale from '../entity/point-of-sale/point-of-sale';
34
import ServerSettingsStore from '../server-settings/server-settings-store';
35
import { ISettings } from '../entity/server-setting';
36
import { QRAuthenticatorStatus } from '../entity/authenticator/qr-authenticator';
37
import WebSocketService from '../service/websocket-service';
38
import QRService from '../service/qr-service';
39
import AuthenticationSecurePinRequest from './request/authentication-secure-pin-request';
40
import AuthenticationSecureNfcRequest from './request/authentication-secure-nfc-request';
41
import AuthenticationSecureEanRequest from './request/authentication-secure-ean-request';
42
import { UserType } from '../entity/user/user';
43
import AuthenticationController from './authentication-controller';
44
import AuthenticationService from '../service/authentication-service';
45
import NfcAuthenticator from '../entity/authenticator/nfc-authenticator';
46
import EanAuthenticator from '../entity/authenticator/ean-authenticator';
47
import { AuthenticationContext } from '../service/authentication-service';
48
import UserService from '../service/user-service';
49

50
/**
1✔
51
 * Handles authenticated-only authentication endpoints for token management and specialized flows.
52
 * All endpoints require valid JWT tokens and build upon existing authentication.
53
 *
54
 * ## Internal Implementation Notes
55
 * - Token refresh maintains the same access level by preserving the posId property (if present)
56
 * - POS authentication uses custom expiry settings from server settings
57
 * - QR confirmation integrates with WebSocket service for real-time notifications
58
 * - All methods use the role manager for permission validation
59
 *
60
 * @promote
61
 */
1✔
62
export default class AuthenticationSecureController extends BaseController {
1✔
63
  private logger: Logger = log4js.getLogger('AuthenticationController');
1✔
64

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

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

81
  /**
1✔
82
   * @inheritdoc
83
   */
1✔
84
  public getPolicy(): Policy {
1✔
85
    return {
3✔
86
      '/refreshToken': {
3✔
87
        GET: {
3✔
88
          policy: async () => Promise.resolve(true),
3✔
89
          handler: this.refreshToken.bind(this),
3✔
90
          restrictions: { lesser: true, acceptedTOS: false },
3✔
91
        },
3✔
92
      },
3✔
93
      '/pointofsale/:id(\\d+)': {
3✔
94
        GET: {
3✔
95
          policy: async (req) => this.roleManager.can(req.token.roles, 'authenticate', await PointOfSaleController.getRelation(req), 'User', ['pointOfSale']),
3✔
96
          handler: this.authenticatePointOfSale.bind(this),
3✔
97
        },
3✔
98
      },
3✔
99
      '/qr/:sessionId/confirm': {
3✔
100
        POST: {
3✔
101
          policy: async () => Promise.resolve(true),
3✔
102
          handler: this.confirmQRCode.bind(this),
3✔
103
          restrictions: { lesser: false },
3✔
104
        },
3✔
105
      },
3✔
106
      '/pin-secure': {
3✔
107
        POST: {
3✔
108
          policy: async () => Promise.resolve(true),
3✔
109
          handler: this.securePINLogin.bind(this),
3✔
110
          restrictions: { lesser: false },
3✔
111
        },
3✔
112
      },
3✔
113
      '/nfc-secure': {
3✔
114
        POST: {
3✔
115
          policy: async () => Promise.resolve(true),
3✔
116
          handler: this.secureNfcLogin.bind(this),
3✔
117
          restrictions: { lesser: false },
3✔
118
        },
3✔
119
      },
3✔
120
      '/ean-secure': {
3✔
121
        POST: {
3✔
122
          policy: async () => Promise.resolve(true),
3✔
123
          handler: this.secureEanLogin.bind(this),
3✔
124
          restrictions: { lesser: false },
3✔
125
        },
3✔
126
      },
3✔
127
    };
3✔
128
  }
3✔
129

130
  /**
1✔
131
   * GET /authentication/refreshToken
132
   * @summary Get a new JWT token, maintaining the same access level (posId) as the original token
133
   * @operationId refreshToken
134
   * @tags authenticate - Operations of the authentication controller
135
   * @security JWT
136
   * @return {AuthenticationResponse} 200 - The created json web token.
137
   */
1✔
138
  private async refreshToken(req: RequestWithToken, res: Response): Promise<void> {
1✔
139
    this.logger.trace('Refresh token for user', req.token.user.id);
3✔
140

141
    try {
3✔
142
      const userOptions = UserService.getOptions({ id: req.token.user.id, allowPos: true });
3✔
143
      const user = await User.findOne(userOptions);
3✔
144
      if (!user) {
3!
145
        res.status(404).json('User not found.');
×
146
        return;
×
147
      }
×
148

149
      let expiry: number | undefined = undefined;
3✔
150

151
      if (user.type === UserType.POINT_OF_SALE) {
3✔
152
        expiry = ServerSettingsStore.getInstance().getSetting('jwtExpiryPointOfSale') as ISettings['jwtExpiryPointOfSale'];
2✔
153
      }
2✔
154

155
      const result = await new AuthenticationService().getSaltedToken({
3✔
156
        user,
3✔
157
        context: {
3✔
158
          roleManager: this.roleManager,
3✔
159
          tokenHandler: this.tokenHandler,
3✔
160
        },
3✔
161
        expiry,
3✔
162
        posId: req.token.posId,
3✔
163
      });
3✔
164
      res.json(AuthenticationService.asAuthenticationResponse(result.user, result.roles, result.organs, result.token));
3✔
165
    } catch (error) {
3!
166
      this.logger.error('Could not create token:', error);
×
167
      res.status(500).json('Internal server error.');
×
168
    }
×
169
  }
3✔
170

171
  /**
1✔
172
   * GET /authentication/pointofsale/{id}
173
   * @summary Get a JWT token for the given POS
174
   * @operationId authenticatePointOfSale
175
   * @tags authenticate - Operations of the authentication controller
176
   * @security JWT
177
   * @param {integer} id.path.required - The id of the user
178
   * @return {AuthenticationResponse} 200 - The created json web token.
179
   * @return {string} 404 - Point of sale not found
180
   * @return {string} 500 - Internal server error
181
   */
1✔
182
  private async authenticatePointOfSale(req: RequestWithToken, res: Response): Promise<void> {
1✔
183
    this.logger.trace('Authenticate point of sale', req.params.id, 'by user', req.token.user.id);
5✔
184

185
    try {
5✔
186
      const pointOfSaleId = Number(req.params.id);
5✔
187
      const options = UserService.getOptions({ pointOfSaleId });
5✔
188
      const user = await User.findOne(options);
5✔
189
      if (!user || !user.pointOfSale) {
5✔
190
        res.status(404).json('Point of sale not found.');
2✔
191
        return;
2✔
192
      }
2✔
193

194
      const expiry = ServerSettingsStore.getInstance().getSetting('jwtExpiryPointOfSale') as ISettings['jwtExpiryPointOfSale'];
3✔
195
      const result = await new AuthenticationService().getSaltedToken({
3✔
196
        user,
3✔
197
        context: {
3✔
198
          roleManager: this.roleManager,
3✔
199
          tokenHandler: this.tokenHandler,
3✔
200
        },
3✔
201
        expiry,
3✔
202
      });
3✔
203
      res.json(AuthenticationService.asAuthenticationResponse(result.user, result.roles, result.organs, result.token));
3✔
204
    } catch (error) {
5!
205
      this.logger.error('Could not create token:', error);
×
206
      res.status(500).json('Internal server error.');
×
207
    }
×
208
  }
5✔
209

210
  /**
1✔
211
   * POST /authentication/qr/{sessionId}/confirm
212
   * @summary Confirm QR code authentication from mobile app
213
   * @operationId confirmQRCode
214
   * @tags authenticate - Operations of authentication controller
215
   * @param {string} sessionId.path.required - The session ID
216
   * @security JWT
217
   * @return 200 - Successfully confirmed
218
   * @return {string} 400 - Validation error
219
   * @return {string} 404 - Session not found
220
   * @return {string} 410 - Session expired
221
   * @return {string} 500 - Internal server error
222
   */
1✔
223
  private async confirmQRCode(req: RequestWithToken, res: Response): Promise<void> {
1✔
224
    const { sessionId } = req.params;
11✔
225
    this.logger.trace('Confirming QR code for session', sessionId, 'by user', req.token.user);
11✔
226

227
    try {
11✔
228
      const qrAuthenticator = await (new QRService()).get(sessionId);
11✔
229
      if (!qrAuthenticator) {
11✔
230
        res.status(404).json('Session not found.');
1✔
231
        return;
1✔
232
      }
1✔
233

234
      if (qrAuthenticator.status === QRAuthenticatorStatus.EXPIRED) {
11✔
235
        res.status(410).json('Session has expired.');
1✔
236
        return;
1✔
237
      }
1✔
238

239
      if (qrAuthenticator.status !== QRAuthenticatorStatus.PENDING) {
11✔
240
        res.status(400).json('Session is no longer pending.');
2✔
241
        return;
2✔
242
      }
2✔
243

244
      const userOptions = UserService.getOptions({ id: req.token.user.id, allowPos: true });
6✔
245
      const user = await User.findOne(userOptions);
6✔
246
      if (!user) {
11!
247
        res.status(404).json('User not found.');
×
248
        return;
×
249
      }
✔
250
      const result = await new AuthenticationService().getSaltedToken({
6✔
251
        user,
6✔
252
        context: {
6✔
253
          roleManager: this.roleManager,
6✔
254
          tokenHandler: this.tokenHandler,
6✔
255
        },
6✔
256
        posId: req.token.posId,
6✔
257
      });
6✔
258
      const authResponse = AuthenticationService.asAuthenticationResponse(result.user, result.roles, result.organs, result.token);
5✔
259

260
      // Let the service handle all business logic validation
5✔
261
      await (new QRService()).confirm(qrAuthenticator, user);
5✔
262

263
      // Notify WebSocket clients about the confirmation
4✔
264
      WebSocketService.emitQRConfirmed(qrAuthenticator, authResponse);
4✔
265
      res.status(200).json({ message: 'QR code confirmed successfully.' });
4✔
266
    } catch (error) {
4✔
267
      this.logger.error('Could not confirm QR code:', error);
4✔
268
      res.status(500).json('Internal server error.');
4✔
269
    }
4✔
270
  }
11✔
271

272
  /**
1✔
273
   * POST /authentication/pin-secure
274
   * @summary Secure PIN authentication that requires POS user authentication
275
   * @operationId securePINAuthentication
276
   * @tags authenticate - Operations of authentication controller
277
   * @security JWT
278
   * @param {AuthenticationSecurePinRequest} request.body.required - The PIN login request with posId
279
   * @return {AuthenticationResponse} 200 - The created json web token
280
   * @return {string} 403 - Authentication error (invalid POS user or credentials)
281
   * @return {string} 500 - Internal server error
282
   */
1✔
283
  private async securePINLogin(req: RequestWithToken, res: Response): Promise<void> {
1✔
284
    const body = req.body as AuthenticationSecurePinRequest;
10✔
285
    this.logger.trace('Secure PIN authentication for user', body.userId, 'by POS user', req.token.user.id);
10✔
286

287
    try {
10✔
288
      // Verify the caller is a POS user
10✔
289
      const tokenUser = await User.findOne(UserService.getOptions({ id: req.token.user.id, allowPos: true }));
10✔
290
      if (!tokenUser || tokenUser.type !== UserType.POINT_OF_SALE) {
10✔
291
        res.status(403).json('Only POS users can use secure PIN authentication.');
1✔
292
        return;
1✔
293
      }
1✔
294

295
      // Verify the POS user's ID matches the posId in the request
9✔
296
      const pointOfSale = await PointOfSale.findOne({ where: { user: { id: tokenUser.id } } });
9✔
297
      if (!pointOfSale || pointOfSale.id !== body.posId) {
10✔
298
        res.status(403).json('POS user ID does not match the requested posId.');
3✔
299
        return;
3✔
300
      }
3✔
301

302
      // Reuse the PIN login constructor logic
6✔
303
      await (AuthenticationController.PINLoginConstructor(this.roleManager,
6✔
304
        this.tokenHandler, body.pin, body.userId, body.posId))(req, res);
6✔
305
    } catch (error) {
10!
306
      this.logger.error('Could not authenticate using secure PIN:', error);
×
307
      res.status(500).json('Internal server error.');
×
308
    }
×
309
  }
10✔
310

311
  /**
1✔
312
   * POST /authentication/nfc-secure
313
   * @summary Secure NFC authentication that requires POS user authentication
314
   * @operationId secureNfcAuthentication
315
   * @tags authenticate - Operations of authentication controller
316
   * @security JWT
317
   * @param {AuthenticationSecureNfcRequest} request.body.required - The NFC login request with posId
318
   * @return {AuthenticationResponse} 200 - The created json web token
319
   * @return {string} 403 - Authentication error (invalid POS user or credentials)
320
   * @return {string} 500 - Internal server error
321
   */
1✔
322
  private async secureNfcLogin(req: RequestWithToken, res: Response): Promise<void> {
1✔
323
    const body = req.body as AuthenticationSecureNfcRequest;
5✔
324
    this.logger.trace('Secure NFC authentication for nfcCode', body.nfcCode, 'by POS user', req.token.user.id);
5✔
325

326
    try {
5✔
327
      // Verify the caller is a POS user
5✔
328
      const tokenUser = await User.findOne(UserService.getOptions({ id: req.token.user.id, allowPos: true }));
5✔
329
      if (!tokenUser || tokenUser.type !== UserType.POINT_OF_SALE) {
5✔
330
        res.status(403).json('Only POS users can use secure NFC authentication.');
1✔
331
        return;
1✔
332
      }
1✔
333

334
      // Verify the POS user's ID matches the posId in the request
4✔
335
      const pointOfSale = await PointOfSale.findOne({ where: { user: { id: tokenUser.id } } });
4✔
336
      if (!pointOfSale || pointOfSale.id !== body.posId) {
5✔
337
        res.status(403).json('POS user ID does not match the requested posId.');
1✔
338
        return;
1✔
339
      }
1✔
340

341
      // Look up the NFC authenticator
3✔
342
      const authenticator = await NfcAuthenticator.findOne({
3✔
343
        where: { nfcCode: body.nfcCode },
3✔
344
        relations: UserService.getRelations<NfcAuthenticator>(),
3✔
345
      });
3✔
346
      if (authenticator == null || authenticator.user == null) {
5✔
347
        res.status(403).json({
1✔
348
          message: 'Invalid credentials.',
1✔
349
        });
1✔
350
        return;
1✔
351
      }
1✔
352

353
      const context: AuthenticationContext = {
2✔
354
        roleManager: this.roleManager,
2✔
355
        tokenHandler: this.tokenHandler,
2✔
356
      };
2✔
357

358
      this.logger.trace('Successful secure NFC authentication for user', authenticator.user);
2✔
359

360
      const result = await new AuthenticationService().getSaltedToken({
2✔
361
        user: authenticator.user,
2✔
362
        context,
2✔
363
        posId: body.posId,
2✔
364
      });
2✔
365
      res.json(AuthenticationService.asAuthenticationResponse(result.user, result.roles, result.organs, result.token));
2✔
366
    } catch (error) {
5!
367
      this.logger.error('Could not authenticate using secure NFC:', error);
×
368
      res.status(500).json('Internal server error.');
×
369
    }
×
370
  }
5✔
371

372
  /**
1✔
373
   * POST /authentication/ean-secure
374
   * @summary Secure EAN authentication that requires POS user authentication
375
   * @operationId secureEanAuthentication
376
   * @tags authenticate - Operations of authentication controller
377
   * @security JWT
378
   * @param {AuthenticationSecureEanRequest} request.body.required - The EAN login request with posId
379
   * @return {AuthenticationResponse} 200 - The created json web token
380
   * @return {string} 403 - Authentication error (invalid POS user or credentials)
381
   * @return {string} 500 - Internal server error
382
   */
1✔
383
  private async secureEanLogin(req: RequestWithToken, res: Response): Promise<void> {
1✔
384
    const body = req.body as AuthenticationSecureEanRequest;
4✔
385
    this.logger.trace('Secure EAN authentication for eanCode', body.eanCode, 'by POS user', req.token.user.id);
4✔
386

387
    try {
4✔
388
      // Verify the caller is a POS user
4✔
389
      const tokenUser = await User.findOne(UserService.getOptions({ id: req.token.user.id, allowPos: true }));
4✔
390
      if (!tokenUser || tokenUser.type !== UserType.POINT_OF_SALE) {
4✔
391
        res.status(403).json('Only POS users can use secure EAN authentication.');
1✔
392
        return;
1✔
393
      }
1✔
394

395
      // Verify the POS user's ID matches the posId in the request
3✔
396
      const pointOfSale = await PointOfSale.findOne({ where: { user: { id: tokenUser.id } } });
3✔
397
      if (!pointOfSale || pointOfSale.id !== body.posId) {
4✔
398
        res.status(403).json('POS user ID does not match the requested posId.');
1✔
399
        return;
1✔
400
      }
1✔
401

402
      // Look up the EAN authenticator
2✔
403
      const authenticator = await EanAuthenticator.findOne({
2✔
404
        where: { eanCode: body.eanCode },
2✔
405
        relations: UserService.getRelations<EanAuthenticator>(),
2✔
406
      });
2✔
407
      if (authenticator == null || authenticator.user == null) {
4✔
408
        res.status(403).json({
1✔
409
          message: 'Invalid credentials.',
1✔
410
        });
1✔
411
        return;
1✔
412
      }
1✔
413

414
      const context: AuthenticationContext = {
1✔
415
        roleManager: this.roleManager,
1✔
416
        tokenHandler: this.tokenHandler,
1✔
417
      };
1✔
418

419
      this.logger.trace('Successful secure EAN authentication for user', authenticator.user);
1✔
420

421
      const result = await new AuthenticationService().getSaltedToken({
1✔
422
        user: authenticator.user,
1✔
423
        context,
1✔
424
        posId: body.posId,
1✔
425
      });
1✔
426
      res.json(AuthenticationService.asAuthenticationResponse(result.user, result.roles, result.organs, result.token));
1✔
427
    } catch (error) {
4!
428
      this.logger.error('Could not authenticate using secure EAN:', error);
×
429
      res.status(500).json('Internal server error.');
×
430
    }
×
431
  }
4✔
432
}
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