Commit e75444d4 authored by Revant Nandgaonkar's avatar Revant Nandgaonkar

feat: Client Authentication during token exchange

authorization-server - client has authenticationMethod
viz. Public Client, Body Param or Basic Header
admin-client - client component shows dropdown selection
default is Public Client

adds security enhancement
parent 6edf0ccb
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(new ValidationPipe({ whitelist: true }))
@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,
......
......@@ -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
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,
tokenDeleteEndpoint,
userDeleteEndpoint,
redirectUris: callbackURLs,
......
......@@ -22,3 +22,6 @@ export const ERROR_UPDATING_SERVICE_SETTINGS =
'Error in updating service settings';
export const DELETING = 'Deleting';
export const UNDO = 'Undo';
export const BASIC_HEADER = 'Basic Header';
export const PUBLIC_CLIENT = 'Public Client';
export const BODY_PARAM = 'Body Param';
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment