totp deactivate, delete tokens on oauth deauth
This commit is contained in:
parent
7a06c882ac
commit
d92b8ab828
77
package-lock.json
generated
77
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
15
src/main.ts
15
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<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 redisClient = redis.createClient({
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -12,7 +12,6 @@ import { RefreshTokenAdapter } from './adapter/refresh-token.adapter';
|
||||
import { UserAdapter } from './adapter/user.adapter';
|
||||
|
||||
const SCOPE_DESCRIPTION: Record<string, string> = {
|
||||
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', {
|
||||
|
@ -118,7 +118,7 @@ export class OAuth2ClientService {
|
||||
user: { id: user.id },
|
||||
id: authId,
|
||||
},
|
||||
relations: ['user'],
|
||||
relations: ['user', 'client'],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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({
|
||||
client: { id: client.id },
|
||||
user: user ? { id: user.id } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,7 @@ export class UserTOTPService {
|
||||
public async getUserTOTP(user: User): Promise<UserToken> {
|
||||
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<void> {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userTokenRepository.delete({
|
||||
type: UserTokenType.RECOVERY,
|
||||
user: { id: token.user.id },
|
||||
});
|
||||
await this.userTokenRepository.remove(token);
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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) => {
|
||||
|
@ -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('/');
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user