blob: b5afd367ec90e0ae66493470d56f40d0cd976e49 [file] [log] [blame]
Issam E. Maghni0ef4a362022-10-05 23:20:16 +00001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
Issam E. Maghnif796a092022-10-09 20:25:26 +000018import argon2 from 'argon2';
Misha Krieger-Raynauld708a9632022-10-14 22:55:59 -040019import { Router } from 'express';
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000020import asyncHandler from 'express-async-handler';
Issam E. Maghnif796a092022-10-09 20:25:26 +000021import { ParamsDictionary, Request } from 'express-serve-static-core';
22import { SignJWT } from 'jose';
23import log from 'loglevel';
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000024import { Service } from 'typedi';
25
Misha Krieger-Raynauld708a9632022-10-14 22:55:59 -040026import { StatusCode } from '../constants.js';
Issam E. Maghnif796a092022-10-09 20:25:26 +000027import { Creds } from '../creds.js';
28import { Jamid } from '../jamid.js';
29import { Vault } from '../vault.js';
30
31interface Credentials {
32 username?: string;
33 password?: string;
34}
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000035
36@Service()
Issam E. Maghnif796a092022-10-09 20:25:26 +000037export class AuthRouter {
38 constructor(private readonly jamid: Jamid, private readonly creds: Creds, private readonly vault: Vault) {}
39
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000040 async build() {
Misha Krieger-Raynauld708a9632022-10-14 22:55:59 -040041 const router = Router();
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000042
Issam E. Maghnif796a092022-10-09 20:25:26 +000043 const privKey = await this.vault.privKey();
44
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000045 router.post(
46 '/new-account',
Issam E. Maghnif796a092022-10-09 20:25:26 +000047 asyncHandler(async (req: Request<ParamsDictionary, any, Credentials>, res, _next) => {
48 const { username, password } = req.body;
49 if (!username || !password) {
50 res.status(StatusCode.BAD_REQUEST).send('Missing username or password');
51 return;
52 }
53
54 const hashedPassword = await argon2.hash(password, { type: argon2.argon2id });
55
56 // TODO: add JAMS support
57 // managerUri: 'https://jams.savoirfairelinux.com',
58 // managerUsername: data.username,
59 // TODO: find a way to store the password directly in Jami
60 // Maybe by using the "password" field? But as I tested, it's not
61 // returned when getting user infos.
62 const { accountId } = await this.jamid.createAccount(new Map());
63
64 // TODO: understand why the password arg in this call must be empty
65 const { state } = await this.jamid.registerUsername(accountId, username, '');
66 if (state !== 0) {
67 this.jamid.destroyAccount(accountId);
68 if (state === 2) {
69 res.status(StatusCode.BAD_REQUEST).send('Invalid username or password');
70 } else if (state === 3) {
71 res.status(StatusCode.CONFLICT).send('Username already exists');
72 } else {
73 log.error(`POST - Unhandled state ${state}`);
74 res.sendStatus(StatusCode.INTERNAL_SERVER_ERROR);
75 }
76 return;
77 }
78
79 this.creds.set(username, hashedPassword);
80 await this.creds.save();
81
82 res.sendStatus(StatusCode.CREATED);
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000083 })
84 );
85
86 router.post(
87 '/login',
Issam E. Maghnif796a092022-10-09 20:25:26 +000088 asyncHandler(async (req: Request<ParamsDictionary, any, Credentials>, res, _next) => {
89 const { username, password } = req.body;
90 if (!username || !password) {
91 res.status(StatusCode.BAD_REQUEST).send('Missing username or password');
92 return;
93 }
94
95 // The account may either be:
96 // 1. not be found
97 // 2. found but not on this instance (but I'm not sure about this)
98 const accountId = this.jamid.usernameToAccountId(username);
99 if (accountId === undefined) {
100 res.status(StatusCode.NOT_FOUND).send('Username not found');
101 return;
102 }
103
104 // TODO: load the password from Jami
105 const hashedPassword = this.creds.get(username);
106 if (!hashedPassword) {
107 res.status(StatusCode.NOT_FOUND).send('Password not found');
108 return;
109 }
110
111 log.debug(this.jamid.getAccountDetails(accountId));
112
113 const isPasswordVerified = await argon2.verify(hashedPassword, password);
114 if (!isPasswordVerified) {
115 res.sendStatus(StatusCode.UNAUTHORIZED);
116 return;
117 }
118
119 const jwt = await new SignJWT({ id: accountId })
120 .setProtectedHeader({ alg: 'EdDSA' })
121 .setIssuedAt()
122 // TODO: use valid issuer and andiance
123 .setIssuer('urn:example:issuer')
124 .setAudience('urn:example:audience')
125 .setExpirationTime('2h')
126 .sign(privKey);
127 res.json({ accessToken: jwt });
Issam E. Maghni0ef4a362022-10-05 23:20:16 +0000128 })
129 );
130
Misha Krieger-Raynauld708a9632022-10-14 22:55:59 -0400131 return router;
Issam E. Maghni0ef4a362022-10-05 23:20:16 +0000132 }
133}