Skip to content

Commit 039fa8c

Browse files
authored
feat(provider): make client secret non mandatory for public clients (#154)
* feat(provider): added clientType to auth client making client secret non mandatory for public clients GH-153
1 parent 13b67ac commit 039fa8c

File tree

8 files changed

+230
-30
lines changed

8 files changed

+230
-30
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@
141141
},
142142
"@semantic-release/npm": {
143143
"npm": "^9.4.2"
144-
}
144+
},
145+
"@openapi-contrib/openapi-schema-to-json-schema": "3.2.0"
145146
},
146147
"release": {
147148
"branches": [

src/component.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
OtpVerifyProvider,
4040
} from './strategies';
4141
import {Strategies} from './strategies/keys';
42+
import {SecureClientPasswordStrategyFactoryProvider} from './strategies/passport/passport-client-password/secure-client-password-strategy-factory-provider';
4243
import {
4344
CognitoAuthVerifyProvider,
4445
CognitoStrategyFactoryProvider,
@@ -114,6 +115,14 @@ export class AuthenticationComponent implements Component {
114115
[Strategies.Passport.AZURE_AD_VERIFIER.key]: AzureADAuthVerifyProvider,
115116
[Strategies.Passport.KEYCLOAK_VERIFIER.key]: KeycloakVerifyProvider,
116117
};
118+
119+
if (this.config?.secureClient) {
120+
this.providers = {
121+
...this.providers,
122+
[Strategies.Passport.CLIENT_PASSWORD_STRATEGY_FACTORY.key]:
123+
SecureClientPasswordStrategyFactoryProvider,
124+
};
125+
}
117126
this.bindings = [];
118127
if (this.config?.useClientAuthenticationMiddleware) {
119128
this.bindings.push(

src/strategies/passport/passport-client-password/client-password-strategy-factory-provider.ts

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {inject, Provider} from '@loopback/core';
22
import {HttpErrors, Request} from '@loopback/rest';
3-
import * as ClientPasswordStrategy from 'passport-oauth2-client-password';
3+
import * as ClientPasswordStrategy from './client-password-strategy';
44

55
import {AuthErrorKeys} from '../../../error-keys';
66
import {IAuthClient} from '../../../types';
@@ -27,60 +27,52 @@ export class ClientPasswordStrategyFactoryProvider
2727
this.getClientPasswordVerifier(options, verifier);
2828
}
2929

30+
clientPasswordVerifierHelper(
31+
client: IAuthClient | null,
32+
clientSecret: string | undefined,
33+
) {
34+
if (!client?.clientSecret || client.clientSecret !== clientSecret) {
35+
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientVerificationFailed);
36+
} else {
37+
// do nothing
38+
}
39+
}
40+
3041
getClientPasswordVerifier(
3142
options?: ClientPasswordStrategy.StrategyOptionsWithRequestInterface,
3243
verifierPassed?: VerifyFunction.OauthClientPasswordFn,
3344
): ClientPasswordStrategy.Strategy {
3445
const verifyFn = verifierPassed ?? this.verifier;
3546
if (options?.passReqToCallback) {
3647
return new ClientPasswordStrategy.Strategy(
37-
options,
38-
3948
// eslint-disable-next-line @typescript-eslint/no-misused-promises
4049
async (
41-
req: Request,
4250
clientId: string,
43-
clientSecret: string,
44-
cb: (err: Error | null, client?: IAuthClient | false) => void,
51+
clientSecret: string | undefined,
52+
cb: (err: Error | null, client?: IAuthClient | null) => void,
53+
req: Request | undefined,
4554
) => {
4655
try {
4756
const client = await verifyFn(clientId, clientSecret, req);
48-
if (!client) {
49-
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
50-
} else if (
51-
!client.clientSecret ||
52-
client.clientSecret !== clientSecret
53-
) {
54-
throw new HttpErrors.Unauthorized(
55-
AuthErrorKeys.ClientVerificationFailed,
56-
);
57-
}
57+
this.clientPasswordVerifierHelper(client, clientSecret);
5858
cb(null, client);
5959
} catch (err) {
6060
cb(err);
6161
}
6262
},
63+
options,
6364
);
6465
} else {
6566
return new ClientPasswordStrategy.Strategy(
6667
// eslint-disable-next-line @typescript-eslint/no-misused-promises
6768
async (
6869
clientId: string,
69-
clientSecret: string,
70-
cb: (err: Error | null, client?: IAuthClient | false) => void,
70+
clientSecret: string | undefined,
71+
cb: (err: Error | null, client?: IAuthClient | null) => void,
7172
) => {
7273
try {
7374
const client = await verifyFn(clientId, clientSecret);
74-
if (!client) {
75-
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
76-
} else if (
77-
!client.clientSecret ||
78-
client.clientSecret !== clientSecret
79-
) {
80-
throw new HttpErrors.Unauthorized(
81-
AuthErrorKeys.ClientVerificationFailed,
82-
);
83-
}
75+
this.clientPasswordVerifierHelper(client, clientSecret);
8476
cb(null, client);
8577
} catch (err) {
8678
cb(err);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Type definitions for passport-oauth2-client-password 0.1.2
2+
// Project: https://github.com/jaredhanson/passport-oauth2-client-password
3+
// Definitions by: Ivan Zubok <https://github.com/akaNightmare>
4+
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5+
// TypeScript Version: 2.3
6+
7+
import * as passport from 'passport';
8+
import * as express from 'express';
9+
import {IAuthClient, IAuthSecureClient} from '../../../types';
10+
11+
export interface StrategyOptionsWithRequestInterface {
12+
passReqToCallback: boolean;
13+
}
14+
15+
export interface VerifyFunctionWithRequest {
16+
(
17+
clientId: string,
18+
clientSecret: string | undefined,
19+
done: (
20+
error: Error | null,
21+
client?: IAuthSecureClient | IAuthClient | null,
22+
info?: Object | undefined,
23+
) => void,
24+
req?: express.Request,
25+
): void;
26+
}
27+
28+
export class Strategy extends passport.Strategy {
29+
constructor(
30+
verify: VerifyFunctionWithRequest,
31+
options?: StrategyOptionsWithRequestInterface,
32+
) {
33+
super();
34+
if (!verify)
35+
throw new Error(
36+
'OAuth 2.0 client password strategy requires a verify function',
37+
);
38+
39+
this.verify = verify;
40+
if (options) this.passReqToCallback = options.passReqToCallback;
41+
this.name = 'oauth2-client-password';
42+
}
43+
44+
private readonly verify: VerifyFunctionWithRequest;
45+
private readonly passReqToCallback: boolean;
46+
name: string;
47+
authenticate(req: express.Request, options?: {}): void {
48+
if (!req?.body?.client_id) {
49+
return this.fail();
50+
}
51+
52+
const clientId = req.body['client_id'];
53+
const clientSecret = req.body['client_secret'];
54+
55+
const verified = (
56+
err: Error | null,
57+
client: IAuthSecureClient | IAuthClient | null | undefined,
58+
info: Object | undefined,
59+
) => {
60+
if (err) {
61+
return this.error(err);
62+
}
63+
if (!client) {
64+
return this.fail();
65+
}
66+
this.success(client, info);
67+
};
68+
69+
if (this.passReqToCallback) {
70+
this.verify(clientId, clientSecret, verified, req);
71+
} else {
72+
this.verify(clientId, clientSecret, verified);
73+
}
74+
}
75+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './client-password-verify.provider';
22
export * from './client-password-strategy-factory-provider';
3+
export * from './client-password-strategy';
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {inject, Provider} from '@loopback/core';
2+
import {HttpErrors, Request} from '@loopback/rest';
3+
import * as ClientPasswordStrategy from './client-password-strategy';
4+
5+
import {AuthErrorKeys} from '../../../error-keys';
6+
import {ClientType, IAuthSecureClient} from '../../../types';
7+
import {Strategies} from '../../keys';
8+
import {VerifyFunction} from '../../types';
9+
10+
export interface SecureClientPasswordStrategyFactory {
11+
(
12+
options?: ClientPasswordStrategy.StrategyOptionsWithRequestInterface,
13+
verifierPassed?: VerifyFunction.OauthSecureClientPasswordFn,
14+
): ClientPasswordStrategy.Strategy;
15+
}
16+
17+
export class SecureClientPasswordStrategyFactoryProvider
18+
implements Provider<SecureClientPasswordStrategyFactory>
19+
{
20+
constructor(
21+
@inject(Strategies.Passport.OAUTH2_CLIENT_PASSWORD_VERIFIER)
22+
private readonly verifier: VerifyFunction.OauthSecureClientPasswordFn,
23+
) {}
24+
25+
value(): SecureClientPasswordStrategyFactory {
26+
return (options, verifier) =>
27+
this.getSecureClientPasswordVerifier(options, verifier);
28+
}
29+
30+
secureClientPasswordVerifierHelper(
31+
client: IAuthSecureClient | null,
32+
clientSecret: string | undefined,
33+
) {
34+
if (
35+
!client ||
36+
(client.clientType !== ClientType.public &&
37+
(!client.clientSecret || client.clientSecret !== clientSecret))
38+
) {
39+
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientVerificationFailed);
40+
} else {
41+
// do nothing
42+
}
43+
}
44+
45+
getSecureClientPasswordVerifier(
46+
options?: ClientPasswordStrategy.StrategyOptionsWithRequestInterface,
47+
verifierPassed?: VerifyFunction.OauthSecureClientPasswordFn,
48+
): ClientPasswordStrategy.Strategy {
49+
const verifyFn = verifierPassed ?? this.verifier;
50+
if (options?.passReqToCallback) {
51+
return new ClientPasswordStrategy.Strategy(
52+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
53+
async (
54+
clientId: string,
55+
clientSecret: string | undefined,
56+
cb: (err: Error | null, client?: IAuthSecureClient | null) => void,
57+
req: Request | undefined,
58+
) => {
59+
try {
60+
const client = await verifyFn(clientId, clientSecret, req);
61+
this.secureClientPasswordVerifierHelper(client, clientSecret);
62+
63+
cb(null, client);
64+
} catch (err) {
65+
cb(err);
66+
}
67+
},
68+
options,
69+
);
70+
} else {
71+
return new ClientPasswordStrategy.Strategy(
72+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
73+
async (
74+
clientId: string,
75+
clientSecret: string | undefined,
76+
cb: (err: Error | null, client?: IAuthSecureClient | null) => void,
77+
) => {
78+
try {
79+
const client = await verifyFn(clientId, clientSecret);
80+
81+
this.secureClientPasswordVerifierHelper(client, clientSecret);
82+
83+
cb(null, client);
84+
} catch (err) {
85+
cb(err);
86+
}
87+
},
88+
);
89+
}
90+
}
91+
}

src/strategies/types/types.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as FacebookStrategy from 'passport-facebook';
66
import * as AppleStrategy from 'passport-apple';
77
import * as SamlStrategy from '@node-saml/passport-saml';
88
import {DecodedIdToken} from 'passport-apple';
9-
import {Cognito, IAuthClient, IAuthUser} from '../../types';
9+
import {Cognito, IAuthClient, IAuthSecureClient, IAuthUser} from '../../types';
1010
import {Keycloak} from './keycloak.types';
1111
import {Otp} from '../passport';
1212

@@ -23,6 +23,11 @@ export namespace VerifyFunction {
2323
(clientId: string, clientSecret: string, req?: Request): Promise<T | null>;
2424
}
2525

26+
export interface OauthSecureClientPasswordFn<T = IAuthSecureClient>
27+
extends GenericAuthFn<T> {
28+
(clientId: string, clientSecret: string, req?: Request): Promise<T | null>;
29+
}
30+
2631
export interface LocalPasswordFn<T = IAuthUser> extends GenericAuthFn<T> {
2732
(username: string, password: string, req?: Request): Promise<T | null>;
2833
}
@@ -45,6 +50,19 @@ export namespace VerifyFunction {
4550
): Promise<{client: T; user: S} | null>;
4651
}
4752

53+
export interface SecureResourceOwnerPasswordFn<
54+
T = IAuthSecureClient,
55+
S = IAuthUser,
56+
> {
57+
(
58+
clientId: string,
59+
clientSecret: string,
60+
username: string,
61+
password: string,
62+
req?: Request,
63+
): Promise<{client: T; user: S} | null>;
64+
}
65+
4866
export interface GoogleAuthFn<T = IAuthUser> extends GenericAuthFn<T> {
4967
(
5068
accessToken: string,

src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ export interface IAuthClient {
1414
redirectUrl?: string;
1515
}
1616

17+
export interface IAuthSecureClient {
18+
clientId: string;
19+
clientSecret: string;
20+
clientType: ClientType;
21+
redirectUrl?: string;
22+
}
23+
1724
export interface IAuthUser {
1825
id?: number | string;
1926
username: string;
@@ -49,4 +56,10 @@ export interface ClientAuthCode<T extends IAuthUser, ID = number> {
4956
export interface AuthenticationConfig {
5057
useClientAuthenticationMiddleware?: boolean;
5158
useUserAuthenticationMiddleware?: boolean;
59+
secureClient?: boolean;
60+
}
61+
62+
export enum ClientType {
63+
public = 'public',
64+
private = 'private',
5265
}

0 commit comments

Comments
 (0)