From f4046e0fc3d9fbcef3a40b75919baa483e63977c Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 20 Jan 2020 01:00:00 +0100 Subject: [PATCH] Add option to add a mail whitelist and disable sign up --- Readme.md | 8 ++++++++ src/api/auth.ts | 8 +++++--- src/api/parent.ts | 15 ++++++++++++--- src/config.ts | 37 +++++++++++++++++++++++++++++++++++++ src/util/mail.ts | 30 +++++++++++++++++++++++++++++- 5 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 src/config.ts diff --git a/Readme.md b/Readme.md index 2fe6008..642a31a 100644 --- a/Readme.md +++ b/Readme.md @@ -60,6 +60,14 @@ This fixes the causes of lint warnings (where possible). - MAIL_SERVER_BLACKLIST - list of domains, separated by comma - if the user tries to use such a mail service, then he will get the notification that this provider is not supported +- MAIL_WHITELIST + - list of mail addresses (``someone@somewhere.com``) or domains (``mailbox.org``), separated by comma + - if a user requests signing in with a mail address which is not in this list, then the request is rejected + - if the list is empty, then any mail address (except with domains from the blacklist) is allowed + - note: this allows a third party who knows the server url to check if a certain mail address is allowed by trying to sign in with it +- DISABLE_SIGNUP + - ``yes`` or ``no`` (default: no) + - disables creating new families if ``yes`` is selected ## HTTPS diff --git a/src/api/auth.ts b/src/api/auth.ts index fb4b588..985c598 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -20,7 +20,7 @@ import { Router } from 'express' import { BadRequest } from 'http-errors' import { Database } from '../database' import { sendLoginCode, signInByMailCode } from '../function/authentication/login-by-mail' -import { isMailServerBlacklisted, sanitizeMailAddress } from '../util/mail' +import { isMailAddressCoveredByWhitelist, isMailServerBlacklisted, sanitizeMailAddress } from '../util/mail' import { isSendMailLoginCodeRequest, isSignInByMailCodeRequest @@ -41,7 +41,9 @@ export const createAuthRouter = (database: Database) => { throw new BadRequest() } - if (isMailServerBlacklisted(mail)) { + if (!isMailAddressCoveredByWhitelist(mail)) { + res.json({ mailAddressNotWhitelisted: true }) + } else if (isMailServerBlacklisted(mail)) { res.json({ mailServerBlacklisted: true }) } else { const { mailLoginToken } = await sendLoginCode({ diff --git a/src/api/parent.ts b/src/api/parent.ts index ef534cd..04ea3aa 100644 --- a/src/api/parent.ts +++ b/src/api/parent.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -17,7 +17,8 @@ import { json } from 'body-parser' import { Router } from 'express' -import { BadRequest, Unauthorized } from 'http-errors' +import { BadRequest, Forbidden, Unauthorized } from 'http-errors' +import { config } from '../config' import { Database } from '../database' import { removeDevice } from '../function/device/remove-device' import { canRecoverPassword } from '../function/parent/can-recover-password' @@ -47,7 +48,11 @@ export const createParentRouter = ({ database, websocket }: {database: Database, const { mailAuthToken } = req.body const { status, mail } = await getStatusByMailToken({ database, mailAuthToken }) - res.json({ status, mail }) + res.json({ + status, + mail, + canCreateFamily: !config.disableSignup + }) } catch (ex) { next(ex) } @@ -55,6 +60,10 @@ export const createParentRouter = ({ database, websocket }: {database: Database, router.post('/create-family', json(), async (req, res, next) => { try { + if (config.disableSignup) { + throw new Forbidden() + } + if (!isCreateFamilyByMailTokenRequest(req.body)) { throw new BadRequest() } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..508c89f --- /dev/null +++ b/src/config.ts @@ -0,0 +1,37 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// note: soem configuration parameters are read directly at the location where they are used +interface Config { + mailWhitelist: Array + disableSignup: boolean +} + +function parseYesNo (value: string) { + if (value === 'yes') { + return true + } else if (value === 'no') { + return false + } else { + throw new Error('invalid value "' + value + '", expected "" or "no"') + } +} + +export const config: Config = { + mailWhitelist: (process.env.MAIL_WHITELIST || '').split(',').map((item) => item.trim()).filter((item) => item.length > 0), + disableSignup: parseYesNo(process.env.DISABLE_SIGNUP || 'no') +} diff --git a/src/util/mail.ts b/src/util/mail.ts index 9a32002..989ef75 100644 --- a/src/util/mail.ts +++ b/src/util/mail.ts @@ -18,6 +18,7 @@ import { parseOneAddress } from 'email-addresses' import * as Email from 'email-templates' import { join } from 'path' +import { config } from '../config' const mailimprint = process.env.MAIL_IMPRINT || 'not defined' const mailServerBlacklist = (process.env.MAIL_SERVER_BLACKLIST || '').split(',').filter((item) => !!item) @@ -84,13 +85,40 @@ export const sendUninstallWarningMail = async ({ receiver, deviceName }: { }) } -export function isMailServerBlacklisted (mail: string) { +export function isMailServerBlacklisted (mail: string): boolean { const parts = mail.split('@') const domain = parts[parts.length - 1] return mailServerBlacklist.indexOf(domain.toLowerCase()) !== -1 } +export function isMailAddressCoveredByWhitelist (mail: string): boolean { + if (config.mailWhitelist.length === 0) { + return true + } + + const mailParts = mail.split('@') + const mailDomain = mailParts[mailParts.length - 1] + + for (let i = 0; i < config.mailWhitelist.length; i++) { + const whtielistItem = config.mailWhitelist[i] + + const isDomain = whtielistItem.indexOf('@') === -1 + + if (isDomain) { + if (mailDomain === whtielistItem) { + return true + } + } else { + if (mail === whtielistItem) { + return true + } + } + } + + return false +} + export function sanitizeMailAddress (input: string): string | null { const parsed = parseOneAddress(input)