blob: b5b239930b2f7fd9a0706ae77c01aba761302424 [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-Raynauldcfa44302022-11-30 18:36:36 -050022import { AccessToken, AccountDetails, HttpStatusCode, UserCredentials } from 'jami-web-common';
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040023import { Container } from 'typedi';
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000024
Misha Krieger-Raynauldaddd6fe2022-10-22 12:46:04 -040025import { Jamid } from '../jamid/jamid.js';
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -050026import { NameRegistrationEndedState, RegistrationState } from '../jamid/state-enums.js';
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -050027import { Accounts } from '../storage/accounts.js';
28import { signJwt } from '../utils/jwt.js';
Issam E. Maghnif796a092022-10-09 20:25:26 +000029
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040030const jamid = Container.get(Jamid);
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -050031const accounts = Container.get(Accounts);
Issam E. Maghnif796a092022-10-09 20:25:26 +000032
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040033export const authRouter = Router();
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000034
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040035authRouter.post(
36 '/new-account',
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050037 asyncHandler(async (req: Request<ParamsDictionary, string, Partial<UserCredentials>>, res) => {
Issam E. Maghnib05ad992022-11-20 23:50:48 +000038 const { username, password, isJams } = req.body;
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050039 if (username === undefined || password === undefined) {
40 res.status(HttpStatusCode.BadRequest).send('Missing username or password in body');
41 return;
42 }
43
Issam E. Maghnib05ad992022-11-20 23:50:48 +000044 const isAccountAlreadyCreated = accounts.get(username, isJams) !== undefined;
45 if (isAccountAlreadyCreated) {
46 res.status(HttpStatusCode.Conflict).send('Username already exists locally');
47 return;
48 }
49
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050050 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
Issam E. Maghni5bc33a32022-11-22 21:36:19 +000057 const accountDetails: Partial<AccountDetails> = {
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050058 // TODO: Enable encrypted archives
Issam E. Maghni5bc33a32022-11-22 21:36:19 +000059 // 'Account.archivePassword': password
60 };
Issam E. Maghnib05ad992022-11-20 23:50:48 +000061 if (isJams) {
62 accountDetails['Account.managerUri'] = 'https://jams.savoirfairelinux.com/';
63 accountDetails['Account.managerUsername'] = username;
64 }
65 const { accountId, state } = await jamid.addAccount(accountDetails);
Issam E. Maghnif796a092022-10-09 20:25:26 +000066
Issam E. Maghnib05ad992022-11-20 23:50:48 +000067 if (isJams) {
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -050068 if (state === RegistrationState.ErrorGeneric) {
Issam E. Maghnib05ad992022-11-20 23:50:48 +000069 jamid.removeAccount(accountId);
70 res.status(HttpStatusCode.Unauthorized).send('Invalid JAMS credentials');
71 return;
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040072 }
Issam E. Maghnib05ad992022-11-20 23:50:48 +000073 } else {
74 const state = await jamid.registerUsername(accountId, username, '');
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -050075 if (state !== NameRegistrationEndedState.Success) {
Issam E. Maghnib05ad992022-11-20 23:50:48 +000076 jamid.removeAccount(accountId);
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -050077 switch (state) {
78 case NameRegistrationEndedState.InvalidName:
79 res.status(HttpStatusCode.BadRequest).send('Invalid username or password');
80 break;
81 case NameRegistrationEndedState.AlreadyTaken:
82 res.status(HttpStatusCode.Conflict).send('Username already exists');
83 break;
84 default:
85 throw new Error(`Unhandled state ${state}`);
Issam E. Maghnib05ad992022-11-20 23:50:48 +000086 }
87 return;
88 }
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040089 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000090
Issam E. Maghnib05ad992022-11-20 23:50:48 +000091 accounts.set(username, hashedPassword, isJams);
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -050092 await accounts.save();
Issam E. Maghnif796a092022-10-09 20:25:26 +000093
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040094 res.sendStatus(HttpStatusCode.Created);
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040095 })
96);
Issam E. Maghnif796a092022-10-09 20:25:26 +000097
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040098authRouter.post(
99 '/login',
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500100 asyncHandler(async (req: Request<ParamsDictionary, AccessToken | string, Partial<UserCredentials>>, res) => {
101 const { username, password, isJams } = req.body;
102 if (username === undefined || password === undefined) {
103 res.status(HttpStatusCode.BadRequest).send('Missing username or password in body');
104 return;
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500105 }
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500106
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500107 // Check if the account is stored on this daemon instance
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500108 const accountId = jamid.getAccountIdFromUsername(username);
109 if (accountId === undefined) {
idillon440e5302023-08-03 20:25:24 -0400110 res.status(HttpStatusCode.Unauthorized).send('Username not found');
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500111 return;
112 }
113
114 const hashedPassword = accounts.get(username, isJams);
115 if (hashedPassword === undefined) {
116 res
idillon440e5302023-08-03 20:25:24 -0400117 .status(HttpStatusCode.Unauthorized)
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500118 .send('Password not found (the account does not have a password set on the server)');
119 return;
120 }
121
122 const isPasswordVerified = await argon2.verify(hashedPassword, password);
123 if (!isPasswordVerified) {
124 res.status(HttpStatusCode.Unauthorized).send('Incorrect password');
125 return;
126 }
127
128 const jwt = await signJwt(accountId);
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500129 res.status(HttpStatusCode.Ok).send({ accessToken: jwt });
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500130 })
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400131);