Skip to content

Commit

Permalink
feat(provider): make client secret non mandatory for public clients
Browse files Browse the repository at this point in the history
make client secret non mandatory for public clients

GH-153
  • Loading branch information
Tyagi-Sunny committed Mar 31, 2023
1 parent 0e51de7 commit a996951
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 78 deletions.
1 change: 1 addition & 0 deletions src/error-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export const enum AuthErrorKeys {
KeyInvalid = 'Key Invalid',
OtpInvalid = 'Otp Invalid',
OtpExpired = 'Otp Token Incorrect or Expired',
ConfidentialClientSecretMissing = 'Confidential Client Secret Missing',
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {inject, Provider} from '@loopback/core';
import {HttpErrors, Request} from '@loopback/rest';
import * as ClientPasswordStrategy from 'passport-oauth2-client-password';
import * as ClientPasswordStrategy from './client-password-strategy';

import {AuthErrorKeys} from '../../../error-keys';
import {IAuthClient} from '../../../types';
Expand All @@ -27,68 +27,56 @@ export class ClientPasswordStrategyFactoryProvider
this.getClientPasswordVerifier(options, verifier);
}

clientPasswordVerifierHelper(
client: IAuthClient | null,
clientSecret: string | undefined,
) {
if (!client) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
} else if (!clientSecret) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientSecretMissing);
} else if (!client.clientSecret || client.clientSecret !== clientSecret) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientVerificationFailed);
} else {
// do nothing
}
}

getClientPasswordVerifier(
options?: ClientPasswordStrategy.StrategyOptionsWithRequestInterface,
verifierPassed?: VerifyFunction.OauthClientPasswordFn,
): ClientPasswordStrategy.Strategy {
const verifyFn = verifierPassed ?? this.verifier;
if (options?.passReqToCallback) {
return new ClientPasswordStrategy.Strategy(
options,

// eslint-disable-next-line @typescript-eslint/no-misused-promises
async (
req: Request,
clientId: string,
clientSecret: string,
cb: (err: Error | null, client?: IAuthClient | false) => void,
clientSecret: string | undefined,
cb: (err: Error | null, client?: IAuthClient | null) => void,
req: Request | undefined,
) => {
try {
const client = await verifyFn(clientId, clientSecret, req);
if (!client) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
} else if (!clientSecret) {
throw new HttpErrors.Unauthorized(
AuthErrorKeys.ClientSecretMissing,
);
} else if (
!client.clientSecret ||
client.clientSecret !== clientSecret
) {
throw new HttpErrors.Unauthorized(
AuthErrorKeys.ClientVerificationFailed,
);
}
this.clientPasswordVerifierHelper(client, clientSecret);
cb(null, client);
} catch (err) {
cb(err);
}
},
options,
);
} else {
return new ClientPasswordStrategy.Strategy(
// eslint-disable-next-line @typescript-eslint/no-misused-promises
async (
clientId: string,
clientSecret: string,
cb: (err: Error | null, client?: IAuthClient | false) => void,
clientSecret: string | undefined,
cb: (err: Error | null, client?: IAuthClient | null) => void,
) => {
try {
const client = await verifyFn(clientId, clientSecret);
if (!client) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
} else if (!clientSecret) {
throw new HttpErrors.Unauthorized(
AuthErrorKeys.ClientSecretMissing,
);
} else if (
!client.clientSecret ||
client.clientSecret !== clientSecret
) {
throw new HttpErrors.Unauthorized(
AuthErrorKeys.ClientVerificationFailed,
);
}
this.clientPasswordVerifierHelper(client, clientSecret);
cb(null, client);
} catch (err) {
cb(err);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Type definitions for passport-oauth2-client-password 0.1.2
// Project: https://github.com/jaredhanson/passport-oauth2-client-password
// Definitions by: Ivan Zubok <https://github.com/akaNightmare>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3

import * as passport from 'passport';
import * as express from 'express';
import {IAuthClient, IAuthSecureClient} from '../../../types';

export interface StrategyOptionsWithRequestInterface {
passReqToCallback: boolean;
}

export interface VerifyFunctionWithRequest {
(
clientId: string,
clientSecret: string | undefined,
done: (
error: Error | null,
client?: IAuthSecureClient | IAuthClient | null,
info?: Object | undefined,
) => void,
req?: express.Request,
): void;
}

export class Strategy extends passport.Strategy {
constructor(
verify: VerifyFunctionWithRequest,
options?: StrategyOptionsWithRequestInterface,
) {
super();
if (!verify)
throw new Error(
'OAuth 2.0 client password strategy requires a verify function',
);

this.verify = verify;
if (options) this.passReqToCallback = options.passReqToCallback;
this.name = 'oauth2-client-password';
}

private readonly verify: VerifyFunctionWithRequest;
private readonly passReqToCallback: boolean;
name: string;
authenticate(req: express.Request, options?: {}): void {
if (
/* eslint-disable @typescript-eslint/prefer-optional-chain */
!req.body ||
!req.body['client_id']
) {
return this.fail();
}

const clientId = req.body['client_id'];
const clientSecret = req.body['client_secret'];

const verified = (
err: Error | null,
client: IAuthSecureClient | IAuthClient | null | undefined,
info: Object | undefined,
) => {
if (err) {
return this.error(err);
}
if (!client) {
return this.fail();
}
this.success(client, info);
};

if (this.passReqToCallback) {
this.verify(clientId, clientSecret, verified, req);
} else {
this.verify(clientId, clientSecret, verified);
}
}
}
1 change: 1 addition & 0 deletions src/strategies/passport/passport-client-password/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './client-password-verify.provider';
export * from './client-password-strategy-factory-provider';
export * from './client-password-strategy';
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {HttpErrors, Request} from '@loopback/rest';
import * as ClientPasswordStrategy from './client-password-strategy';

import {AuthErrorKeys} from '../../../error-keys';
import {ClientType, IAuthClient, IAuthSecureClient} from '../../../types';
import {ClientType, IAuthSecureClient} from '../../../types';
import {Strategies} from '../../keys';
import {VerifyFunction} from '../../types';

Expand All @@ -27,6 +27,26 @@ export class SecureClientPasswordStrategyFactoryProvider
this.getSecureClientPasswordVerifier(options, verifier);
}

secureClientPasswordVerifierHelper(
client: IAuthSecureClient | null,
clientSecret: string | undefined,
) {
if (!client) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
} else if (client.clientType !== ClientType.public && !clientSecret) {
throw new HttpErrors.Unauthorized(
AuthErrorKeys.ConfidentialClientSecretMissing,
);
} else if (
client.clientType !== ClientType.public &&
(!client.clientSecret || client.clientSecret !== clientSecret)
) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientVerificationFailed);
} else {
// do nothing
}
}

