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/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",
|
||||||
|
@ -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",
|
||||||
|
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 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',
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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', {
|
||||||
|
@ -118,7 +118,7 @@ export class OAuth2ClientService {
|
|||||||
user: { id: user.id },
|
user: { id: user.id },
|
||||||
id: authId,
|
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({
|
await this.tokenRepository.delete({
|
||||||
client: { id: client.id },
|
client: { id: client.id },
|
||||||
|
user: user ? { id: user.id } : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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) {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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('/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user