blob: f04529ee9060059eda9175dd4074007f03c14c1b [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';
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040022import { HttpStatusCode } from 'jami-web-common';
Issam E. Maghnif796a092022-10-09 20:25:26 +000023import { SignJWT } from 'jose';
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040024import { Container } from 'typedi';
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000025
Issam E. Maghnif796a092022-10-09 20:25:26 +000026import { Creds } from '../creds.js';
Misha Krieger-Raynauldaddd6fe2022-10-22 12:46:04 -040027import { Jamid } from '../jamid/jamid.js';
Issam E. Maghnif796a092022-10-09 20:25:26 +000028import { Vault } from '../vault.js';
29
30interface Credentials {
31 username?: string;
32 password?: string;
33}
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000034
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040035const jamid = Container.get(Jamid);
36const creds = Container.get(Creds);
37const vault = Container.get(Vault);
Issam E. Maghnif796a092022-10-09 20:25:26 +000038
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040039export const authRouter = Router();
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000040
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040041authRouter.post(
42 '/new-account',
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -040043 asyncHandler(async (req: Request<ParamsDictionary, string, Credentials>, res, _next) => {
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040044 const { username, password } = req.body;
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050045 if (username === undefined || password === undefined) {
46 res.status(HttpStatusCode.BadRequest).send('Missing username or password in body');
47 return;
48 }
49
50 if (password === '') {
51 res.status(HttpStatusCode.BadRequest).send('Password may not be empty');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040052 return;
53 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000054
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040055 const hashedPassword = await argon2.hash(password, { type: argon2.argon2id });
Issam E. Maghnif796a092022-10-09 20:25:26 +000056
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040057 // TODO: add JAMS support
58 // managerUri: 'https://jams.savoirfairelinux.com',
59 // managerUsername: data.username,
60 // TODO: find a way to store the password directly in Jami
61 // Maybe by using the "password" field? But as I tested, it's not
62 // returned when getting user infos.
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -040063 const accountId = await jamid.addAccount(new Map());
Issam E. Maghnif796a092022-10-09 20:25:26 +000064
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040065 // TODO: understand why the password arg in this call must be empty
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -040066 const state = await jamid.registerUsername(accountId, username, '');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040067 if (state !== 0) {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040068 jamid.removeAccount(accountId);
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040069 if (state === 2) {
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040070 res.status(HttpStatusCode.BadRequest).send('Invalid username or password');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040071 } else if (state === 3) {
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040072 res.status(HttpStatusCode.Conflict).send('Username already exists');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040073 } else {
74 throw new Error(`Unhandled state ${state}`);
75 }
76 return;
77 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000078
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040079 creds.set(username, hashedPassword);
80 await creds.save();
Issam E. Maghnif796a092022-10-09 20:25:26 +000081
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040082 res.sendStatus(HttpStatusCode.Created);
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040083 })
84);
Issam E. Maghnif796a092022-10-09 20:25:26 +000085
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040086authRouter.post(
87 '/login',
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -040088 asyncHandler(async (req: Request<ParamsDictionary, { accessToken: string } | string, Credentials>, res, _next) => {
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040089 const { username, password } = req.body;
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050090 if (username === undefined || password === undefined) {
91 res.status(HttpStatusCode.BadRequest).send('Missing username or password in body');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040092 return;
93 }
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000094
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040095 // The account may either be:
96 // 1. not found
97 // 2. found but not on this instance (but I'm not sure about this)
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040098 const accountId = jamid.getAccountIdFromUsername(username);
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040099 if (accountId === undefined) {
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -0400100 res.status(HttpStatusCode.NotFound).send('Username not found');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400101 return;
102 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000103
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400104 // TODO: load the password from Jami
105 const hashedPassword = creds.get(username);
106 if (!hashedPassword) {
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -0500107 res
108 .status(HttpStatusCode.NotFound)
109 .send('Password not found (the account does not have a password set on the server)');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400110 return;
111 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000112
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400113 const isPasswordVerified = await argon2.verify(hashedPassword, password);
114 if (!isPasswordVerified) {
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -0500115 res.status(HttpStatusCode.Unauthorized).send('Incorrect password');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400116 return;
117 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000118
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400119 const jwt = await new SignJWT({ id: accountId })
120 .setProtectedHeader({ alg: 'EdDSA' })
121 .setIssuedAt()
122 // TODO: use valid issuer and audience
123 .setIssuer('urn:example:issuer')
124 .setAudience('urn:example:audience')
125 .setExpirationTime('2h')
126 .sign(vault.privateKey);
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400127 res.send({ accessToken: jwt });
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400128 })
129);