Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(provider): add SAML passport authentication strategy #113

Merged
merged 7 commits into from
Jan 20, 2023
29 changes: 29 additions & 0 deletions src/__tests__/fixtures/providers/saml.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {Provider} from '@loopback/core';
import {VerifyFunction} from '../../../strategies';
import * as SamlStrategy from 'passport-saml';
import {IAuthUser} from '../../../types';
import {Request} from '@loopback/rest';

export class BearerTokenVerifyProvider
implements Provider<VerifyFunction.SamlFn>
{
constructor() {
//this is intentional
}

value(): VerifyFunction.SamlFn {
return async (
profile: SamlStrategy.Profile,
cb: SamlStrategy.VerifiedCallback,
req?: Request,
) => {
const userToPass: IAuthUser = {
id: 1,
username: 'xyz',
password: 'pass',
};

return userToPass;
};
}
}
71 changes: 71 additions & 0 deletions src/__tests__/integration/action-sequence/saml/saml.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {Client, createClientForHandler} from '@loopback/testlab';
import {RestServer, Request} from '@loopback/rest';
import {Application, Provider} from '@loopback/core';
import {get} from '@loopback/openapi-v3';
import {authenticate} from '../../../../decorators';
import {STRATEGY} from '../../../../strategy-name.enum';
import {getApp} from '../helpers/helpers';
import {MyAuthenticationSequence} from '../../../fixtures/sequences/authentication.sequence';
import {Strategies} from '../../../../strategies/keys';
import {VerifyFunction} from '../../../../strategies';
import {userWithoutReqObj} from '../../../fixtures/data/bearer-data';
import * as SamlStrategy from 'passport-saml';

describe('getting saml strategy with options', () => {
let app: Application;
let server: RestServer;
beforeEach(givenAServer);
beforeEach(givenAuthenticatedSequence);
beforeEach(getAuthVerifier);

it('should return 302 when name is passed and passReqToCallback is set true', async () => {
class TestController {
@get('/test')
@authenticate(STRATEGY.SAML, {
name: 'string',
passReqToCallback: true,
})
test() {
return 'test successful';
}
}

app.controller(TestController);

await whenIMakeRequestTo(server).get('/test').expect(302);
});

function whenIMakeRequestTo(restServer: RestServer): Client {
return createClientForHandler(restServer.requestHandler);
}

async function givenAServer() {
app = getApp();
server = await app.getServer(RestServer);
}

function getAuthVerifier() {
app.bind(Strategies.SAML_VERIFIER).toProvider(SamlVerifyProvider);
}

function givenAuthenticatedSequence() {
// bind user defined sequence
server.sequence(MyAuthenticationSequence);
}
});

