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 --- shared/models/users/index.ts | 1 + .../models/users/two-factor-enable-result.model.ts | 7 ++ shared/models/users/user.model.ts | 2 + shared/server-commands/server/server.ts | 12 +++- shared/server-commands/users/index.ts | 1 + shared/server-commands/users/login-command.ts | 73 ++++++++++++++------- shared/server-commands/users/two-factor-command.ts | 75 ++++++++++++++++++++++ shared/server-commands/users/users-command.ts | 3 +- 8 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 shared/models/users/two-factor-enable-result.model.ts create mode 100644 shared/server-commands/users/two-factor-command.ts (limited to 'shared') diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts index b25978587..32f7a441c 100644 --- a/shared/models/users/index.ts +++ b/shared/models/users/index.ts @@ -1,3 +1,4 @@ +export * from './two-factor-enable-result.model' export * from './user-create-result.model' export * from './user-create.model' export * from './user-flag.model' diff --git a/shared/models/users/two-factor-enable-result.model.ts b/shared/models/users/two-factor-enable-result.model.ts new file mode 100644 index 000000000..1fc801f0a --- /dev/null +++ b/shared/models/users/two-factor-enable-result.model.ts @@ -0,0 +1,7 @@ +export interface TwoFactorEnableResult { + otpRequest: { + requestToken: string + secret: string + uri: string + } +} diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 63c5c8a92..7b6494ff8 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts @@ -62,6 +62,8 @@ export interface User { pluginAuth: string | null lastLoginDate: Date | null + + twoFactorEnabled: boolean } export interface MyUserSpecialPlaylist { diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts index a8f8c1d84..7096faf21 100644 --- a/shared/server-commands/server/server.ts +++ b/shared/server-commands/server/server.ts @@ -13,7 +13,15 @@ import { AbusesCommand } from '../moderation' import { OverviewsCommand } from '../overviews' import { SearchCommand } from '../search' import { SocketIOCommand } from '../socket' -import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users' +import { + AccountsCommand, + BlocklistCommand, + LoginCommand, + NotificationsCommand, + SubscriptionsCommand, + TwoFactorCommand, + UsersCommand +} from '../users' import { BlacklistCommand, CaptionsCommand, @@ -136,6 +144,7 @@ export class PeerTubeServer { videos?: VideosCommand videoStats?: VideoStatsCommand views?: ViewsCommand + twoFactor?: TwoFactorCommand constructor (options: { serverNumber: number } | { url: string }) { if ((options as any).url) { @@ -417,5 +426,6 @@ export class PeerTubeServer { this.videoStudio = new VideoStudioCommand(this) this.videoStats = new VideoStatsCommand(this) this.views = new ViewsCommand(this) + this.twoFactor = new TwoFactorCommand(this) } } diff --git a/shared/server-commands/users/index.ts b/shared/server-commands/users/index.ts index f6f93b4d2..1afc02dc1 100644 --- a/shared/server-commands/users/index.ts +++ b/shared/server-commands/users/index.ts @@ -5,4 +5,5 @@ export * from './login' export * from './login-command' export * from './notifications-command' export * from './subscriptions-command' +export * from './two-factor-command' export * from './users-command' diff --git a/shared/server-commands/users/login-command.ts b/shared/server-commands/users/login-command.ts index 54070e426..f2fc6d1c5 100644 --- a/shared/server-commands/users/login-command.ts +++ b/shared/server-commands/users/login-command.ts @@ -2,34 +2,27 @@ import { HttpStatusCode, PeerTubeProblemDocument } from '@shared/models' import { unwrapBody } from '../requests' import { AbstractCommand, OverrideCommandOptions } from '../shared' +type LoginOptions = OverrideCommandOptions & { + client?: { id?: string, secret?: string } + user?: { username: string, password?: string } + otpToken?: string +} + export class LoginCommand extends AbstractCommand { - login (options: OverrideCommandOptions & { - client?: { id?: string, secret?: string } - user?: { username: string, password?: string } - } = {}) { - const { client = this.server.store.client, user = this.server.store.user } = options - const path = '/api/v1/users/token' + async login (options: LoginOptions = {}) { + const res = await this._login(options) - const body = { - client_id: client.id, - client_secret: client.secret, - username: user.username, - password: user.password ?? 'password', - response_type: 'code', - grant_type: 'password', - scope: 'upload' - } + return this.unwrapLoginBody(res.body) + } - return unwrapBody<{ access_token: string, refresh_token: string } & PeerTubeProblemDocument>(this.postBodyRequest({ - ...options, + async loginAndGetResponse (options: LoginOptions = {}) { + const res = await this._login(options) - path, - requestType: 'form', - fields: body, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - })) + return { + res, + body: this.unwrapLoginBody(res.body) + } } getAccessToken (arg1?: { username: string, password?: string }): Promise @@ -129,4 +122,38 @@ export class LoginCommand extends AbstractCommand { defaultExpectedStatus: HttpStatusCode.OK_200 }) } + + private _login (options: LoginOptions) { + const { client = this.server.store.client, user = this.server.store.user, otpToken } = options + const path = '/api/v1/users/token' + + const body = { + client_id: client.id, + client_secret: client.secret, + username: user.username, + password: user.password ?? 'password', + response_type: 'code', + grant_type: 'password', + scope: 'upload' + } + + const headers = otpToken + ? { 'x-peertube-otp': otpToken } + : {} + + return this.postBodyRequest({ + ...options, + + path, + headers, + requestType: 'form', + fields: body, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + private unwrapLoginBody (body: any) { + return body as { access_token: string, refresh_token: string } & PeerTubeProblemDocument + } } diff --git a/shared/server-commands/users/two-factor-command.ts b/shared/server-commands/users/two-factor-command.ts new file mode 100644 index 000000000..6c9d270ae --- /dev/null +++ b/shared/server-commands/users/two-factor-command.ts @@ -0,0 +1,75 @@ +import { TOTP } from 'otpauth' +import { HttpStatusCode, TwoFactorEnableResult } from '@shared/models' +import { unwrapBody } from '../requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class TwoFactorCommand extends AbstractCommand { + + static buildOTP (options: { + secret: string + }) { + const { secret } = options + + return new TOTP({ + issuer: 'PeerTube', + algorithm: 'SHA1', + digits: 6, + period: 30, + secret + }) + } + + request (options: OverrideCommandOptions & { + userId: number + currentPassword: string + }) { + const { currentPassword, userId } = options + + const path = '/api/v1/users/' + userId + '/two-factor/request' + + return unwrapBody(this.postBodyRequest({ + ...options, + + path, + fields: { currentPassword }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } + + confirmRequest (options: OverrideCommandOptions & { + userId: number + requestToken: string + otpToken: string + }) { + const { userId, requestToken, otpToken } = options + + const path = '/api/v1/users/' + userId + '/two-factor/confirm-request' + + return this.postBodyRequest({ + ...options, + + path, + fields: { requestToken, otpToken }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + disable (options: OverrideCommandOptions & { + userId: number + currentPassword: string + }) { + const { userId, currentPassword } = options + const path = '/api/v1/users/' + userId + '/two-factor/disable' + + return this.postBodyRequest({ + ...options, + + path, + fields: { currentPassword }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } +} diff --git a/shared/server-commands/users/users-command.ts b/shared/server-commands/users/users-command.ts index e7d021059..811b9685b 100644 --- a/shared/server-commands/users/users-command.ts +++ b/shared/server-commands/users/users-command.ts @@ -202,7 +202,8 @@ export class UsersCommand extends AbstractCommand { token, userId: user.id, userChannelId: me.videoChannels[0].id, - userChannelName: me.videoChannels[0].name + userChannelName: me.videoChannels[0].name, + password } } -- cgit v1.2.3 From 2166c058f34dff6f91566930d12448805d829de7 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 7 Oct 2022 14:23:42 +0200 Subject: Allow admins to disable two factor auth --- shared/server-commands/users/two-factor-command.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) (limited to 'shared') diff --git a/shared/server-commands/users/two-factor-command.ts b/shared/server-commands/users/two-factor-command.ts index 6c9d270ae..5542acfda 100644 --- a/shared/server-commands/users/two-factor-command.ts +++ b/shared/server-commands/users/two-factor-command.ts @@ -21,7 +21,7 @@ export class TwoFactorCommand extends AbstractCommand { request (options: OverrideCommandOptions & { userId: number - currentPassword: string + currentPassword?: string }) { const { currentPassword, userId } = options @@ -58,7 +58,7 @@ export class TwoFactorCommand extends AbstractCommand { disable (options: OverrideCommandOptions & { userId: number - currentPassword: string + currentPassword?: string }) { const { userId, currentPassword } = options const path = '/api/v1/users/' + userId + '/two-factor/disable' @@ -72,4 +72,21 @@ export class TwoFactorCommand extends AbstractCommand { defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 }) } + + async requestAndConfirm (options: OverrideCommandOptions & { + userId: number + currentPassword?: string + }) { + const { userId, currentPassword } = options + + const { otpRequest } = await this.request({ userId, currentPassword }) + + await this.confirmRequest({ + userId, + requestToken: otpRequest.requestToken, + otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate() + }) + + return otpRequest + } } -- cgit v1.2.3