totp deactivate, delete tokens on oauth deauth

This commit is contained in:
Evert Prants 2022-08-28 16:17:15 +03:00
parent 7a06c882ac
commit d92b8ab828
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
17 changed files with 220 additions and 48 deletions

77
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@nestjs/core": "^9.0.11", "@nestjs/core": "^9.0.11",
"@nestjs/platform-express": "^9.0.11", "@nestjs/platform-express": "^9.0.11",
"@nestjs/serve-static": "^3.0.0", "@nestjs/serve-static": "^3.0.0",
"@nestjs/swagger": "^6.1.0",
"@nestjs/throttler": "^3.0.0", "@nestjs/throttler": "^3.0.0",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
@ -2898,6 +2899,25 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" "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": { "node_modules/@nestjs/platform-express": {
"version": "9.0.11", "version": "9.0.11",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.0.11.tgz", "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", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.2.5.tgz",
"integrity": "sha512-l6qtdDPIkmAmzEO6egquYDfqQGPMRNGjYtrU13HAXb3YSRrt7HSb1sJY0pKp6o2bAa86tSB6iwaW2JbthPKr7Q==" "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": { "node_modules/@nestjs/testing": {
"version": "9.0.11", "version": "9.0.11",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.0.11.tgz", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.0.11.tgz",
@ -8685,8 +8728,7 @@
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"dev": true
}, },
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
@ -11018,6 +11060,11 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/symbol-observable": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", "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": { "@nestjs/platform-express": {
"version": "9.0.11", "version": "9.0.11",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.0.11.tgz", "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": { "@nestjs/testing": {
"version": "9.0.11", "version": "9.0.11",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.0.11.tgz", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.0.11.tgz",
@ -18979,8 +19044,7 @@
"lodash": { "lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"dev": true
}, },
"lodash.debounce": { "lodash.debounce": {
"version": "4.0.8", "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", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" "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": { "symbol-observable": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",

View File

@ -29,6 +29,7 @@
"@nestjs/core": "^9.0.11", "@nestjs/core": "^9.0.11",
"@nestjs/platform-express": "^9.0.11", "@nestjs/platform-express": "^9.0.11",
"@nestjs/serve-static": "^3.0.0", "@nestjs/serve-static": "^3.0.0",
"@nestjs/swagger": "^6.1.0",
"@nestjs/throttler": "^3.0.0", "@nestjs/throttler": "^3.0.0",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",

View File

@ -6,13 +6,28 @@ import * as connectRedis from 'connect-redis';
import * as redis from 'redis'; import * as redis from 'redis';
import * as cookieParser from 'cookie-parser'; import * as cookieParser from 'cookie-parser';
import { join } from 'path'; import { join } from 'path';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { NestExpressApplication } from '@nestjs/platform-express'; 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(); dotenv.config();
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule); const app = await NestFactory.create<NestExpressApplication>(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 RedisStore = connectRedis(session);
const redisClient = redis.createClient({ const redisClient = redis.createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379', url: process.env.REDIS_URL || 'redis://localhost:6379',

View File

@ -9,10 +9,7 @@ import { NextFunction, Request, Response } from 'express';
export class AuthMiddleware implements NestMiddleware { export class AuthMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) { use(req: Request, res: Response, next: NextFunction) {
if (!req.session.user) { if (!req.session.user) {
if ( if (req.header('content-type')?.includes('application/json')) {
req.header('content-type')?.includes('application/json') ||
req.header('accept')?.includes('application/json')
) {
throw new UnauthorizedException('Unauthorized'); throw new UnauthorizedException('Unauthorized');
} }

View File

@ -15,6 +15,7 @@ import {
UseGuards, UseGuards,
UseInterceptors, UseInterceptors,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { unlink } from 'fs/promises'; import { unlink } from 'fs/promises';
import { Privileges } from 'src/decorators/privileges.decorator'; 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']; const REQUIRED_CLIENT_FIELDS = ['title', 'scope', 'grants', 'activated'];
@ApiBearerAuth()
@ApiTags('admin')
@Controller('/api/admin/oauth2') @Controller('/api/admin/oauth2')
@UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard) @UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard)
export class OAuth2AdminController { export class OAuth2AdminController {

View File

@ -6,6 +6,7 @@ import {
Post, Post,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Privileges } from 'src/decorators/privileges.decorator'; import { Privileges } from 'src/decorators/privileges.decorator';
import { Scopes } from 'src/decorators/scopes.decorator'; import { Scopes } from 'src/decorators/scopes.decorator';
import { OAuth2Guard } from 'src/guards/oauth2.guard'; 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 { ScopesGuard } from 'src/guards/scopes.guard';
import { PrivilegeService } from 'src/modules/objects/privilege/privilege.service'; import { PrivilegeService } from 'src/modules/objects/privilege/privilege.service';
@ApiBearerAuth()
@ApiTags('admin')
@Controller('/api/admin/privileges') @Controller('/api/admin/privileges')
@UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard) @UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard)
export class PrivilegeAdminController { export class PrivilegeAdminController {

View File

@ -11,6 +11,7 @@ import {
Query, Query,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Privileges } from 'src/decorators/privileges.decorator'; import { Privileges } from 'src/decorators/privileges.decorator';
import { Scopes } from 'src/decorators/scopes.decorator'; import { Scopes } from 'src/decorators/scopes.decorator';
import { OAuth2Guard } from 'src/guards/oauth2.guard'; import { OAuth2Guard } from 'src/guards/oauth2.guard';
@ -24,6 +25,8 @@ import { PageOptions } from 'src/types/pagination.interfaces';
const RELATIONS = ['picture', 'privileges']; const RELATIONS = ['picture', 'privileges'];
@ApiBearerAuth()
@ApiTags('admin')
@Controller('/api/admin/users') @Controller('/api/admin/users')
@UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard) @UseGuards(OAuth2Guard, PrivilegesGuard, ScopesGuard)
export class UserAdminController { export class UserAdminController {

View File

@ -12,7 +12,6 @@ import { RefreshTokenAdapter } from './adapter/refresh-token.adapter';
import { UserAdapter } from './adapter/user.adapter'; import { UserAdapter } from './adapter/user.adapter';
const SCOPE_DESCRIPTION: Record<string, string> = { const SCOPE_DESCRIPTION: Record<string, string> = {
management: 'Manage Icy Network on your behalf',
email: 'Email address', email: 'Email address',
image: 'Profile picture', image: 'Profile picture',
}; };
@ -34,7 +33,7 @@ export class OAuth2Service {
this._oauthAdapter, this._oauthAdapter,
async (req, res, client, scope) => { async (req, res, client, scope) => {
const fullClient = await this.clientService.getById(client.id as string); const fullClient = await this.clientService.getById(client.id as string);
const allowedScopes = [...ALWAYS_AVAILABLE]; let allowedScopes = [...ALWAYS_AVAILABLE];
let disallowedScopes = [...ALWAYS_UNAVAILABLE]; let disallowedScopes = [...ALWAYS_UNAVAILABLE];
Object.keys(SCOPE_DESCRIPTION).forEach((item) => { Object.keys(SCOPE_DESCRIPTION).forEach((item) => {
@ -46,10 +45,11 @@ export class OAuth2Service {
}); });
if (scope.includes('management')) { if (scope.includes('management')) {
disallowedScopes = [ allowedScopes = [
'THIS APPLICATION COULD ACCESS SENSITIVE INFORMATION!', 'Manage Icy Network on your behalf',
'MAKE SURE YOU TRUST THE DEVELOPERS OF THIS APPLICATION', 'Commit administrative actions to the extent of your user privileges',
]; ];
disallowedScopes = null;
} }
res.render('authorize', { res.render('authorize', {

View File

@ -118,7 +118,7 @@ export class OAuth2ClientService {
user: { id: user.id }, user: { id: user.id },
id: authId, id: authId,
}, },
relations: ['user'], relations: ['user', 'client'],
}); });
} }

View File

@ -64,9 +64,13 @@ export class OAuth2TokenService {
}); });
} }
public async wipeClientTokens(client: OAuth2Client): Promise<void> { public async wipeClientTokens(
client: OAuth2Client,
user?: User,
): Promise<void> {
await this.tokenRepository.delete({ await this.tokenRepository.delete({
client: { id: client.id }, client: { id: client.id },
user: user ? { id: user.id } : undefined,
}); });
} }

View File

@ -34,6 +34,7 @@ export class UserTOTPService {
public async getUserTOTP(user: User): Promise<UserToken> { public async getUserTOTP(user: User): Promise<UserToken> {
return this.userTokenRepository.findOne({ return this.userTokenRepository.findOne({
where: { user: { id: user.id }, type: UserTokenType.TOTP }, where: { user: { id: user.id }, type: UserTokenType.TOTP },
relations: ['user'],
}); });
} }
@ -68,4 +69,16 @@ export class UserTOTPService {
return [totp, recovery]; return [totp, recovery];
} }
public async deactivateTOTP(token: UserToken): Promise<void> {
if (!token) {
return;
}
await this.userTokenRepository.delete({
type: UserTokenType.RECOVERY,
user: { id: token.user.id },
});
await this.userTokenRepository.remove(token);
}
} }

View File

@ -8,6 +8,7 @@ import {
Res, Res,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import { Scope } from 'src/decorators/scope.decorator'; import { Scope } from 'src/decorators/scope.decorator';
import { CurrentUser } from 'src/decorators/user.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 { User } from 'src/modules/objects/user/user.entity';
import { OAuth2Service } from '../../oauth2/oauth2.service'; import { OAuth2Service } from '../../oauth2/oauth2.service';
@ApiTags('oauth2')
@Controller('oauth2') @Controller('oauth2')
export class OAuth2Controller { export class OAuth2Controller {
constructor( constructor(
@ -52,6 +54,7 @@ export class OAuth2Controller {
return this._service.oauth.controller.token(req, res, next); return this._service.oauth.controller.token(req, res, next);
} }
@ApiBearerAuth()
@Post('introspect') @Post('introspect')
public introspectWrapper( public introspectWrapper(
@Req() req: Request, @Req() req: Request,
@ -64,6 +67,7 @@ export class OAuth2Controller {
// User information endpoint // User information endpoint
// TODO: Move to API // TODO: Move to API
@ApiBearerAuth()
@Get('user') @Get('user')
@UseGuards(OAuth2Guard) @UseGuards(OAuth2Guard)
public async userInfo( public async userInfo(

View File

@ -19,6 +19,7 @@ import { Throttle } from '@nestjs/throttler';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { unlink } from 'fs/promises'; import { unlink } from 'fs/promises';
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service'; 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 { UploadService } from 'src/modules/objects/upload/upload.service';
import { UserTOTPService } from 'src/modules/objects/user-token/user-totp-token.service'; import { UserTOTPService } from 'src/modules/objects/user-token/user-totp-token.service';
import { UserService } from 'src/modules/objects/user/user.service'; import { UserService } from 'src/modules/objects/user/user.service';
@ -36,6 +37,7 @@ export class SettingsController {
private readonly _user: UserService, private readonly _user: UserService,
private readonly _totp: UserTOTPService, private readonly _totp: UserTOTPService,
private readonly _client: OAuth2ClientService, private readonly _client: OAuth2ClientService,
private readonly _oaToken: OAuth2TokenService,
) {} ) {}
@Get() @Get()
@ -162,6 +164,7 @@ export class SettingsController {
return; return;
} }
await this._oaToken.wipeClientTokens(getAuth.client, req.user);
await this._client.revokeAuthorization(getAuth); await this._client.revokeAuthorization(getAuth);
if (jsreq) { if (jsreq) {

View File

@ -15,11 +15,12 @@ import { ConfigurationModule } from 'src/modules/config/config.module';
import { ConfigurationService } from 'src/modules/config/config.service'; import { ConfigurationService } from 'src/modules/config/config.service';
import { UploadModule } from 'src/modules/objects/upload/upload.module'; import { UploadModule } from 'src/modules/objects/upload/upload.module';
import { UserModule } from 'src/modules/objects/user/user.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 { OAuth2Module } from '../../oauth2/oauth2.module';
import { SettingsController } from './settings.controller'; import { SettingsController } from './settings.controller';
import { SettingsService } from './settings.service'; 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({ @Module({
controllers: [SettingsController], controllers: [SettingsController],
@ -30,6 +31,7 @@ import { UserTokenModule } from 'src/modules/objects/user-token/user-token.modul
UserTokenModule, UserTokenModule,
OAuth2Module, OAuth2Module,
OAuth2ClientModule, OAuth2ClientModule,
OAuth2TokenModule,
MulterModule.registerAsync({ MulterModule.registerAsync({
imports: [ConfigurationModule], imports: [ConfigurationModule],
useFactory: async (config: ConfigurationService) => { useFactory: async (config: ConfigurationService) => {

View File

@ -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 { Request, Response } from 'express';
import { SessionData } from 'express-session';
import { UserTOTPService } from 'src/modules/objects/user-token/user-totp-token.service'; 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 { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
import { QRCodeService } from 'src/modules/utility/services/qr-code.service'; import { QRCodeService } from 'src/modules/utility/services/qr-code.service';
import { TokenService } from 'src/modules/utility/services/token.service'; import { TokenService } from 'src/modules/utility/services/token.service';
@ -12,30 +12,31 @@ export class TwoFactorController {
private totp: UserTOTPService, private totp: UserTOTPService,
private qr: QRCodeService, private qr: QRCodeService,
private token: TokenService, private token: TokenService,
private user: UserService,
private form: FormUtilityService, private form: FormUtilityService,
) {} ) {}
@Get('activate') @Get('activate')
public async twoFAStatus( public async twoFAStatus(@Req() req: Request, @Res() res: Response) {
@Session() session: SessionData,
@Req() req: Request,
@Res() res: Response,
) {
const twoFA = await this.totp.getUserTOTP(req.user); const twoFA = await this.totp.getUserTOTP(req.user);
let secret: string; let secret: string;
if (!twoFA) { if (!twoFA) {
if (session.challenge) { const challengeString = req.query.challenge as string;
const challenge = await this.token.decryptChallenge(session.challenge); if (challengeString) {
if (challenge.type === 'totp') { const challenge = await this.token.decryptChallenge(challengeString);
if (challenge.type === 'totp' && challenge.user === req.user.uuid) {
secret = challenge.secret; secret = challenge.secret;
} }
} }
if (!secret) { if (!secret) {
secret = this.totp.createTOTPSecret(); secret = this.totp.createTOTPSecret();
const challenge = { type: 'totp', secret }; const challenge = { type: 'totp', secret, user: req.user.uuid };
session.challenge = await this.token.encryptChallenge(challenge); 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); const url = this.totp.getTOTPURL(secret, req.user.username);
@ -54,21 +55,25 @@ export class TwoFactorController {
@Post('activate') @Post('activate')
public async twoFAActivate( public async twoFAActivate(
@Session() session: SessionData,
@Body() body: { code: string }, @Body() body: { code: string },
@Req() req: Request, @Req() req: Request,
@Res() res: Response, @Res() res: Response,
) { ) {
let secret: string; let secret: string;
try { try {
if (!session.challenge || !body.code) { const challengeString = req.query.challenge as string;
if (!challengeString || !body.code) {
throw new Error('Invalid request'); throw new Error('Invalid request');
} }
const challenge = await this.token.decryptChallenge(session.challenge); const challenge = await this.token.decryptChallenge(challengeString);
secret = challenge.secret; secret = challenge.secret;
if (challenge.type !== 'totp' || !secret) { if (
challenge.type !== 'totp' ||
challenge.user !== req.user.uuid ||
!secret
) {
throw new Error('Invalid request'); throw new Error('Invalid request');
} }
@ -87,7 +92,59 @@ export class TwoFactorController {
// TODO: show the recovery tokens to the user // TODO: show the recovery tokens to the user
await this.totp.activateTOTP(req.user, secret); 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('/'); res.redirect('/');
} }
} }

View File

@ -38,18 +38,19 @@ block body
if url.type == 'terms' if url.type == 'terms'
a.authorize__client-url(href=url.url, target="_blank", rel="nofollow") Terms of Service 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 .scopes
each allowed in allowedScopes each allowed in allowedScopes
span.scopes__scope.scopes__scope--allowed #{allowed} 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
.scopes span.scopes__scope.scopes__scope--disallowed #{allowed}
each allowed in disallowedScopes
span.scopes__scope.scopes__scope--disallowed #{allowed}
form(method="POST", action="") form(method="POST", action="")
div.form-container div.form-container

View File

@ -42,9 +42,6 @@ block settings
p Two-factor authentication is enabled. p Two-factor authentication is enabled.
a.btn.btn-primary(href="/account/two-factor/disable") Disable a.btn.btn-primary(href="/account/two-factor/disable") Disable
else else
p You can enable two-factor authentication using an authenticator app of your choice, such as p You can enable two-factor authentication using an authenticator app of your choice, such as Google Authenticator.
b Google Authenticator | By clicking activate you will be prompted with a QR code which you will need to scan with such app.
| or
b andOTP
|.
a.btn.btn-primary(href="/account/two-factor/activate") Activate a.btn.btn-primary(href="/account/two-factor/activate") Activate