blob: 08796a746bfb98469f82df80dc22264a796e9667 [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';
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040024import { Container } from 'typedi';
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000025
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';
Misha Krieger-Raynauldaddd6fe2022-10-22 12:46:04 -040028import { Jamid } from '../jamid/jamid.js';
Issam E. Maghnif796a092022-10-09 20:25:26 +000029import { Vault } from '../vault.js';
30
31interface Credentials {
32 username?: string;
33 password?: string;
34}
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000035
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040036const jamid = Container.get(Jamid);
37const creds = Container.get(Creds);
38const vault = Container.get(Vault);
Issam E. Maghnif796a092022-10-09 20:25:26 +000039
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040040export const authRouter = Router();
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000041
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040042authRouter.post(
43 '/new-account',
44 asyncHandler(async (req: Request<ParamsDictionary, any, Credentials>, res, _next) => {
45 const { username, password } = req.body;
46 if (!username || !password) {
47 res.status(StatusCode.BAD_REQUEST).send('Missing username or password');
48 return;
49 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000050
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040051 const hashedPassword = await argon2.hash(password, { type: argon2.argon2id });
Issam E. Maghnif796a092022-10-09 20:25:26 +000052
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040053 // TODO: add JAMS support
54 // managerUri: 'https://jams.savoirfairelinux.com',
55 // managerUsername: data.username,
56 // TODO: find a way to store the password directly in Jami
57 // Maybe by using the "password" field? But as I tested, it's not
58 // returned when getting user infos.
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040059 const { accountId } = await jamid.addAccount(new Map());
Issam E. Maghnif796a092022-10-09 20:25:26 +000060
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040061 // TODO: understand why the password arg in this call must be empty
62 const { state } = await jamid.registerUsername(accountId, username, '');
63 if (state !== 0) {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040064 jamid.removeAccount(accountId);
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040065 if (state === 2) {
66 res.status(StatusCode.BAD_REQUEST).send('Invalid username or password');
67 } else if (state === 3) {
68 res.status(StatusCode.CONFLICT).send('Username already exists');
69 } else {
70 throw new Error(`Unhandled state ${state}`);
71 }
72 return;
73 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000074
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040075 creds.set(username, hashedPassword);
76 await creds.save();
Issam E. Maghnif796a092022-10-09 20:25:26 +000077
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040078 res.sendStatus(StatusCode.CREATED);
79 })
80);
Issam E. Maghnif796a092022-10-09 20:25:26 +000081
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040082authRouter.post(
83 '/login',
84 asyncHandler(async (req: Request<ParamsDictionary, any, Credentials>, res, _next) => {
85 const { username, password } = req.body;
86 if (!username || !password) {
87 res.status(StatusCode.BAD_REQUEST).send('Missing username or password');
88 return;
89 }
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000090
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040091 // The account may either be:
92 // 1. not found
93 // 2. found but not on this instance (but I'm not sure about this)
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040094 const accountId = jamid.getAccountIdFromUsername(username);
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040095 if (accountId === undefined) {
96 res.status(StatusCode.NOT_FOUND).send('Username not found');
97 return;
98 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000099
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400100 // TODO: load the password from Jami
101 const hashedPassword = creds.get(username);
102 if (!hashedPassword) {
103 res.status(StatusCode.NOT_FOUND).send('Password not found');
104 return;
105 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000106
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400107 log.debug(jamid.getAccountDetails(accountId));
Issam E. Maghnif796a092022-10-09 20:25:26 +0000108
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400109 const isPasswordVerified = await argon2.verify(hashedPassword, password);
110 if (!isPasswordVerified) {
111 res.sendStatus(StatusCode.UNAUTHORIZED);
112 return;
113 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000114
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400115 const jwt = await new SignJWT({ id: accountId })
116 .setProtectedHeader({ alg: 'EdDSA' })
117 .setIssuedAt()
118 // TODO: use valid issuer and audience
119 .setIssuer('urn:example:issuer')
120 .setAudience('urn:example:audience')
121 .setExpirationTime('2h')
122 .sign(vault.privateKey);
123 res.json({ accessToken: jwt });
124 })
125);