Skip to content

Commit

Permalink
feat(provider): add SAML passport authentication strategy (#113)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
RaghavaroraSF committed Jan 20, 2023
1 parent 1110855 commit decb68a
Show file tree
Hide file tree
Showing 14 changed files with 397 additions and 1 deletion.
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) {
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`,
);
};
}
}

0 comments on commit decb68a

Please sign in to comment.