class SamlVerifyProvider implements Provider<VerifyFunction.SamlFn> {
constructor() {
//this is intentional
}

value(): VerifyFunction.SamlFn {
return async (
profile: SamlStrategy.Profile,
cd: SamlStrategy.VerifiedCallback,
req?: Request,
) => {
return userWithoutReqObj;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {Client, createClientForHandler} from '@loopback/testlab';
import {RestServer, Request} from '@loopback/rest';
import {Application, Provider} from '@loopback/core';
import {get} from '@loopback/openapi-v3';
import {authenticate} from '../../../../decorators';
import {STRATEGY} from '../../../../strategy-name.enum';
import {getApp} from '../helpers/helpers';
import {MyAuthenticationMiddlewareSequence} from '../../../fixtures/sequences/authentication-middleware.sequence';
import {Strategies} from '../../../../strategies/keys';
import {VerifyFunction} from '../../../../strategies';
import {userWithoutReqObj} from '../../../fixtures/data/bearer-data';
import * as SamlStrategy from 'passport-saml';

describe('getting saml strategy with options using Middleware Sequence', () => {
let app: Application;
let server: RestServer;
beforeEach(givenAServer);
beforeEach(givenAuthenticatedSequence);
beforeEach(getAuthVerifier);

it('should return 302 when name is passed and passReqToCallback is set true', async () => {
class TestController {
@get('/test')
@authenticate(STRATEGY.SAML, {
name: 'string',
passReqToCallback: true,
})
test() {
return 'test successful';
}
}

app.controller(TestController);

await whenIMakeRequestTo(server).get('/test').expect(302);
});

function whenIMakeRequestTo(restServer: RestServer): Client {
return createClientForHandler(restServer.requestHandler);
}

async function givenAServer() {
app = getApp();
server = await app.getServer(RestServer);
}

function getAuthVerifier() {
app.bind(Strategies.SAML_VERIFIER).toProvider(SamlVerifyProvider);
}

function givenAuthenticatedSequence() {
// bind user defined sequence
server.sequence(MyAuthenticationMiddlewareSequence);
}
});

class SamlVerifyProvider implements Provider<VerifyFunction.SamlFn> {
constructor() {
//this is intentional
}

value(): VerifyFunction.SamlFn {
return async (
profile: SamlStrategy.Profile,
cd: SamlStrategy.VerifiedCallback,
req?: Request,
) => {
return userWithoutReqObj;
};
}
}
66 changes: 66 additions & 0 deletions src/__tests__/unit/saml-strategy.unit.ts/saml-strategy.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {IAuthUser} from '../../../types';
import {expect} from '@loopback/testlab';
import * as SamlStrategy from 'passport-saml';
import {
SamlStrategyFactoryProvider,
SamlStrategyFactory,
} from '../../../strategies/SAML';
import {StrategyOptions} from 'passport-saml/lib/passport-saml/types';

describe('getting saml strategy with options', () => {
it('should return strategy by passing options and passReqToCallback as true', async () => {
const strategyVerifier: SamlStrategyFactory = await getStrategy();

const options: StrategyOptions = {
name: 'string',
passReqToCallback: true,
};

const SamlStrategyVerifier = strategyVerifier(options);

expect(SamlStrategyVerifier).to.have.property('name');
expect(SamlStrategyVerifier)
.to.have.property('authenticate')
.which.is.a.Function();
});

it('should return strategy by passing options and passReqToCallback as false', async () => {
const strategyVerifier: SamlStrategyFactory = await getStrategy();

const options: StrategyOptions = {
name: 'string',
passReqToCallback: false,
};

const SamlStrategyVerifier = strategyVerifier(options);

expect(SamlStrategyVerifier).to.have.property('name');
expect(SamlStrategyVerifier)
.to.have.property('authenticate')
.which.is.a.Function();
});
});

async function getStrategy() {
const provider = new SamlStrategyFactoryProvider(verifierBearer);

//this fuction will return a function which will then accept options.
return provider.value();
}

//returning a user
function verifierBearer(
profile: SamlStrategy.Profile,
): Promise<IAuthUser | null> {
const userToPass: IAuthUser = {
id: 1,
username: 'xyz',
password: 'pass',
};

return new Promise(function (resolve, reject) {
if (userToPass) {
resolve(userToPass);
}
});
}
4 changes: 4 additions & 0 deletions src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
ResourceOwnerVerifyProvider,
PassportOtpStrategyFactoryProvider,
OtpVerifyProvider,
SamlStrategyFactoryProvider,
SamlVerifyProvider,
} from './strategies';
import {Strategies} from './strategies/keys';
import {
Expand Down Expand Up @@ -84,6 +86,7 @@ export class AuthenticationComponent implements Component {
KeycloakStrategyFactoryProvider,
[Strategies.Passport.COGNITO_OAUTH2_STRATEGY_FACTORY.key]:
CognitoStrategyFactoryProvider,
[Strategies.SAML_STRATEGY_FACTORY.key]: SamlStrategyFactoryProvider,

// Verifier functions
[Strategies.Passport.OAUTH2_CLIENT_PASSWORD_VERIFIER.key]:
Expand All @@ -106,6 +109,7 @@ export class AuthenticationComponent implements Component {
[Strategies.Passport.APPLE_OAUTH2_VERIFIER.key]: AppleAuthVerifyProvider,
[Strategies.Passport.AZURE_AD_VERIFIER.key]: AzureADAuthVerifyProvider,
[Strategies.Passport.KEYCLOAK_VERIFIER.key]: KeycloakVerifyProvider,
[Strategies.SAML_VERIFIER.key]: SamlVerifyProvider,
};
this.bindings = [];
if (this.config?.useClientAuthenticationMiddleware) {
Expand Down
2 changes: 2 additions & 0 deletions src/strategies/SAML/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './saml-strategy-factory-provider';
export * from './saml-verify.provider';
96 changes: 96 additions & 0 deletions src/strategies/SAML/saml-strategy-factory-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SONAR-IGNORE-ALL
import {inject, Provider} from '@loopback/core';
import {HttpErrors, Request} from '@loopback/rest';
import {AnyObject} from '@loopback/repository';
import {HttpsProxyAgent} from 'https-proxy-agent';
import {Profile, Strategy, VerifiedCallback} from 'passport-saml';
import {
SamlConfig,
StrategyOptions,
} from 'passport-saml/lib/passport-saml/types';
import {AuthErrorKeys} from '../../error-keys';
import {Strategies} from '../../keys';
import {VerifyFunction} from '../../types';

export interface SamlStrategyFactory {
(options: StrategyOptions, verifierPassed?: VerifyFunction.SamlFn): Strategy;
}

export class SamlStrategyFactoryProvider
implements Provider<SamlStrategyFactory>
{
constructor(
@inject(Strategies.SAML_VERIFIER)
private readonly verifierSaml: VerifyFunction.SamlFn,
) {}

value(): SamlStrategyFactory {
return (options, verifier) =>
this.getSamlStrategyVerifier(options, verifier);
}

getSamlStrategyVerifier(
options: StrategyOptions,
verifierPassed?: VerifyFunction.SamlFn,
): Strategy {
const verifyFn = verifierPassed ?? this.verifierSaml;
let strategy;
const func = async (
req: Request,
profile: Profile | null | undefined,
cb: VerifiedCallback,
) => {
try {
const user = await verifyFn(profile, cb, req);
if (!user) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
}
cb(null, user as unknown as Record<string, unknown>);
} catch (err) {
cb(err);
}
};
if (options && options.passReqToCallback === true) {
RaghavaroraSF marked this conversation as resolved.
Show resolved Hide resolved
strategy = new Strategy(
options as SamlConfig,
// eslint-disable-next-line @typescript-eslint/no-misused-promises
func,
);
} else {
strategy = new Strategy(
options as SamlConfig,
// eslint-disable-next-line @typescript-eslint/no-misused-promises
async (profile: Profile | null | undefined, cb: VerifiedCallback) => {
try {
const user = await verifyFn(profile, cb);
if (!user) {
throw new HttpErrors.Unauthorized(
AuthErrorKeys.InvalidCredentials,
);
}
cb(null, user as unknown as Record<string, unknown>);
} catch (err) {
cb(err);
}
},
);
}
this._setupProxy(strategy);
return strategy;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _setupProxy(strategy: AnyObject) {
// Setup proxy if any
let httpsProxyAgent;
if (process.env['https_proxy']) {
httpsProxyAgent = new HttpsProxyAgent(process.env['https_proxy']);
strategy._oauth2.setAgent(httpsProxyAgent);
} else if (process.env['HTTPS_PROXY']) {
httpsProxyAgent = new HttpsProxyAgent(process.env['HTTPS_PROXY']);
strategy._oauth2.setAgent(httpsProxyAgent);
} else {
//this is intentional
}
}
}
29 changes: 29 additions & 0 deletions src/strategies/SAML/saml-verify.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {Provider} from '@loopback/context';
import {HttpErrors, Request} from '@loopback/rest';

import * as SamlStrategy from 'passport-saml';

import {VerifyFunction} from '../../types';

/**
* A provider for default implementation of VerifyFunction.LocalPasswordFn
*
* It will just throw an error saying Not Implemented
*/
export class SamlVerifyProvider implements Provider<VerifyFunction.SamlFn> {
constructor() {
//This is intentional
}

value(): VerifyFunction.SamlFn {
return async (
profile: SamlStrategy.Profile,
cb: SamlStrategy.VerifiedCallback,
req?: Request,
) => {
throw new HttpErrors.NotImplemented(
`VerifyFunction.SamlFn is not implemented`,
);
};
}
}
Loading