From d92b8ab828b9bbb70785565384c22586416fb64b Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sun, 28 Aug 2022 16:17:15 +0300 Subject: [PATCH] totp deactivate, delete tokens on oauth deauth --- package-lock.json | 77 +++++++++++++++- package.json | 1 + src/main.ts | 15 +++ src/middleware/auth.middleware.ts | 5 +- .../api/admin/oauth2-admin.controller.ts | 3 + .../api/admin/privilege-admin.controller.ts | 3 + .../api/admin/user-admin.controller.ts | 3 + src/modules/oauth2/oauth2.service.ts | 10 +- .../oauth2-client/oauth2-client.service.ts | 2 +- .../oauth2-token/oauth2-token.service.ts | 6 +- .../user-token/user-totp-token.service.ts | 13 +++ .../oauth2-router/oauth2-router.controller.ts | 4 + .../settings/settings.controller.ts | 3 + .../settings/settings.module.ts | 6 +- .../two-factor/two-factor.controller.ts | 91 +++++++++++++++---- views/authorize.pug | 19 ++-- views/settings/security.pug | 7 +- 17 files changed, 220 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab859e9..44bd770 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@nestjs/core": "^9.0.11", "@nestjs/platform-express": "^9.0.11", "@nestjs/serve-static": "^3.0.0", + "@nestjs/swagger": "^6.1.0", "@nestjs/throttler": "^3.0.0", "bcrypt": "^5.0.1", "class-transformer": "^0.5.1", @@ -2898,6 +2899,25 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, + "node_modules/@nestjs/mapped-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.1.0.tgz", + "integrity": "sha512-+2kSly4P1QI+9eGt+/uGyPdEG1hVz7nbpqPHWZVYgoqz8eOHljpXPag+UCVRw9zo2XCu4sgNUIGe8Uk0+OvUQg==", + "peerDependencies": { + "@nestjs/common": "^7.0.8 || ^8.0.0 || ^9.0.0", + "class-transformer": "^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "class-validator": "^0.11.1 || ^0.12.0 || ^0.13.0", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "9.0.11", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.0.11.tgz", @@ -2992,6 +3012,29 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.2.5.tgz", "integrity": "sha512-l6qtdDPIkmAmzEO6egquYDfqQGPMRNGjYtrU13HAXb3YSRrt7HSb1sJY0pKp6o2bAa86tSB6iwaW2JbthPKr7Q==" }, + "node_modules/@nestjs/swagger": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-6.1.0.tgz", + "integrity": "sha512-Lflplv216nXkH6By/jMggQjpuH1V67M07tgHXUAZujAwG3LAJ1CfSvzuFckK4MAb54xQTYvFgfVPWkXqvKXzdA==", + "dependencies": { + "@nestjs/mapped-types": "1.1.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "4.14.0" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0", + "@nestjs/common": "^9.0.0", + "@nestjs/core": "^9.0.0", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "9.0.11", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.0.11.tgz", @@ -8685,8 +8728,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -11018,6 +11060,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.14.0.tgz", + "integrity": "sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw==" + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -14526,6 +14573,12 @@ } } }, + "@nestjs/mapped-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.1.0.tgz", + "integrity": "sha512-+2kSly4P1QI+9eGt+/uGyPdEG1hVz7nbpqPHWZVYgoqz8eOHljpXPag+UCVRw9zo2XCu4sgNUIGe8Uk0+OvUQg==", + "requires": {} + }, "@nestjs/platform-express": { "version": "9.0.11", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.0.11.tgz", @@ -14600,6 +14653,18 @@ } } }, + "@nestjs/swagger": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-6.1.0.tgz", + "integrity": "sha512-Lflplv216nXkH6By/jMggQjpuH1V67M07tgHXUAZujAwG3LAJ1CfSvzuFckK4MAb54xQTYvFgfVPWkXqvKXzdA==", + "requires": { + "@nestjs/mapped-types": "1.1.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "4.14.0" + } + }, "@nestjs/testing": { "version": "9.0.11", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.0.11.tgz", @@ -18979,8 +19044,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.debounce": { "version": "4.0.8", @@ -20739,6 +20803,11 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, + "swagger-ui-dist": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.14.0.tgz", + "integrity": "sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw==" + }, "symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/package.json b/package.json index 634b9af..761c3af 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@nestjs/core": "^9.0.11", "@nestjs/platform-express": "^9.0.11", "@nestjs/serve-static": "^3.0.0", + "@nestjs/swagger": "^6.1.0", "@nestjs/throttler": "^3.0.0", "bcrypt": "^5.0.1", "class-transformer": "^0.5.1", diff --git a/src/main.ts b/src/main.ts index a2ca686..250be00 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,13 +6,28 @@ import * as connectRedis from 'connect-redis'; import * as redis from 'redis'; import * as cookieParser from 'cookie-parser'; import { join } from 'path'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { NestExpressApplication } from '@nestjs/platform-express'; +import { AdminApiModule } from './modules/api/admin/admin.module'; +import { OAuth2RouterModule } from './modules/static-front-end/oauth2-router/oauth2-router.module'; dotenv.config(); async function bootstrap() { const app = await NestFactory.create(AppModule); + const config = new DocumentBuilder() + .setTitle('Icy Network Authentication Server') + .setDescription('Central authentication and management server') + .setVersion('1.0') + .addTag('admin') + .addTag('oauth2') + .build(); + const document = SwaggerModule.createDocument(app, config, { + include: [AdminApiModule, OAuth2RouterModule], + }); + SwaggerModule.setup('api', app, document); + const RedisStore = connectRedis(session); const redisClient = redis.createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379', diff --git a/src/middleware/auth.middleware.ts b/src/middleware/auth.middleware.ts index 652d490..1903ff0 100644 --- a/src/middleware/auth.middleware.ts +++ b/src/middleware/auth.middleware.ts @@ -9,10 +9,7 @@ import { NextFunction, Request, Response } from 'express'; export class AuthMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { if (!req.session.user) { - if ( - req.header('content-type')?.includes('application/json') || - req.header('accept')?.includes('application/json') - ) { + if (req.header('content-type')?.includes('application/json')) { throw new UnauthorizedException('Unauthorized'); } diff --git a/src/modules/api/admin/oauth2-admin.controller.ts b/src/modules/api/admin/oauth2-admin.controller.ts index 1cb87a0..c9d7c8a 100644 --- a/src/modules/api/admin/oauth2-admin.controller.ts +++ b/src/modules/api/admin/oauth2-admin.controller.ts @@ -15,6 +15,7 @@ import { UseGuards, UseInterceptors, } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express'; import { unlink } from 'fs/promises'; import { Privileges } from 'src/decorators/privileges.decorator'; @@ -53,6 +54,8 @@ const URL_TYPES = ['redirect_uri', 'terms', 'privacy', 'website']; const REQUIRED_CLIENT_FIELDS = ['title', 'scope', 'grants', 'activated']; +@ApiBearerAuth() +@ApiTags('admin') @Controller('/api/admin/oauth2') @UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard) export class OAuth2AdminController { diff --git a/src/modules/api/admin/privilege-admin.controller.ts b/src/modules/api/admin/privilege-admin.controller.ts index fcecc11..8af8794 100644 --- a/src/modules/api/admin/privilege-admin.controller.ts +++ b/src/modules/api/admin/privilege-admin.controller.ts @@ -6,6 +6,7 @@ import { Post, UseGuards, } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { Privileges } from 'src/decorators/privileges.decorator'; import { Scopes } from 'src/decorators/scopes.decorator'; import { OAuth2Guard } from 'src/guards/oauth2.guard'; @@ -13,6 +14,8 @@ import { PrivilegesGuard } from 'src/guards/privileges.guard'; import { ScopesGuard } from 'src/guards/scopes.guard'; import { PrivilegeService } from 'src/modules/objects/privilege/privilege.service'; +@ApiBearerAuth() +@ApiTags('admin') @Controller('/api/admin/privileges') @UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard) export class PrivilegeAdminController { diff --git a/src/modules/api/admin/user-admin.controller.ts b/src/modules/api/admin/user-admin.controller.ts index 4d9a9e5..59be8d2 100644 --- a/src/modules/api/admin/user-admin.controller.ts +++ b/src/modules/api/admin/user-admin.controller.ts @@ -11,6 +11,7 @@ import { Query, UseGuards, } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { Privileges } from 'src/decorators/privileges.decorator'; import { Scopes } from 'src/decorators/scopes.decorator'; import { OAuth2Guard } from 'src/guards/oauth2.guard'; @@ -24,6 +25,8 @@ import { PageOptions } from 'src/types/pagination.interfaces'; const RELATIONS = ['picture', 'privileges']; +@ApiBearerAuth() +@ApiTags('admin') @Controller('/api/admin/users') @UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard) export class UserAdminController { diff --git a/src/modules/oauth2/oauth2.service.ts b/src/modules/oauth2/oauth2.service.ts index 5da25b7..cee2c84 100644 --- a/src/modules/oauth2/oauth2.service.ts +++ b/src/modules/oauth2/oauth2.service.ts @@ -12,7 +12,6 @@ import { RefreshTokenAdapter } from './adapter/refresh-token.adapter'; import { UserAdapter } from './adapter/user.adapter'; const SCOPE_DESCRIPTION: Record = { - management: 'Manage Icy Network on your behalf', email: 'Email address', image: 'Profile picture', }; @@ -34,7 +33,7 @@ export class OAuth2Service { this._oauthAdapter, async (req, res, client, scope) => { const fullClient = await this.clientService.getById(client.id as string); - const allowedScopes = [...ALWAYS_AVAILABLE]; + let allowedScopes = [...ALWAYS_AVAILABLE]; let disallowedScopes = [...ALWAYS_UNAVAILABLE]; Object.keys(SCOPE_DESCRIPTION).forEach((item) => { @@ -46,10 +45,11 @@ export class OAuth2Service { }); if (scope.includes('management')) { - disallowedScopes = [ - 'THIS APPLICATION COULD ACCESS SENSITIVE INFORMATION!', - 'MAKE SURE YOU TRUST THE DEVELOPERS OF THIS APPLICATION', + allowedScopes = [ + 'Manage Icy Network on your behalf', + 'Commit administrative actions to the extent of your user privileges', ]; + disallowedScopes = null; } res.render('authorize', { diff --git a/src/modules/objects/oauth2-client/oauth2-client.service.ts b/src/modules/objects/oauth2-client/oauth2-client.service.ts index 690f493..87989ad 100644 --- a/src/modules/objects/oauth2-client/oauth2-client.service.ts +++ b/src/modules/objects/oauth2-client/oauth2-client.service.ts @@ -118,7 +118,7 @@ export class OAuth2ClientService { user: { id: user.id }, id: authId, }, - relations: ['user'], + relations: ['user', 'client'], }); } diff --git a/src/modules/objects/oauth2-token/oauth2-token.service.ts b/src/modules/objects/oauth2-token/oauth2-token.service.ts index f041d66..520cb8a 100644 --- a/src/modules/objects/oauth2-token/oauth2-token.service.ts +++ b/src/modules/objects/oauth2-token/oauth2-token.service.ts @@ -64,9 +64,13 @@ export class OAuth2TokenService { }); } - public async wipeClientTokens(client: OAuth2Client): Promise { + public async wipeClientTokens( + client: OAuth2Client, + user?: User, + ): Promise { await this.tokenRepository.delete({ client: { id: client.id }, + user: user ? { id: user.id } : undefined, }); } diff --git a/src/modules/objects/user-token/user-totp-token.service.ts b/src/modules/objects/user-token/user-totp-token.service.ts index 562be2d..aa81552 100644 --- a/src/modules/objects/user-token/user-totp-token.service.ts +++ b/src/modules/objects/user-token/user-totp-token.service.ts @@ -34,6 +34,7 @@ export class UserTOTPService { public async getUserTOTP(user: User): Promise { return this.userTokenRepository.findOne({ where: { user: { id: user.id }, type: UserTokenType.TOTP }, + relations: ['user'], }); } @@ -68,4 +69,16 @@ export class UserTOTPService { return [totp, recovery]; } + + public async deactivateTOTP(token: UserToken): Promise { + if (!token) { + return; + } + + await this.userTokenRepository.delete({ + type: UserTokenType.RECOVERY, + user: { id: token.user.id }, + }); + await this.userTokenRepository.remove(token); + } } diff --git a/src/modules/static-front-end/oauth2-router/oauth2-router.controller.ts b/src/modules/static-front-end/oauth2-router/oauth2-router.controller.ts index 600b3ed..2d7f2be 100644 --- a/src/modules/static-front-end/oauth2-router/oauth2-router.controller.ts +++ b/src/modules/static-front-end/oauth2-router/oauth2-router.controller.ts @@ -8,6 +8,7 @@ import { Res, UseGuards, } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { NextFunction, Request, Response } from 'express'; import { Scope } from 'src/decorators/scope.decorator'; import { CurrentUser } from 'src/decorators/user.decorator'; @@ -16,6 +17,7 @@ import { ConfigurationService } from 'src/modules/config/config.service'; import { User } from 'src/modules/objects/user/user.entity'; import { OAuth2Service } from '../../oauth2/oauth2.service'; +@ApiTags('oauth2') @Controller('oauth2') export class OAuth2Controller { constructor( @@ -52,6 +54,7 @@ export class OAuth2Controller { return this._service.oauth.controller.token(req, res, next); } + @ApiBearerAuth() @Post('introspect') public introspectWrapper( @Req() req: Request, @@ -64,6 +67,7 @@ export class OAuth2Controller { // User information endpoint // TODO: Move to API + @ApiBearerAuth() @Get('user') @UseGuards(OAuth2Guard) public async userInfo( diff --git a/src/modules/static-front-end/settings/settings.controller.ts b/src/modules/static-front-end/settings/settings.controller.ts index cdfb2e2..ded128d 100644 --- a/src/modules/static-front-end/settings/settings.controller.ts +++ b/src/modules/static-front-end/settings/settings.controller.ts @@ -19,6 +19,7 @@ import { Throttle } from '@nestjs/throttler'; import { Request, Response } from 'express'; import { unlink } from 'fs/promises'; import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service'; +import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service'; import { UploadService } from 'src/modules/objects/upload/upload.service'; import { UserTOTPService } from 'src/modules/objects/user-token/user-totp-token.service'; import { UserService } from 'src/modules/objects/user/user.service'; @@ -36,6 +37,7 @@ export class SettingsController { private readonly _user: UserService, private readonly _totp: UserTOTPService, private readonly _client: OAuth2ClientService, + private readonly _oaToken: OAuth2TokenService, ) {} @Get() @@ -162,6 +164,7 @@ export class SettingsController { return; } + await this._oaToken.wipeClientTokens(getAuth.client, req.user); await this._client.revokeAuthorization(getAuth); if (jsreq) { diff --git a/src/modules/static-front-end/settings/settings.module.ts b/src/modules/static-front-end/settings/settings.module.ts index ab300ae..70b3aa7 100644 --- a/src/modules/static-front-end/settings/settings.module.ts +++ b/src/modules/static-front-end/settings/settings.module.ts @@ -15,11 +15,12 @@ import { ConfigurationModule } from 'src/modules/config/config.module'; import { ConfigurationService } from 'src/modules/config/config.service'; import { UploadModule } from 'src/modules/objects/upload/upload.module'; import { UserModule } from 'src/modules/objects/user/user.module'; +import { OAuth2TokenModule } from 'src/modules/objects/oauth2-token/oauth2-token.module'; +import { OAuth2ClientModule } from 'src/modules/objects/oauth2-client/oauth2-client.module'; +import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module'; import { OAuth2Module } from '../../oauth2/oauth2.module'; import { SettingsController } from './settings.controller'; import { SettingsService } from './settings.service'; -import { OAuth2ClientModule } from 'src/modules/objects/oauth2-client/oauth2-client.module'; -import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module'; @Module({ controllers: [SettingsController], @@ -30,6 +31,7 @@ import { UserTokenModule } from 'src/modules/objects/user-token/user-token.modul UserTokenModule, OAuth2Module, OAuth2ClientModule, + OAuth2TokenModule, MulterModule.registerAsync({ imports: [ConfigurationModule], useFactory: async (config: ConfigurationService) => { diff --git a/src/modules/static-front-end/two-factor/two-factor.controller.ts b/src/modules/static-front-end/two-factor/two-factor.controller.ts index 5db1192..614ad49 100644 --- a/src/modules/static-front-end/two-factor/two-factor.controller.ts +++ b/src/modules/static-front-end/two-factor/two-factor.controller.ts @@ -1,7 +1,7 @@ -import { Body, Controller, Get, Post, Req, Res, Session } from '@nestjs/common'; +import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common'; import { Request, Response } from 'express'; -import { SessionData } from 'express-session'; import { UserTOTPService } from 'src/modules/objects/user-token/user-totp-token.service'; +import { UserService } from 'src/modules/objects/user/user.service'; import { FormUtilityService } from 'src/modules/utility/services/form-utility.service'; import { QRCodeService } from 'src/modules/utility/services/qr-code.service'; import { TokenService } from 'src/modules/utility/services/token.service'; @@ -12,30 +12,31 @@ export class TwoFactorController { private totp: UserTOTPService, private qr: QRCodeService, private token: TokenService, + private user: UserService, private form: FormUtilityService, ) {} @Get('activate') - public async twoFAStatus( - @Session() session: SessionData, - @Req() req: Request, - @Res() res: Response, - ) { + public async twoFAStatus(@Req() req: Request, @Res() res: Response) { const twoFA = await this.totp.getUserTOTP(req.user); let secret: string; if (!twoFA) { - if (session.challenge) { - const challenge = await this.token.decryptChallenge(session.challenge); - if (challenge.type === 'totp') { + const challengeString = req.query.challenge as string; + if (challengeString) { + const challenge = await this.token.decryptChallenge(challengeString); + if (challenge.type === 'totp' && challenge.user === req.user.uuid) { secret = challenge.secret; } } if (!secret) { secret = this.totp.createTOTPSecret(); - const challenge = { type: 'totp', secret }; - session.challenge = await this.token.encryptChallenge(challenge); + const challenge = { type: 'totp', secret, user: req.user.uuid }; + const encrypted = await this.token.encryptChallenge(challenge); + const cleanURL = req.originalUrl.replace(/\?(.*)$/, ''); + res.redirect(`${cleanURL}?challenge=${encrypted}`); + return; } const url = this.totp.getTOTPURL(secret, req.user.username); @@ -54,21 +55,25 @@ export class TwoFactorController { @Post('activate') public async twoFAActivate( - @Session() session: SessionData, @Body() body: { code: string }, @Req() req: Request, @Res() res: Response, ) { let secret: string; try { - if (!session.challenge || !body.code) { + const challengeString = req.query.challenge as string; + if (!challengeString || !body.code) { throw new Error('Invalid request'); } - const challenge = await this.token.decryptChallenge(session.challenge); + const challenge = await this.token.decryptChallenge(challengeString); secret = challenge.secret; - if (challenge.type !== 'totp' || !secret) { + if ( + challenge.type !== 'totp' || + challenge.user !== req.user.uuid || + !secret + ) { throw new Error('Invalid request'); } @@ -87,7 +92,59 @@ export class TwoFactorController { // TODO: show the recovery tokens to the user await this.totp.activateTOTP(req.user, secret); - session.challenge = null; + req.flash('message', { + error: false, + text: 'Two-factor authenticator has been enabled successfully. Your account is now more secure!', + }); + res.redirect('/'); + } + + @Get('disable') + public async disableTwoFA(@Req() req: Request, @Res() res: Response) { + const twoFA = await this.totp.getUserTOTP(req.user); + if (!twoFA) { + return res.redirect('/'); + } + + res.render('password', this.form.populateTemplate(req)); + } + + @Post('disable') + public async disableTwoFAPost( + @Req() req: Request, + @Res() res: Response, + @Body() body: { password: string }, + ) { + const twoFA = await this.totp.getUserTOTP(req.user); + if (!twoFA) { + return res.redirect('/'); + } + + try { + if (!body.password) { + throw new Error('Please enter your password'); + } + + if ( + !(await this.user.comparePasswords(req.user.password, body.password)) + ) { + throw new Error('The entered password is invalid.'); + } + + await this.totp.deactivateTOTP(twoFA); + } catch (e: any) { + req.flash('message', { + error: true, + text: e.message, + }); + res.redirect('/account/two-factor/disable'); + return; + } + + req.flash('message', { + error: false, + text: 'Two-factor authenticator has been disabled successfully. Your account is now less secure!!', + }); res.redirect('/'); } } diff --git a/views/authorize.pug b/views/authorize.pug index 3450067..24581be 100644 --- a/views/authorize.pug +++ b/views/authorize.pug @@ -38,18 +38,19 @@ block body if url.type == 'terms' a.authorize__client-url(href=url.url, target="_blank", rel="nofollow") Terms of Service - h2.text-center This application will have access to.. + if allowedScopes + h2.text-center This application will have access to.. - .scopes - each allowed in allowedScopes - span.scopes__scope.scopes__scope--allowed #{allowed} + .scopes + each allowed in allowedScopes + span.scopes__scope.scopes__scope--allowed #{allowed} + if disallowedScopes + h2.text-center This application will not have access to.. - h2.text-center This application will not have access to.. - - .scopes - each allowed in disallowedScopes - span.scopes__scope.scopes__scope--disallowed #{allowed} + .scopes + each allowed in disallowedScopes + span.scopes__scope.scopes__scope--disallowed #{allowed} form(method="POST", action="") div.form-container diff --git a/views/settings/security.pug b/views/settings/security.pug index 704e90f..cd5e059 100644 --- a/views/settings/security.pug +++ b/views/settings/security.pug @@ -42,9 +42,6 @@ block settings p Two-factor authentication is enabled. a.btn.btn-primary(href="/account/two-factor/disable") Disable else - p You can enable two-factor authentication using an authenticator app of your choice, such as - b Google Authenticator - | or - b andOTP - |. + p You can enable two-factor authentication using an authenticator app of your choice, such as Google Authenticator. + | By clicking activate you will be prompted with a QR code which you will need to scan with such app. a.btn.btn-primary(href="/account/two-factor/activate") Activate