blob: 086f640b31b10ca4e33c7ae17ff6380c89c82d13 [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;
45 if (!username || !password) {
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040046 res.status(HttpStatusCode.BadRequest).send('Missing username or password');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040047 return;
48 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000049
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040050 const hashedPassword = await argon2.hash(password, { type: argon2.argon2id });
Issam E. Maghnif796a092022-10-09 20:25:26 +000051
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040052 // TODO: add JAMS support
53 // managerUri: 'https://jams.savoirfairelinux.com',
54 // managerUsername: data.username,
55 // TODO: find a way to store the password directly in Jami
56 // Maybe by using the "password" field? But as I tested, it's not
57 // returned when getting user infos.
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -040058 const accountId = await jamid.addAccount(new Map());
Issam E. Maghnif796a092022-10-09 20:25:26 +000059
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040060 // TODO: understand why the password arg in this call must be empty
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -040061 const state = await jamid.registerUsername(accountId, username, '');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040062 if (state !== 0) {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040063 jamid.removeAccount(accountId);
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040064 if (state === 2) {
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040065 res.status(HttpStatusCode.BadRequest).send('Invalid username or password');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040066 } else if (state === 3) {
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040067 res.status(HttpStatusCode.Conflict).send('Username already exists');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040068 } else {
69 throw new Error(`Unhandled state ${state}`);
70 }
71 return;
72 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000073
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040074 creds.set(username, hashedPassword);
75 await creds.save();
Issam E. Maghnif796a092022-10-09 20:25:26 +000076
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040077 res.sendStatus(HttpStatusCode.Created);
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040078 })
79);
Issam E. Maghnif796a092022-10-09 20:25:26 +000080
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040081authRouter.post(
82 '/login',
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -040083 asyncHandler(async (req: Request<ParamsDictionary, { accessToken: string } | string, Credentials>, res, _next) => {
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040084 const { username, password } = req.body;
85 if (!username || !password) {
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040086 res.status(HttpStatusCode.BadRequest).send('Missing username or password');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040087 return;
88 }
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000089
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040090 // The account may either be:
91 // 1. not found
92 // 2. found but not on this instance (but I'm not sure about this)
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040093 const accountId = jamid.getAccountIdFromUsername(username);
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040094 if (accountId === undefined) {
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -040095 res.status(HttpStatusCode.NotFound).send('Username not found');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040096 return;
97 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000098
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040099 // TODO: load the password from Jami
100 const hashedPassword = creds.get(username);
101 if (!hashedPassword) {
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -0400102 res.status(HttpStatusCode.NotFound).send('Password not found');
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400103 return;
104 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000105
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400106 const isPasswordVerified = await argon2.verify(hashedPassword, password);
107 if (!isPasswordVerified) {
Misha Krieger-Raynauld2f5d1ce2022-10-23 21:13:33 -0400108 res.sendStatus(HttpStatusCode.Unauthorized);
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400109 return;
110 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000111
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400112 const jwt = await new SignJWT({ id: accountId })
113 .setProtectedHeader({ alg: 'EdDSA' })
114 .setIssuedAt()
115 // TODO: use valid issuer and audience
116 .setIssuer('urn:example:issuer')
117 .setAudience('urn:example:audience')
118 .setExpirationTime('2h')
119 .sign(vault.privateKey);
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400120 res.send({ accessToken: jwt });
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -0400121 })
122);