From 56f47830758ff8e92abcfcc5f35d474ab12fe215 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 5 Oct 2022 15:37:15 +0200 Subject: Support two factor authentication in backend --- server/lib/auth/oauth.ts | 27 ++++++++++++++++++++++++++- server/lib/redis.ts | 25 ++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 4 deletions(-) (limited to 'server/lib') diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts index fa1887315..b541142a5 100644 --- a/server/lib/auth/oauth.ts +++ b/server/lib/auth/oauth.ts @@ -11,8 +11,20 @@ import OAuth2Server, { import { randomBytesPromise } from '@server/helpers/core-utils' import { MOAuthClient } from '@server/types/models' import { sha1 } from '@shared/extra-utils' -import { OAUTH_LIFETIME } from '../../initializers/constants' +import { HttpStatusCode } from '@shared/models' +import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' +import { isOTPValid } from '@server/helpers/otp' + +class MissingTwoFactorError extends Error { + code = HttpStatusCode.UNAUTHORIZED_401 + name = 'missing_two_factor' +} + +class InvalidTwoFactorError extends Error { + code = HttpStatusCode.BAD_REQUEST_400 + name = 'invalid_two_factor' +} /** * @@ -94,6 +106,9 @@ function handleOAuthAuthenticate ( } export { + MissingTwoFactorError, + InvalidTwoFactorError, + handleOAuthToken, handleOAuthAuthenticate } @@ -118,6 +133,16 @@ async function handlePasswordGrant (options: { const user = await getUser(request.body.username, request.body.password, bypassLogin) if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid') + if (user.otpSecret) { + if (!request.headers[OTP.HEADER_NAME]) { + throw new MissingTwoFactorError('Missing two factor header') + } + + if (isOTPValid({ secret: user.otpSecret, token: request.headers[OTP.HEADER_NAME] }) !== true) { + throw new InvalidTwoFactorError('Invalid two factor header') + } + } + const token = await buildToken() return saveToken(token, client, user, { bypassLogin }) diff --git a/server/lib/redis.ts b/server/lib/redis.ts index 9b3c72300..b7523492a 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts @@ -9,6 +9,7 @@ import { CONTACT_FORM_LIFETIME, RESUMABLE_UPLOAD_SESSION_LIFETIME, TRACKER_RATE_LIMITS, + TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME, USER_EMAIL_VERIFY_LIFETIME, USER_PASSWORD_CREATE_LIFETIME, USER_PASSWORD_RESET_LIFETIME, @@ -108,10 +109,24 @@ class Redis { return this.removeValue(this.generateResetPasswordKey(userId)) } - async getResetPasswordLink (userId: number) { + async getResetPasswordVerificationString (userId: number) { return this.getValue(this.generateResetPasswordKey(userId)) } + /* ************ Two factor auth request ************ */ + + async setTwoFactorRequest (userId: number, otpSecret: string) { + const requestToken = await generateRandomString(32) + + await this.setValue(this.generateTwoFactorRequestKey(userId, requestToken), otpSecret, TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME) + + return requestToken + } + + async getTwoFactorRequestToken (userId: number, requestToken: string) { + return this.getValue(this.generateTwoFactorRequestKey(userId, requestToken)) + } + /* ************ Email verification ************ */ async setVerifyEmailVerificationString (userId: number) { @@ -342,6 +357,10 @@ class Redis { return 'reset-password-' + userId } + private generateTwoFactorRequestKey (userId: number, token: string) { + return 'two-factor-request-' + userId + '-' + token + } + private generateVerifyEmailKey (userId: number) { return 'verify-email-' + userId } @@ -391,8 +410,8 @@ class Redis { return JSON.parse(value) } - private setObject (key: string, value: { [ id: string ]: number | string }) { - return this.setValue(key, JSON.stringify(value)) + private setObject (key: string, value: { [ id: string ]: number | string }, expirationMilliseconds?: number) { + return this.setValue(key, JSON.stringify(value), expirationMilliseconds) } private async setValue (key: string, value: string, expirationMilliseconds?: number) { -- cgit v1.2.3 From a3e5f804ad821f6979e8735b0569b1209986fedc Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 10 Oct 2022 11:12:23 +0200 Subject: Encrypt OTP secret --- server/lib/auth/oauth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'server/lib') diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts index b541142a5..35b05ec5a 100644 --- a/server/lib/auth/oauth.ts +++ b/server/lib/auth/oauth.ts @@ -9,12 +9,12 @@ import OAuth2Server, { UnsupportedGrantTypeError } from '@node-oauth/oauth2-server' import { randomBytesPromise } from '@server/helpers/core-utils' +import { isOTPValid } from '@server/helpers/otp' import { MOAuthClient } from '@server/types/models' import { sha1 } from '@shared/extra-utils' import { HttpStatusCode } from '@shared/models' import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' -import { isOTPValid } from '@server/helpers/otp' class MissingTwoFactorError extends Error { code = HttpStatusCode.UNAUTHORIZED_401 @@ -138,7 +138,7 @@ async function handlePasswordGrant (options: { throw new MissingTwoFactorError('Missing two factor header') } - if (isOTPValid({ secret: user.otpSecret, token: request.headers[OTP.HEADER_NAME] }) !== true) { + if (await isOTPValid({ encryptedSecret: user.otpSecret, token: request.headers[OTP.HEADER_NAME] }) !== true) { throw new InvalidTwoFactorError('Invalid two factor header') } } -- cgit v1.2.3