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

GEWIS / sudosos-backend / 21245630223

22 Jan 2026 10:42AM UTC coverage: 89.612% (+0.01%) from 89.601%
21245630223

push

github

web-flow
ci: fix backticks in workflow (#718)

1695 of 2043 branches covered (82.97%)

Branch coverage included in aggregate %.

8726 of 9586 relevant lines covered (91.03%)

1009.57 hits per line

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

90.42
/src/controller/authentication-secure-controller.ts
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
 */
20

21
/**
22
 * @module internal/controllers
23
 */
24

25
import { Response } from 'express';
26
import log4js, { Logger } from 'log4js';
2✔
27
import BaseController, { BaseControllerOptions } from './base-controller';
2✔
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';
2✔
32
import PointOfSaleController from './point-of-sale-controller';
2✔
33
import PointOfSale from '../entity/point-of-sale/point-of-sale';
2✔
34
import ServerSettingsStore from '../server-settings/server-settings-store';
2✔
35
import { ISettings } from '../entity/server-setting';
36
import { QRAuthenticatorStatus } from '../entity/authenticator/qr-authenticator';
2✔
37
import WebSocketService from '../service/websocket-service';
2✔
38
import QRService from '../service/qr-service';
2✔
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';
2✔
43
import AuthenticationController from './authentication-controller';
2✔
44
import AuthenticationService from '../service/authentication-service';
2✔
45
import NfcAuthenticator from '../entity/authenticator/nfc-authenticator';
2✔
46
import EanAuthenticator from '../entity/authenticator/ean-authenticator';
2✔
47
import { AuthenticationContext } from '../service/authentication-service';
48
import UserService from '../service/user-service';
2✔
49

50
/**
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
 */
62
export default class AuthenticationSecureController extends BaseController {
2✔
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 secure 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(options: BaseControllerOptions, tokenHandler: TokenHandler) {
76
    super(options);
3✔
77
    this.logger.level = process.env.LOG_LEVEL;
3✔
78
    this.tokenHandler = tokenHandler;
3✔
79
  }
80

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

130
  /**
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
   */
138
  private async refreshToken(req: RequestWithToken, res: Response): Promise<void> {
139
    this.logger.trace('Refresh token for user', req.token.user.id);
2✔
140

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

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

151
      if (req.token.posId) {
2✔
152
        expiry = ServerSettingsStore.getInstance().getSetting('jwtExpiryPointOfSale') as ISettings['jwtExpiryPointOfSale'];
1✔
153
      }
154

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

171
  /**
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
   */
182
  private async authenticatePointOfSale(req: RequestWithToken, res: Response): Promise<void> {
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
      }
193

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

210
  /**
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
   */
223
  private async confirmQRCode(req: RequestWithToken, res: Response): Promise<void> {
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) {
10✔
230
        res.status(404).json('Session not found.');
1✔
231
        return;
1✔
232
      }
233

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

239
      if (qrAuthenticator.status !== QRAuthenticatorStatus.PENDING) {
8✔
240
        res.status(400).json('Session is no longer pending.');
2✔
241
        return;
2✔
242
      }
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) {
6!
247
        res.status(404).json('User not found.');
×
248
        return;
×
249
      }
250
      const token = await new AuthenticationService().getSaltedToken({
6✔
251
        user,
252
        context: {
253
          roleManager: this.roleManager,
254
          tokenHandler: this.tokenHandler,
255
        },
256
        posId: req.token.posId,
257
      });
258

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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