Commit 99b5f41d authored by Revant Nandgaonkar's avatar Revant Nandgaonkar

Security refactor and fixes

Merge branch 'refactor-and-fixes' into 'develop'

See merge request castlecraft/building-blocks!298
parents 2a79fe45 e75444d4
......@@ -45,7 +45,7 @@ export class AuthController {
}
@Post('signup')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@ApiOperation({
title: i18n.__('Signup'),
description: i18n.__('Sign up a new user'),
......@@ -92,7 +92,7 @@ export class AuthController {
}
@Post('password_less')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async passwordLess(@Body() payload: PasswordLessDto, @Req() req) {
const user = await this.authService.passwordLessLogin(payload);
addSessionUser(req, {
......
......@@ -14,11 +14,9 @@ export class CreateSocialLoginDto {
clientSecret: string;
@IsUrl()
@IsOptional()
authorizationURL: string;
@IsUrl()
@IsOptional()
tokenURL: string;
@IsUrl()
......@@ -30,7 +28,6 @@ export class CreateSocialLoginDto {
baseURL: string;
@IsUrl()
@IsOptional()
profileURL: string;
@IsUrl()
......
......@@ -38,7 +38,7 @@ export class SocialLoginController {
@Post('v1/create')
@Roles(ADMINISTRATOR)
@UseGuards(AuthGuard('bearer', { session: false, callback }), RoleGuard)
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async create(@Body() body: CreateSocialLoginDto, @Req() req, @Res() res) {
const payload: any = body;
payload.createdBy = req.user.user;
......
import { Injectable } from '@nestjs/common';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from './passport.strategy';
import { Strategy } from 'passport-oauth2-code';
import { ClientService } from '../../../client-management/entities/client/client.service';
import { ClientAuthentication } from '../../../client-management/entities/client/client.interface';
@Injectable()
export class AuthorizationCodeStrategy extends PassportStrategy(Strategy) {
constructor(private readonly clientService: ClientService) {
super();
super({ passReqToCallback: true });
}
async validate(code, clientId, clientSecret, redirectURI, verified) {
async validate(req, code, clientId, clientSecret, redirectURI, verified) {
// check the client for allowed redirect uri and pas the code
try {
const client = await this.clientService.findOne({ clientId });
if (!client) return verified(null, false);
if (!client.redirectUris.includes(redirectURI))
return verified(null, false);
return verified(null, client);
// Check Authentication for Client
if (client.authenticationMethod === ClientAuthentication.BasicHeader) {
if (req.headers.authorization) {
const basicAuthHeader = req.headers.authorization.split(' ')[1];
const [reqClientId, reqClientSecret] = Buffer.from(
basicAuthHeader,
'base64',
)
.toString()
.split(':');
if (
reqClientId === client.clientId &&
reqClientSecret === client.clientSecret
) {
return verified(null, client);
}
}
} else if (
client.authenticationMethod === ClientAuthentication.BodyParam
) {
if (
req.body.client_id === client.clientId &&
req.body.client_secret === client.clientSecret
) {
return verified(null, client);
}
} else if (
!client.authenticationMethod ||
client.authenticationMethod === ClientAuthentication.PublicClient
) {
return verified(null, client);
}
return verified(null, false);
} catch (error) {
return verified(error, null);
return verified(new UnauthorizedException(error), null);
}
}
}
......@@ -41,7 +41,7 @@ export class ClientController {
@Post('v1/create')
@UseGuards(AuthGuard('bearer', { session: false, callback }))
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ forbidNonWhitelisted: true }))
async create(@Body() body: CreateClientDto, @Req() req, @Res() res) {
const payload: any = body;
if (!(await this.userService.checkAdministrator(req.user.user))) {
......@@ -56,6 +56,7 @@ export class ClientController {
}
@Put('v1/update/:clientId')
@UsePipes(new ValidationPipe({ forbidNonWhitelisted: true }))
@UseGuards(AuthGuard('bearer', { session: false, callback }))
async update(
@Body() payload: CreateClientDto,
......
......@@ -74,7 +74,7 @@ export class ScopeController {
}
@Post('v1/create')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(AuthGuard('bearer', { session: false, callback }), RoleGuard)
async create(@Body() body: CreateScopeDto, @Res() res) {
......
......@@ -16,4 +16,11 @@ export interface Client extends Document {
userDeleteEndpoint?: string;
tokenDeleteEndpoint?: string;
changedClientSecret?: string;
authenticationMethod?: ClientAuthentication;
}
export enum ClientAuthentication {
BasicHeader = 'BASIC_HEADER',
BodyParam = 'BODY_PARAM',
PublicClient = 'PUBLIC_CLIENT',
}
import * as mongoose from 'mongoose';
import * as uuidv4 from 'uuid/v4';
import { randomBytes } from 'crypto';
import { ClientAuthentication } from './client.interface';
const schema = new mongoose.Schema(
{
......@@ -19,6 +20,10 @@ const schema = new mongoose.Schema(
userDeleteEndpoint: String,
tokenDeleteEndpoint: String,
changedClientSecret: String,
authenticationMethod: {
type: String,
default: ClientAuthentication.PublicClient,
},
},
{ collection: 'client', versionKey: false },
);
......
......@@ -5,12 +5,14 @@ import {
IsNumberString,
ValidateNested,
IsBoolean,
IsEnum,
} from 'class-validator';
import { ApiModelProperty } from '@nestjs/swagger';
import { i18n } from '../../../i18n/i18n.config';
import { Type } from 'class-transformer';
import { RedirectURIsDTO } from './redirect-uris.dto';
import { AllowedScopeDTO } from './allowed-scopes.dto';
import { ClientAuthentication } from './client.interface';
export class CreateClientDto {
@IsString()
......@@ -44,14 +46,14 @@ export class CreateClientDto {
'Client app endpoint which will receive the token/code',
),
})
@ValidateNested()
@ValidateNested({ each: true })
@Type(() => RedirectURIsDTO)
redirectUris: RedirectURIsDTO[];
@ApiModelProperty({
description: i18n.__('Allowed Scopes for Client app'),
})
@ValidateNested()
@ValidateNested({ each: true })
@Type(() => AllowedScopeDTO)
allowedScopes: AllowedScopeDTO[];
......@@ -74,4 +76,16 @@ export class CreateClientDto {
type: 'string',
})
tokenDeleteEndpoint: string;
@IsEnum(ClientAuthentication)
@IsOptional()
@ApiModelProperty({
description: i18n.__(
'Type of method to authenticate client during authorization code exchange',
),
required: false,
type: 'string',
enum: ClientAuthentication,
})
authenticationMethod: string;
}
......@@ -76,5 +76,6 @@
"Bearer Token Revoked Successfully": "Bearer Token Revoked Successfully",
"Invalid Bearer Token": "Invalid Bearer Token",
"Treat this as internal trusted client if trust is greater than 0": "Treat this as internal trusted client if trust is greater than 0",
"Skips the Allow/Deny screen if value is true": "Skips the Allow/Deny screen if value is true"
"Skips the Allow/Deny screen if value is true": "Skips the Allow/Deny screen if value is true",
"Type of method to authenticate client during authorization code exchange": "Type of method to authenticate client during authorization code exchange"
}
\ No newline at end of file
......@@ -35,7 +35,7 @@ export class ServerSettingsController {
}
@Post('v1/update')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(AuthGuard('bearer', { session: false, callback }), RoleGuard)
async updateSettings(@Body() payload: ServerSettingDto, @Req() req) {
......@@ -46,7 +46,7 @@ export class ServerSettingsController {
}
@Post('v1/delete_bearer_tokens')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(AuthGuard('bearer', { session: false, callback }), RoleGuard)
async deleteTokens(@Req() req) {
......@@ -57,7 +57,7 @@ export class ServerSettingsController {
}
@Post('v1/delete_user_sessions')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(AuthGuard('bearer', { session: false, callback }), RoleGuard)
async deleteSessions(@Req() req) {
......
......@@ -14,7 +14,7 @@ export class SignupController {
constructor(private readonly signupService: SignupService) {}
@Post('v1/email')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async signupViaEmail(@Body() payload: SignupViaEmailDto, @Res() res) {
await this.signupService.validateSignupEnabled();
payload.email = payload.email.trim().toLocaleLowerCase();
......
......@@ -53,7 +53,7 @@ export class UserController {
@Post('v1/change_password')
@UseGuards(AuthGuard('bearer', { session: false, callback }))
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async updatePassword(@Req() req, @Body() passwordPayload: ChangePasswordDto) {
const userUuid = req.user.user;
return await this.commandBus.execute(
......@@ -100,7 +100,7 @@ export class UserController {
}
@Post('v1/create')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(AuthGuard('bearer', { session: false, callback }), RoleGuard)
async create(@Body() payload: UserAccountDto, @Req() req) {
......@@ -178,7 +178,7 @@ export class UserController {
}
@Post('v1/generate_password')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async verifyEmail(@Body() payload: VerifyEmailDto) {
return await this.commandBus.execute(
new VerifyEmailAndSetPasswordCommand(payload),
......
......@@ -61,7 +61,7 @@ export class CloudStorageController {
}
@Post('v1/add')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
async addStorage(@Body() payload: StorageValidationDto, @Req() req) {
......@@ -69,7 +69,7 @@ export class CloudStorageController {
}
@Put('v1/modify/:uuid')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
async modifyStorage(@Body() payload: ModifyStorageDto, @Param('uuid') uuid) {
......@@ -79,7 +79,7 @@ export class CloudStorageController {
}
@Delete('v1/remove/:uuid')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
async removeStorage(@Param() uuid, @Req() req) {
......
......@@ -30,14 +30,14 @@ export class EmailController {
@Post('v1/system')
@UseGuards(AuthServerVerificationGuard)
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async sendSystemEmail(@Body() payload: EmailMessageAuthServerDto) {
return await this.emailService.sendSystemMessage(payload);
}
@Post('v1/create')
@UseGuards(TokenGuard)
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async create(@Req() req, @Res() res, @Body() payload: CreateEmailDto) {
payload.owner = req.token.sub;
const emailAccount = await this.emailAccount.save(payload);
......
......@@ -32,7 +32,7 @@ export class Oauth2ProviderController {
@Post('v1/add_provider')
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async addProvider(@Body() payload: OAuth2ProviderDto) {
return await this.commandBus.execute(new AddOAuth2ProviderCommand(payload));
}
......@@ -50,7 +50,7 @@ export class Oauth2ProviderController {
@Post('v1/update_provider/:uuid')
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async updateProvider(
@Param('uuid') uuid,
@Body() payload: OAuth2ProviderDto,
......
......@@ -30,7 +30,7 @@ export class SettingsController {
@Post('v1/update')
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async updateSettings(@Body() payload: ServerSettingsDto) {
return from(this.settingsService.find()).pipe(
switchMap(settings => {
......
......@@ -13,7 +13,7 @@ export class SetupController {
constructor(private readonly setupService: SetupService) {}
@Post()
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async setup(@Body() setupForm: ServerSettingsDto) {
return await this.setupService.setup(setupForm);
}
......
......@@ -28,7 +28,7 @@ export class ProfileController {
@Post('v1/update_profile_details')
@UseGuards(TokenGuard)
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async updateProfileDetails(@Body() profile: ProfileDetailsDTO, @Req() req) {
let updatedProfile: Profile;
if (profile.uuid && profile.uuid === req.token.sub) {
......@@ -48,7 +48,7 @@ export class ProfileController {
@Post('v1/update_personal_details')
@UseGuards(TokenGuard)
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async updateProfile(
@Body() profile: PersonalDetailsDTO,
@Req() req,
......
......@@ -30,7 +30,7 @@ export class SettingsController {
@Post('v1/update')
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async updateSettings(@Body() payload: ServerSettingsDto) {
return from(this.settingsService.find()).pipe(
switchMap(settings => {
......
......@@ -13,7 +13,7 @@ export class SetupController {
constructor(private readonly settingsService: SetupService) {}
@Post()
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async setup(@Body() payload: ServerSettingsDto) {
return await this.settingsService.setup(payload);
}
......
......@@ -54,7 +54,7 @@ export class ServiceTypeController {
}
@Post('v1/create')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
async registerService(@Body() payload: ServiceTypeValidationDto) {
......
......@@ -57,7 +57,7 @@ export class ServiceController {
}
@Post('v1/register')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
async registerService(@Body() payload: CreateServiceDto, @Req() req) {
......@@ -68,7 +68,7 @@ export class ServiceController {
}
@Post('v1/modify/:clientId')
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
async modifyService(
......
......@@ -29,7 +29,7 @@ export class SettingsController {
@Post('v1/update')
@Roles(ADMINISTRATOR)
@UseGuards(TokenGuard, RoleGuard)
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
updateSettings(@Body() payload: ServerSettingsDto) {
return this.settingsService.find().pipe(
switchMap(settings => {
......
......@@ -13,7 +13,7 @@ export class SetupController {
constructor(private readonly settingsService: SetupService) {}
@Post()
@UsePipes(ValidationPipe)
@UsePipes(new ValidationPipe({ whitelist: true }))
async setup(@Body() icSettingsDTO: ServerSettingsDto) {
return await this.settingsService.setup(icSettingsDTO);
}
......
export enum ClientAuthentication {
BasicHeader = 'BASIC_HEADER',
BodyParam = 'BODY_PARAM',
PublicClient = 'PUBLIC_CLIENT',
}
......@@ -8,6 +8,17 @@
<mat-checkbox formControlName="isTrusted" matSuffix class="client-checkboxes">Trusted</mat-checkbox>
<mat-checkbox formControlName="autoApprove" matSuffix>Auto Approve</mat-checkbox>
</mat-form-field>
<mat-form-field>
<mat-label>Authentication Method</mat-label>
<mat-select
[(value)]="authenticationMethod"
formControlName="authenticationMethod"
placeholder="Client Authentication">
<mat-option *ngFor="let method of authMethods" [value]="method.value">
{{ method.viewValue }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="clientId">
<input matInput placeholder="Client ID" formControlName="clientId" password readonly>
</mat-form-field>
......
......@@ -8,8 +8,12 @@ import {
CLOSE,
CLIENT_CREATED,
CLIENT_UPDATED,
BASIC_HEADER,
PUBLIC_CLIENT,
BODY_PARAM,
} from '../../constants/messages';
import { NEW_ID } from '../../constants/common';
import { ClientAuthentication } from './client-authentication.enum';
export const CLIENT_LIST_ROUTE = '/client/list';
......@@ -40,6 +44,13 @@ export class ClientComponent implements OnInit {
clientForm: FormGroup;
callbackURLForms: FormArray;
authenticationMethod = ClientAuthentication.PublicClient;
authMethods = [
{ value: ClientAuthentication.BasicHeader, viewValue: BASIC_HEADER },
{ value: ClientAuthentication.PublicClient, viewValue: PUBLIC_CLIENT },
{ value: ClientAuthentication.BodyParam, viewValue: BODY_PARAM },
];
constructor(
private readonly clientService: ClientService,
private route: ActivatedRoute,
......@@ -53,6 +64,7 @@ export class ClientComponent implements OnInit {
ngOnInit() {
this.clientForm = this.formBuilder.group({
clientName: this.clientName,
authenticationMethod: this.authenticationMethod,
clientURL: this.clientURL,
clientScopes: this.clientScopes,
tokenDeleteEndpoint: this.tokenDeleteEndpoint,
......@@ -69,6 +81,7 @@ export class ClientComponent implements OnInit {
this.subscribeGetClient(this.uuid);
}
this.subscribeGetScopes();
this.setupFieldObservables();
}
createCallbackURLFormGroup(callbackURL?: string): FormGroup {
......@@ -103,6 +116,7 @@ export class ClientComponent implements OnInit {
this.clientService
.createClient(
this.clientForm.controls.clientName.value,
this.clientForm.controls.authenticationMethod.value,
this.getCallbackURLs(),
this.clientForm.controls.clientScopes.value,
this.clientForm.controls.isTrusted.value ? '1' : '0',
......@@ -134,11 +148,12 @@ export class ClientComponent implements OnInit {
.updateClient(
this.clientId,
this.clientForm.controls.clientName.value,
this.clientForm.controls.authenticationMethod.value,
this.clientForm.controls.tokenDeleteEndpoint.value,
this.clientForm.controls.userDeleteEndpoint.value,
this.getCallbackURLs(),
this.clientForm.controls.clientScopes.value,
this.clientForm.controls.isTrusted.value,
this.clientForm.controls.isTrusted.value ? '1' : '0',
this.clientForm.controls.autoApprove.value,
)
.subscribe({
......@@ -169,6 +184,13 @@ export class ClientComponent implements OnInit {
this.clientName = client.name;
this.callbackURLs = client.redirectUris;
this.isTrusted = client.isTrusted;
if (client.authenticationMethod) {
this.authenticationMethod = client.authenticationMethod;
this.clientForm.controls.authenticationMethod.setValue(
client.authenticationMethod,
);
}
this.clientForm.controls.tokenDeleteEndpoint.setValue(
client.tokenDeleteEndpoint,
);
......@@ -187,8 +209,10 @@ export class ClientComponent implements OnInit {
this.clientForm.controls.isTrusted.setValue(client.isTrusted);
this.clientForm.controls.autoApprove.setValue(client.autoApprove);
this.clientForm.controls.clientScopes.setValue(client.allowedScopes);
this.toggleTrustedAutoApprove(this.isTrusted);
this.toggleTrustedAutoApprove(client.isTrusted);
}
setupFieldObservables() {
this.clientForm.controls.isTrusted.valueChanges.subscribe({
next: value => {
this.toggleTrustedAutoApprove(value);
......
......@@ -9,6 +9,7 @@ import { Observable } from 'rxjs';
import { StorageService } from '../../common/services/storage/storage.service';
import { ISSUER_URL } from '../../constants/storage';
import { CANNOT_FETCH_CLIENT } from '../../constants/messages';
import { ClientAuthentication } from './client-authentication.enum';
@Injectable()
export class ClientService {
......@@ -42,6 +43,7 @@ export class ClientService {
createClient(
clientName: string,
authenticationMethod: ClientAuthentication,
callbackURLs: string[],
scopes: string[],
isTrusted: string,
......@@ -50,6 +52,7 @@ export class ClientService {
const url = `${this.storageService.getInfo(ISSUER_URL)}/client/v1/create`;
const clientData = {
name: clientName,
authenticationMethod,
redirectUris: callbackURLs,
allowedScopes: scopes,
isTrusted,
......@@ -61,11 +64,12 @@ export class ClientService {
updateClient(
clientId: string,
clientName: string,
authenticationMethod: ClientAuthentication,
tokenDeleteEndpoint: string,
userDeleteEndpoint: string,
callbackURLs: string[],
scopes: string[],
isTrusted: boolean,
isTrusted: string,
autoApprove: boolean,
) {
const url = `${this.storageService.getInfo(
......@@ -73,6 +77,7 @@ export class ClientService {
)}/client/v1/update/${clientId}`;
return this.http.put(url, {
name: clientName,
authenticationMethod,