diff --git a/README.md b/README.md index d782b97..db26f01 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ It provides support for seven passport based strategies. 8. [passport-apple](https://github.com/ananay/passport-apple) - Passport strategy for authenticating with Apple using the Apple OAuth 2.0 API. This module lets you authenticate using Apple in your Node.js applications. 9. [passport-facebook](https://github.com/jaredhanson/passport-facebook) - Passport strategy for authenticating with Facebook using the Facebook OAuth 2.0 API. This module lets you authenticate using Facebook in your Node.js applications. 10. [passport-cognito-oauth2](https://github.com/ebuychance/passport-cognito-oauth2) - Passport strategy for authenticating with Cognito using the Cognito OAuth 2.0 API. This module lets you authenticate using Cognito in your Node.js applications. -11. custom-passport-otp - Created a Custom Passport strategy for 2-Factor-Authentication using OTP (One Time Password). +11. [passport-SAML](https://github.com/node-saml/passport-saml) - Passport strategy for authenticating with SAML using the SAML 2.0 API. This module lets you authenticate using SAML in your Node.js applications +12. custom-passport-otp - Created a Custom Passport strategy for 2-Factor-Authentication using OTP (One Time Password). You can use one or more strategies of the above in your application. For each of the strategy (only which you use), you just need to provide your own verifier function, making it easily configurable. Rest of the strategy implementation intricacies is handled by extension. @@ -2595,6 +2596,312 @@ this.bind(VerifyBindings.BEARER_SIGNUP_VERIFY_PROVIDER).toProvider( ); ``` +### SAML + +SAML (Security Assertion Markup Language) is an XML-based standard for exchanging authentication and authorization data between parties, in particular, between an identity provider (IdP) and a service provider (SP). + +First, create a AuthUser model implementing the IAuthUser interface. You can implement the interface in the user model itself. See sample below. + +```ts +@model({ + name: 'users', +}) +export class User extends Entity implements IAuthUser { + @property({ + type: 'number', + id: true, + }) + id?: number; + @property({ + type: 'string', + required: true, + name: 'first_name', + }) + firstName: string; + @property({ + type: 'string', + name: 'last_name', + }) + lastName: string; + @property({ + type: 'string', + name: 'middle_name', + }) + middleName?: string; + @property({ + type: 'string', + required: true, + }) + username: string; + @property({ + type: 'string', + }) + email?: string; + // Auth provider - 'SAML' + @property({ + type: 'string', + required: true, + name: 'auth_provider', + }) + authProvider: string; + // Id from external provider + @property({ + type: 'string', + name: 'auth_id', + }) + authId?: string; + @property({ + type: 'string', + name: 'auth_token', + }) + authToken?: string; + @property({ + type: 'string', + }) + password?: string; + constructor(data?: Partial) { + super(data); + } +} +``` + +Now bind this model to USER_MODEL key in application.ts + +```ts +this.bind(AuthenticationBindings.USER_MODEL).to(User); +``` + +Create CRUD repository for the above model. Use loopback CLI. + +```sh +lb4 repository +``` + +Add the verifier function for the strategy. You need to create a provider for the same. You can add your application specific business logic for client auth here. Here is a simple example. + +```ts +import {Provider} from '@loopback/context'; +import {repository} from '@loopback/repository'; +import {HttpErrors} from '@loopback/rest'; +import {AuthErrorKeys, VerifyFunction} from 'loopback4-authentication'; +import {Tenant} from '../../../models'; +import {UserCredentialsRepository, UserRepository} from '../../../repositories'; +import {AuthUser} from '../models/auth-user.model'; +export class SamlVerifyProvider implements Provider { + constructor( + @repository(UserRepository) + public userRepository: UserRepository, + @repository(UserCredentialsRepository) + public userCredsRepository: UserCredentialsRepository, + ) {} + value(): VerifyFunction.SamlFn { + return async (profile) => { + const user = await this.userRepository.findOne({ + where: { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + email: (profile as any)._json.email, + }, + }); + if (!user) { + throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials); + } + if (!user || user.authProvider !== 'saml' || user.authId !== profile.id) { + throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials); + } + const authUser: AuthUser = new AuthUser({ + ...user, + id: user.id as string, + }); + authUser.permissions = []; + return this.postVerifyProvider(profile, authUser); + }; + } +} +``` + +Please note the Verify function type _VerifyFunction.LocalPasswordFn_ + +Now bind this provider to the application in application.ts. + +```ts +import {AuthenticationComponent, Strategies} from 'loopback4-authentication'; +``` + +```ts +// Add authentication component +this.component(AuthenticationComponent); +// Customize authentication verify handlers +this.bind(Strategies.Passport.SAML_VERIFIER).toProvider(SamlVerifyProvider); +``` + +Finally, add the authenticate function as a sequence action to sequence.ts. + +```ts +export class MySequence implements SequenceHandler { + constructor( + @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, + @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, + @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, + @inject(SequenceActions.SEND) public send: Send, + @inject(SequenceActions.REJECT) public reject: Reject, + @inject(AuthenticationBindings.USER_AUTH_ACTION) + protected authenticateRequest: AuthenticateFn, + ) {} + async handle(context: RequestContext) { + try { + const {request, response} = context; + const route = this.findRoute(request); + const args = await this.parseParams(request, route); + request.body = args[args.length - 1]; + const authUser: AuthUser = await this.authenticateRequest( + request, + response, + ); + const result = await this.invoke(route, args); + this.send(response, result); + } catch (err) { + this.reject(context, err); + } + } +} +``` + +After this, you can use decorator to apply auth to controller functions wherever needed. See below. + +```ts + @authenticateClient(STRATEGY.CLIENT_PASSWORD) + @authenticate( + STRATEGY.SAML, + { + accessType: 'offline', + scope: ['profile', 'email'], + authorizationURL: process.env.SAML_URL, + callbackURL: process.env.SAML_CALLBACK_URL, + clientID: process.env.SAML_CLIENT_ID, + clientSecret: process.env.SAML_CLIENT_SECRET, + tokenURL: process.env.SAML_TOKEN_URL, + }, + queryGen('body'), + ) + @authorize({permissions: ['*']}) + @post('/auth/saml', { + description: 'POST Call for saml based login', + responses: { + [STATUS_CODE.OK]: { + description: 'Saml Token Response', + content: { + [CONTENT_TYPE.JSON]: { + schema: {[X_TS_TYPE]: TokenResponse}, + }, + }, + }, + }, + }) + async postLoginViaSaml( + @requestBody({ + content: { + [CONTENT_TYPE.FORM_URLENCODED]: { + schema: getModelSchemaRef(ClientAuthRequest), + }, + }, + }) + clientCreds?: ClientAuthRequest, //NOSONAR + ): Promise { + //do nothing + } + @authenticate( + STRATEGY.SAML, + { + accessType: 'offline', + scope: ['profile', 'email'], + authorizationURL: process.env.SAML_URL, + callbackURL: process.env.SAML_CALLBACK_URL, + clientID: process.env.SAML_CLIENT_ID, + clientSecret: process.env.SAML_CLIENT_SECRET, + tokenURL: process.env.SAML_TOKEN_URL, + }, + queryGen('query'), + ) + @authorize({permissions: ['*']}) + @get('/auth/saml-redirect', { + responses: { + [STATUS_CODE.OK]: { + description: 'Saml Redirect Token Response', + content: { + [CONTENT_TYPE.JSON]: { + schema: {[X_TS_TYPE]: TokenResponse}, + }, + }, + }, + }, + }) + async samlCallback( + @param.query.string('code') code: string, //NOSONAR + @param.query.string('state') state: string, + @param.query.string('session_state') sessionState: string, //NOSONAR + @inject(RestBindings.Http.RESPONSE) response: Response, + @inject(AuthenticationBindings.CURRENT_USER) + user: AuthUser | undefined, + ): Promise { + const clientId = new URLSearchParams(state).get('client_id'); + if (!clientId || !user) { + throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid); + } + const client = await this.authClientRepository.findOne({ + where: { + clientId, + }, + }); + if (!client?.redirectUrl) { + throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid); + } + try { + const token = await this.getAuthCode(client, user); + response.redirect(`${client.redirectUrl}?code=${token}`); + } catch (error) { + this.logger.error(error); + throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials); + } + } +``` + +Please note above that we are creating two new APIs for SAML. The first one is for UI clients to hit. We are authenticating client as well, then passing the details to the SAML. Then, the actual authentication is done by SAML authorization url, which redirects to the second API we created after success. The first API method body is empty as we do not need to handle its response. The SAML provider in this package will do the redirection for you automatically. + +For accessing the authenticated AuthUser model reference, you can inject the CURRENT_USER provider, provided by the extension, which is populated by the auth action sequence above. + +```ts + @inject.getter(AuthenticationBindings.CURRENT_USER) + private readonly getCurrentUser: Getter, +``` + +The `logoutVerify` function is used in the node-saml library as a part of the Passport SAML authentication process. This function is used to verify the authenticity of a SAML logout request. +The logout process in SAML is used to end the user's session on the service provider, and the logoutVerify function is used to verify that the logout request is coming from a trusted IdP. +The implementation of the logoutVerify function may vary depending on the specific requirements and the security constraints of the application. It is typically used to verify the signature on the logout request, to ensure that the request has not been tampered with, and to extract the user's identity information from the request. + +```ts +function logoutVerify( + req: Request, + profile: Profile | null, + done: VerifiedCallback, +): void { + // Check if a user is currently authenticated + if (req.isAuthenticated()) { + // Log the user out by removing their session data + req.logout(done); + // Call the "done" callback to indicate success + done(null, {message: 'User successfully logged out'}); + } else { + // Call the "done" callback with an error to indicate that the user is not logged in + done(new Error('User is not currently logged in')); + } +} +``` + +This function is called when a user logs out of the application.Once this function is implemented,it will be called when the user logs out of the application,allowing the application to perform any necessary tasks before ending the user's session. +@param req - The request object. +@param {Profile | null} profile - The user's profile, as returned by the provider. +@param {VerifiedCallback} done - A callback to be called when the verificationis complete. + ### Https proxy support for keycloak and google auth If a https proxy agent is needed for keycloak and google auth, just add an environment variable named `HTTPS_PROXY` or `https_proxy` with proxy url as value. It will add that proxy agent to the request. diff --git a/package-lock.json b/package-lock.json index 7495cf8..dc7d6ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@exlinc/keycloak-passport": "^1.0.2", "@loopback/context": "^5.0.7", "@loopback/core": "^4.0.7", + "@node-saml/passport-saml": "^4.0.2", "ajv": "^8.11.0", "https-proxy-agent": "^5.0.0", "jsonwebtoken": "^9.0.0", diff --git a/package.json b/package.json index 5802fcc..fd201f4 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@exlinc/keycloak-passport": "^1.0.2", "@loopback/context": "^5.0.7", "@loopback/core": "^4.0.7", + "@node-saml/passport-saml": "^4.0.2", "ajv": "^8.11.0", "https-proxy-agent": "^5.0.0", "jsonwebtoken": "^9.0.0", diff --git a/src/__tests__/unit/saml-strategy.unit.ts/saml-strategy.unit.ts b/src/__tests__/unit/saml-strategy.unit.ts/saml-strategy.unit.ts new file mode 100644 index 0000000..6ce1b26 --- /dev/null +++ b/src/__tests__/unit/saml-strategy.unit.ts/saml-strategy.unit.ts @@ -0,0 +1,70 @@ +import {IAuthUser} from '../../../types'; +import {expect} from '@loopback/testlab'; +import * as SamlStrategy from '@node-saml/passport-saml'; +import { + SamlStrategyFactoryProvider, + SamlStrategyFactory, +} from '../../../strategies/SAML'; +import {SamlConfig} from '@node-saml/passport-saml'; + +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: SamlConfig = { + name: 'string', + passReqToCallback: true, + cert: 'string', + issuer: 'string', + }; + + 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: SamlConfig = { + name: 'string', + passReqToCallback: false, + cert: 'string', + issuer: 'string', + }; + + 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 { + const userToPass: IAuthUser = { + id: 1, + username: 'xyz', + password: 'pass', + }; + + return new Promise(function (resolve, reject) { + if (userToPass) { + resolve(userToPass); + } + }); +} diff --git a/src/component.ts b/src/component.ts index dffc27e..e746da8 100644 --- a/src/component.ts +++ b/src/component.ts @@ -43,6 +43,10 @@ import { CognitoAuthVerifyProvider, CognitoStrategyFactoryProvider, } from './strategies/passport/passport-cognito-oauth2'; +import { + SamlStrategyFactoryProvider, + SamlVerifyProvider, +} from './strategies/SAML'; import {AuthenticationConfig} from './types'; export class AuthenticationComponent implements Component { @@ -72,6 +76,8 @@ export class AuthenticationComponent implements Component { ResourceOwnerPasswordStrategyFactoryProvider, [Strategies.Passport.GOOGLE_OAUTH2_STRATEGY_FACTORY.key]: GoogleAuthStrategyFactoryProvider, + [Strategies.Passport.SAML_STRATEGY_FACTORY.key]: + SamlStrategyFactoryProvider, [Strategies.Passport.INSTAGRAM_OAUTH2_STRATEGY_FACTORY.key]: InstagramAuthStrategyFactoryProvider, [Strategies.Passport.FACEBOOK_OAUTH2_STRATEGY_FACTORY.key]: @@ -97,6 +103,7 @@ export class AuthenticationComponent implements Component { ResourceOwnerVerifyProvider, [Strategies.Passport.GOOGLE_OAUTH2_VERIFIER.key]: GoogleAuthVerifyProvider, + [Strategies.Passport.SAML_VERIFIER.key]: SamlVerifyProvider, [Strategies.Passport.INSTAGRAM_OAUTH2_VERIFIER.key]: InstagramAuthVerifyProvider, [Strategies.Passport.FACEBOOK_OAUTH2_VERIFIER.key]: diff --git a/src/strategies/SAML/index.ts b/src/strategies/SAML/index.ts new file mode 100644 index 0000000..8792d4e --- /dev/null +++ b/src/strategies/SAML/index.ts @@ -0,0 +1,2 @@ +export * from './saml-strategy-factory-provider'; +export * from './saml-verify.provider'; diff --git a/src/strategies/SAML/saml-strategy-factory-provider.ts b/src/strategies/SAML/saml-strategy-factory-provider.ts new file mode 100644 index 0000000..28c111e --- /dev/null +++ b/src/strategies/SAML/saml-strategy-factory-provider.ts @@ -0,0 +1,115 @@ +// 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, + SamlConfig, + VerifyWithRequest, + VerifyWithoutRequest, +} from '@node-saml/passport-saml'; +import {AuthErrorKeys} from '../../error-keys'; +import {Strategies} from '../../keys'; +import {VerifyFunction} from '../../types'; +export interface SamlStrategyFactory { + (options: SamlConfig, verifierPassed?: VerifyFunction.SamlFn): Strategy; +} + +export class SamlStrategyFactoryProvider + implements Provider +{ + constructor( + @inject(Strategies.Passport.SAML_VERIFIER) + private readonly verifierSaml: VerifyFunction.SamlFn, + ) {} + + value(): SamlStrategyFactory { + return (options, verifier) => + this.getSamlStrategyVerifier(options, verifier); + } + + getSamlStrategyVerifier( + options: SamlConfig, + 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); + } catch (err) { + cb(err); + } + }; + if (options && options.passReqToCallback === true) { + strategy = new Strategy( + options, + logoutVerify as VerifyWithRequest, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + func, + ); + } else { + strategy = new Strategy( + options, + logoutVerify as unknown as VerifyWithoutRequest, + // 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); + } catch (err) { + cb(err); + } + }, + ); + } + this._setupProxy(strategy); + return strategy; + } + + 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 + } + } +} +function logoutVerify( + req: Request, + profile: Profile | null, + done: VerifiedCallback, +): void { + // Check if a user is currently authenticated + if (req.isAuthenticated()) { + // Log the user out by removing their session data + req.logout(done); + // Call the "done" callback to indicate success + done(null, {message: 'User successfully logged out'}); + } else { + // Call the "done" callback with an error to indicate that the user is not logged in + done(new Error('User is not currently logged in')); + } +} diff --git a/src/strategies/SAML/saml-verify.provider.ts b/src/strategies/SAML/saml-verify.provider.ts new file mode 100644 index 0000000..e6c1394 --- /dev/null +++ b/src/strategies/SAML/saml-verify.provider.ts @@ -0,0 +1,29 @@ +import {Provider} from '@loopback/context'; +import {HttpErrors, Request} from '@loopback/rest'; + +import * as SamlStrategy from '@node-saml/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 { + 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`, + ); + }; + } +} diff --git a/src/strategies/index.ts b/src/strategies/index.ts index 6e98671..758aab5 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -2,3 +2,4 @@ export * from './client-auth-strategy.provider'; export * from './user-auth-strategy.provider'; export * from './passport'; export * from './types'; +export * from './SAML'; diff --git a/src/strategies/keys.ts b/src/strategies/keys.ts index 065c6ed..3c429cf 100644 --- a/src/strategies/keys.ts +++ b/src/strategies/keys.ts @@ -11,6 +11,7 @@ import {InstagramAuthStrategyFactoryProvider} from './passport'; import {AppleAuthStrategyFactoryProvider} from './passport/passport-apple-oauth2'; import {FacebookAuthStrategyFactoryProvider} from './passport/passport-facebook-oauth2'; import {CognitoStrategyFactoryProvider} from './passport/passport-cognito-oauth2'; +import {SamlStrategyFactoryProvider} from './SAML'; export namespace Strategies { export namespace Passport { @@ -132,5 +133,13 @@ export namespace Strategies { BindingKey.create( 'sf.passport.verifier.cognitoOauth2', ); + // SAML strategy + export const SAML_STRATEGY_FACTORY = + BindingKey.create( + 'sf.passport.strategyFactory.saml', + ); + export const SAML_VERIFIER = BindingKey.create( + 'sf.passport.verifier.saml', + ); } } diff --git a/src/strategies/types/types.ts b/src/strategies/types/types.ts index 95f0f8b..fcd3f77 100644 --- a/src/strategies/types/types.ts +++ b/src/strategies/types/types.ts @@ -4,6 +4,7 @@ import * as AzureADStrategy from 'passport-azure-ad'; import * as InstagramStrategy from 'passport-instagram'; import * as FacebookStrategy from 'passport-facebook'; import * as AppleStrategy from 'passport-apple'; +import * as SamlStrategy from '@node-saml/passport-saml'; import {DecodedIdToken} from 'passport-apple'; import {Cognito, IAuthClient, IAuthUser} from '../../types'; import {Keycloak} from './keycloak.types'; @@ -115,6 +116,13 @@ export namespace VerifyFunction { req?: Request, ): Promise; } + export interface SamlFn extends GenericAuthFn { + ( + profile: SamlStrategy.Profile, + cb: SamlStrategy.VerifiedCallback, + req?: Request, + ): Promise; + } // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface GenericAuthFn { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/strategies/user-auth-strategy.provider.ts b/src/strategies/user-auth-strategy.provider.ts index 6e8d7a7..3026392 100644 --- a/src/strategies/user-auth-strategy.provider.ts +++ b/src/strategies/user-auth-strategy.provider.ts @@ -7,6 +7,7 @@ import * as PassportLocal from 'passport-local'; import * as InstagramStrategy from 'passport-instagram'; import * as FacebookStrategy from 'passport-facebook'; import * as AppleStrategy from 'passport-apple'; +import {SamlConfig} from '@node-saml/passport-saml'; import {AuthenticationBindings} from '../keys'; import {STRATEGY} from '../strategy-name.enum'; import {AuthenticationMetadata} from '../types'; @@ -29,6 +30,7 @@ import { CognitoAuthStrategyFactory, } from './passport'; import {Cognito, Keycloak, VerifyFunction} from './types'; +import {SamlStrategyFactory} from './SAML'; interface ExtendedStrategyOption extends FacebookStrategy.StrategyOption { passReqToCallback?: false; @@ -48,6 +50,8 @@ export class AuthStrategyProvider implements Provider { private readonly getResourceOwnerVerifier: ResourceOwnerPasswordStrategyFactory, @inject(Strategies.Passport.GOOGLE_OAUTH2_STRATEGY_FACTORY) private readonly getGoogleAuthVerifier: GoogleAuthStrategyFactory, + @inject(Strategies.Passport.SAML_STRATEGY_FACTORY) + private readonly getSamlVerifier: SamlStrategyFactory, @inject(Strategies.Passport.AZURE_AD_STRATEGY_FACTORY) private readonly getAzureADAuthVerifier: AzureADAuthStrategyFactory, @inject(Strategies.Passport.KEYCLOAK_STRATEGY_FACTORY) @@ -145,6 +149,11 @@ export class AuthStrategyProvider implements Provider { this.metadata.options as Otp.StrategyOptions, verifier as VerifyFunction.OtpAuthFn, ); + } else if (name === STRATEGY.SAML) { + return this.getSamlVerifier( + this.metadata.options as SamlConfig, + verifier as VerifyFunction.SamlFn, + ); } else { return Promise.reject(`The strategy ${name} is not available.`); } diff --git a/src/strategy-adapter.ts b/src/strategy-adapter.ts index 4f0201f..2356750 100644 --- a/src/strategy-adapter.ts +++ b/src/strategy-adapter.ts @@ -67,7 +67,7 @@ export class StrategyAdapter { }; // authenticate - strategy.authenticate(request, options); + strategy.authenticate(request, options ?? {}); }); } } diff --git a/src/strategy-name.enum.ts b/src/strategy-name.enum.ts index cad2ebb..3fe114d 100644 --- a/src/strategy-name.enum.ts +++ b/src/strategy-name.enum.ts @@ -11,4 +11,5 @@ export const enum STRATEGY { AZURE_AD = 'Azure AD', KEYCLOAK = 'keycloak', OTP = 'otp', + SAML = 'saml', }