-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
1110855
commit decb68a
Showing
14 changed files
with
397 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
71
src/__tests__/integration/action-sequence/saml/saml.integration.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
src/__tests__/integration/middleware-sequence/saml/saml.integration.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
66
src/__tests__/unit/saml-strategy.unit.ts/saml-strategy.unit.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`, | ||
); | ||
}; | ||
} | ||
} |
Oops, something went wrong.