getSecureClientPasswordVerifier(
options?: ClientPasswordStrategy.StrategyOptionsWithRequestInterface,
verifierPassed?: VerifyFunction.OauthSecureClientPasswordFn,
Expand All @@ -38,31 +58,12 @@ export class SecureClientPasswordStrategyFactoryProvider
async (
clientId: string,
clientSecret: string | undefined,
cb: (err: Error | null, client?: IAuthSecureClient | false) => void,
cb: (err: Error | null, client?: IAuthSecureClient | null) => void,
req: Request | undefined,
) => {
try {
const client = await verifyFn(clientId, clientSecret, req);
if (!client) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
} else if (
client.clientType !== ClientType.public &&
!clientSecret
) {
throw new HttpErrors.Unauthorized(
AuthErrorKeys.ConfidentialClientSecretMissing,
);
} else if (
(client.clientType !== ClientType.public &&
!client.clientSecret) ||
client.clientSecret !== clientSecret
) {
throw new HttpErrors.Unauthorized(
AuthErrorKeys.ClientVerificationFailed,
);
} else {
// do nothing
}
this.secureClientPasswordVerifierHelper(client, clientSecret);

cb(null, client);
} catch (err) {
Expand All @@ -77,30 +78,13 @@ export class SecureClientPasswordStrategyFactoryProvider
async (
clientId: string,
clientSecret: string | undefined,
cb: (err: Error | null, client?: IAuthClient | false) => void,
cb: (err: Error | null, client?: IAuthSecureClient | null) => void,
) => {
try {
const client = await verifyFn(clientId, clientSecret);

if (!client) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
} else if (
client.clientType !== ClientType.public &&
!clientSecret
) {
throw new HttpErrors.Unauthorized(
AuthErrorKeys.ConfidentialClientSecretMissing,
);
} else if (
client.clientType !== ClientType.public &&
(!client.clientSecret || client.clientSecret !== clientSecret)
) {
throw new HttpErrors.Unauthorized(
AuthErrorKeys.ClientVerificationFailed,
);
} else {
// do nothing
}
this.secureClientPasswordVerifierHelper(client, clientSecret);

cb(null, client);
} catch (err) {
cb(err);
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,8 @@ export interface AuthenticationConfig {
useClientAuthenticationMiddleware?: boolean;
useUserAuthenticationMiddleware?: boolean;
}

export enum ClientType {
public = 'public',
private = 'private',
}

0 comments on commit a996951

Please sign in to comment.