Skip to content

Commit decb68a

Browse files
feat(provider): add SAML passport authentication strategy (#113)
* feat(provider): add SAML strategy * feat(provider): feat(provider): add SAML strategy * feat(provider): feat(provider): add SAML strategy * feat(provider): feat(provider): add SAML strategy * feat(provider): add SAML passport authentication strategy * feat(provider): add SAML passport authentication strategy * feat(provider): add SAML passport authentication strategy GH-128
1 parent 1110855 commit decb68a

File tree

14 files changed

+397
-1
lines changed

14 files changed

+397
-1
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {Provider} from '@loopback/core';
2+
import {VerifyFunction} from '../../../strategies';
3+
import * as SamlStrategy from 'passport-saml';
4+
import {IAuthUser} from '../../../types';
5+
import {Request} from '@loopback/rest';
6+
7+
export class BearerTokenVerifyProvider
8+
implements Provider<VerifyFunction.SamlFn>
9+
{
10+
constructor() {
11+
//this is intentional
12+
}
13+
14+
value(): VerifyFunction.SamlFn {
15+
return async (
16+
profile: SamlStrategy.Profile,
17+
cb: SamlStrategy.VerifiedCallback,
18+
req?: Request,
19+
) => {
20+
const userToPass: IAuthUser = {
21+
id: 1,
22+
username: 'xyz',
23+
password: 'pass',
24+
};
25+
26+
return userToPass;
27+
};
28+
}
29+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {Client, createClientForHandler} from '@loopback/testlab';
2+
import {RestServer, Request} from '@loopback/rest';
3+
import {Application, Provider} from '@loopback/core';
4+
import {get} from '@loopback/openapi-v3';
5+
import {authenticate} from '../../../../decorators';
6+
import {STRATEGY} from '../../../../strategy-name.enum';
7+
import {getApp} from '../helpers/helpers';
8+
import {MyAuthenticationSequence} from '../../../fixtures/sequences/authentication.sequence';
9+
import {Strategies} from '../../../../strategies/keys';
10+
import {VerifyFunction} from '../../../../strategies';
11+
import {userWithoutReqObj} from '../../../fixtures/data/bearer-data';
12+
import * as SamlStrategy from 'passport-saml';
13+
14+
describe('getting saml strategy with options', () => {
15+
let app: Application;
16+
let server: RestServer;
17+
beforeEach(givenAServer);
18+
beforeEach(givenAuthenticatedSequence);
19+
beforeEach(getAuthVerifier);
20+
21+
it('should return 302 when name is passed and passReqToCallback is set true', async () => {
22+
class TestController {
23+
@get('/test')
24+
@authenticate(STRATEGY.SAML, {
25+
name: 'string',
26+
passReqToCallback: true,
27+
})
28+
test() {
29+
return 'test successful';
30+
}
31+
}
32+
33+
app.controller(TestController);
34+
35+
await whenIMakeRequestTo(server).get('/test').expect(302);
36+
});
37+
38+
function whenIMakeRequestTo(restServer: RestServer): Client {
39+
return createClientForHandler(restServer.requestHandler);
40+
}
41+
42+
async function givenAServer() {
43+
app = getApp();
44+
server = await app.getServer(RestServer);
45+
}
46+
47+
function getAuthVerifier() {
48+
app.bind(Strategies.SAML_VERIFIER).toProvider(SamlVerifyProvider);
49+
}
50+
51+
function givenAuthenticatedSequence() {
52+
// bind user defined sequence
53+
server.sequence(MyAuthenticationSequence);
54+
}
55+
});
56+
57+
class SamlVerifyProvider implements Provider<VerifyFunction.SamlFn> {
58+
constructor() {
59+
//this is intentional
60+
}
61+
62+
value(): VerifyFunction.SamlFn {
63+
return async (
64+
profile: SamlStrategy.Profile,
65+
cd: SamlStrategy.VerifiedCallback,
66+
req?: Request,
67+
) => {
68+
return userWithoutReqObj;
69+
};
70+
}
71+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {Client, createClientForHandler} from '@loopback/testlab';
2+
import {RestServer, Request} from '@loopback/rest';
3+
import {Application, Provider} from '@loopback/core';
4+
import {get} from '@loopback/openapi-v3';
5+
import {authenticate} from '../../../../decorators';
6+
import {STRATEGY} from '../../../../strategy-name.enum';
7+
import {getApp} from '../helpers/helpers';
8+
import {MyAuthenticationMiddlewareSequence} from '../../../fixtures/sequences/authentication-middleware.sequence';
9+
import {Strategies} from '../../../../strategies/keys';
10+
import {VerifyFunction} from '../../../../strategies';
11+
import {userWithoutReqObj} from '../../../fixtures/data/bearer-data';
12+
import * as SamlStrategy from 'passport-saml';
13+
14+
describe('getting saml strategy with options using Middleware Sequence', () => {
15+
let app: Application;
16+
let server: RestServer;
17+
beforeEach(givenAServer);
18+
beforeEach(givenAuthenticatedSequence);
19+
beforeEach(getAuthVerifier);
20+
21+
it('should return 302 when name is passed and passReqToCallback is set true', async () => {
22+
class TestController {
23+
@get('/test')
24+
@authenticate(STRATEGY.SAML, {
25+
name: 'string',
26+
passReqToCallback: true,
27+
})
28+
test() {
29+
return 'test successful';
30+
}
31+
}
32+
33+
app.controller(TestController);
34+
35+
await whenIMakeRequestTo(server).get('/test').expect(302);
36+
});
37+
38+
function whenIMakeRequestTo(restServer: RestServer): Client {
39+
return createClientForHandler(restServer.requestHandler);
40+
}
41+
42+
async function givenAServer() {
43+
app = getApp();
44+
server = await app.getServer(RestServer);
45+
}
46+
47+
function getAuthVerifier() {
48+
app.bind(Strategies.SAML_VERIFIER).toProvider(SamlVerifyProvider);
49+
}
50+
51+
function givenAuthenticatedSequence() {
52+
// bind user defined sequence
53+
server.sequence(MyAuthenticationMiddlewareSequence);
54+
}
55+
});
56+
57+
class SamlVerifyProvider implements Provider<VerifyFunction.SamlFn> {
58+
constructor() {
59+
//this is intentional
60+
}
61+
62+
value(): VerifyFunction.SamlFn {
63+
return async (
64+
profile: SamlStrategy.Profile,
65+
cd: SamlStrategy.VerifiedCallback,
66+
req?: Request,
67+
) => {
68+
return userWithoutReqObj;
69+
};
70+
}
71+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {IAuthUser} from '../../../types';
2+
import {expect} from '@loopback/testlab';
3+
import * as SamlStrategy from 'passport-saml';
4+
import {
5+
SamlStrategyFactoryProvider,
6+
SamlStrategyFactory,
7+
} from '../../../strategies/SAML';
8+
import {StrategyOptions} from 'passport-saml/lib/passport-saml/types';
9+
10+
describe('getting saml strategy with options', () => {
11+
it('should return strategy by passing options and passReqToCallback as true', async () => {
12+
const strategyVerifier: SamlStrategyFactory = await getStrategy();
13+
14+
const options: StrategyOptions = {
15+
name: 'string',
16+
passReqToCallback: true,
17+
};
18+
19+
const SamlStrategyVerifier = strategyVerifier(options);
20+
21+
expect(SamlStrategyVerifier).to.have.property('name');
22+
expect(SamlStrategyVerifier)
23+
.to.have.property('authenticate')
24+
.which.is.a.Function();
25+
});
26+
27+
it('should return strategy by passing options and passReqToCallback as false', async () => {
28+
const strategyVerifier: SamlStrategyFactory = await getStrategy();
29+
30+
const options: StrategyOptions = {
31+
name: 'string',
32+
passReqToCallback: false,
33+
};
34+
35+
const SamlStrategyVerifier = strategyVerifier(options);
36+
37+
expect(SamlStrategyVerifier).to.have.property('name');
38+
expect(SamlStrategyVerifier)
39+
.to.have.property('authenticate')
40+
.which.is.a.Function();
41+
});
42+
});
43+
44+
async function getStrategy() {
45+
const provider = new SamlStrategyFactoryProvider(verifierBearer);
46+
47+
//this fuction will return a function which will then accept options.
48+
return provider.value();
49+
}
50+
51+
//returning a user
52+
function verifierBearer(
53+
profile: SamlStrategy.Profile,
54+
): Promise<IAuthUser | null> {
55+
const userToPass: IAuthUser = {
56+
id: 1,
57+
username: 'xyz',
58+
password: 'pass',
59+
};
60+
61+
return new Promise(function (resolve, reject) {
62+
if (userToPass) {
63+
resolve(userToPass);
64+
}
65+
});
66+
}

src/component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import {
3737
ResourceOwnerVerifyProvider,
3838
PassportOtpStrategyFactoryProvider,
3939
OtpVerifyProvider,
40+
SamlStrategyFactoryProvider,
41+
SamlVerifyProvider,
4042
} from './strategies';
4143
import {Strategies} from './strategies/keys';
4244
import {
@@ -84,6 +86,7 @@ export class AuthenticationComponent implements Component {
8486
KeycloakStrategyFactoryProvider,
8587
[Strategies.Passport.COGNITO_OAUTH2_STRATEGY_FACTORY.key]:
8688
CognitoStrategyFactoryProvider,
89+
[Strategies.SAML_STRATEGY_FACTORY.key]: SamlStrategyFactoryProvider,
8790

8891
// Verifier functions
8992
[Strategies.Passport.OAUTH2_CLIENT_PASSWORD_VERIFIER.key]:
@@ -106,6 +109,7 @@ export class AuthenticationComponent implements Component {
106109
[Strategies.Passport.APPLE_OAUTH2_VERIFIER.key]: AppleAuthVerifyProvider,
107110
[Strategies.Passport.AZURE_AD_VERIFIER.key]: AzureADAuthVerifyProvider,
108111
[Strategies.Passport.KEYCLOAK_VERIFIER.key]: KeycloakVerifyProvider,
112+
[Strategies.SAML_VERIFIER.key]: SamlVerifyProvider,
109113
};
110114
this.bindings = [];
111115
if (this.config?.useClientAuthenticationMiddleware) {

src/strategies/SAML/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './saml-strategy-factory-provider';
2+
export * from './saml-verify.provider';
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// SONAR-IGNORE-ALL
2+
import {inject, Provider} from '@loopback/core';
3+
import {HttpErrors, Request} from '@loopback/rest';
4+
import {AnyObject} from '@loopback/repository';
5+
import {HttpsProxyAgent} from 'https-proxy-agent';
6+
import {Profile, Strategy, VerifiedCallback} from 'passport-saml';
7+
import {
8+
SamlConfig,
9+
StrategyOptions,
10+
} from 'passport-saml/lib/passport-saml/types';
11+
import {AuthErrorKeys} from '../../error-keys';
12+
import {Strategies} from '../../keys';
13+
import {VerifyFunction} from '../../types';
14+
15+
export interface SamlStrategyFactory {
16+
(options: StrategyOptions, verifierPassed?: VerifyFunction.SamlFn): Strategy;
17+
}
18+
19+
export class SamlStrategyFactoryProvider
20+
implements Provider<SamlStrategyFactory>
21+
{
22+
constructor(
23+
@inject(Strategies.SAML_VERIFIER)
24+
private readonly verifierSaml: VerifyFunction.SamlFn,
25+
) {}
26+
27+
value(): SamlStrategyFactory {
28+
return (options, verifier) =>
29+
this.getSamlStrategyVerifier(options, verifier);
30+
}
31+
32+
getSamlStrategyVerifier(
33+
options: StrategyOptions,
34+
verifierPassed?: VerifyFunction.SamlFn,
35+
): Strategy {
36+
const verifyFn = verifierPassed ?? this.verifierSaml;
37+
let strategy;
38+
const func = async (
39+
req: Request,
40+
profile: Profile | null | undefined,
41+
cb: VerifiedCallback,
42+
) => {
43+
try {
44+
const user = await verifyFn(profile, cb, req);
45+
if (!user) {
46+
throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
47+
}
48+
cb(null, user as unknown as Record<string, unknown>);
49+
} catch (err) {
50+
cb(err);
51+
}
52+
};
53+
if (options && options.passReqToCallback === true) {
54+
strategy = new Strategy(
55+
options as SamlConfig,
56+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
57+
func,
58+
);
59+
} else {
60+
strategy = new Strategy(
61+
options as SamlConfig,
62+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
63+
async (profile: Profile | null | undefined, cb: VerifiedCallback) => {
64+
try {
65+
const user = await verifyFn(profile, cb);
66+
if (!user) {
67+
throw new HttpErrors.Unauthorized(
68+
AuthErrorKeys.InvalidCredentials,
69+
);
70+
}
71+
cb(null, user as unknown as Record<string, unknown>);
72+
} catch (err) {
73+
cb(err);
74+
}
75+
},
76+
);
77+
}
78+
this._setupProxy(strategy);
79+
return strategy;
80+
}
81+
82+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
83+
private _setupProxy(strategy: AnyObject) {
84+
// Setup proxy if any
85+
let httpsProxyAgent;
86+
if (process.env['https_proxy']) {
87+
httpsProxyAgent = new HttpsProxyAgent(process.env['https_proxy']);
88+
strategy._oauth2.setAgent(httpsProxyAgent);
89+
} else if (process.env['HTTPS_PROXY']) {
90+
httpsProxyAgent = new HttpsProxyAgent(process.env['HTTPS_PROXY']);
91+
strategy._oauth2.setAgent(httpsProxyAgent);
92+
} else {
93+
//this is intentional
94+
}
95+
}
96+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {Provider} from '@loopback/context';
2+
import {HttpErrors, Request} from '@loopback/rest';
3+
4+
import * as SamlStrategy from 'passport-saml';
5+
6+
import {VerifyFunction} from '../../types';
7+
8+
/**
9+
* A provider for default implementation of VerifyFunction.LocalPasswordFn
10+
*
11+
* It will just throw an error saying Not Implemented
12+
*/
13+
export class SamlVerifyProvider implements Provider<VerifyFunction.SamlFn> {
14+
constructor() {
15+
//This is intentional
16+
}
17+
18+
value(): VerifyFunction.SamlFn {
19+
return async (
20+
profile: SamlStrategy.Profile,
21+
cb: SamlStrategy.VerifiedCallback,
22+
req?: Request,
23+
) => {
24+
throw new HttpErrors.NotImplemented(
25+
`VerifyFunction.SamlFn is not implemented`,
26+
);
27+
};
28+
}
29+
}

0 commit comments

Comments
 (0)