diff --git a/README.md b/README.md index d0500bc..838691a 100644 --- a/README.md +++ b/README.md @@ -2978,6 +2978,47 @@ this.component(AuthenticationComponent); This binding needs to be done before adding the Authentication component to your application. Apart from this all other steps for authentication for all strategies remain the same. +### Custom Sequence Support + +You can also configure `ClientAuthenticationMiddlewareProvider` and `UserAuthenticationMiddlewareProvider` options, which can be invoked using a custom sequence. See the sample below. + +`custom-sequence.ts` + +```ts title="custom-sequence.ts" +export class CustomSequence implements SequenceHandler { + @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) + protected invokeMiddleware: InvokeMiddleware = () => false; + ... + + async handle(context: RequestContext) { + ... + ... + // call custom registered middlewares in the pre-invoke chain + let finished = await this.invokeMiddleware(context, { + chain: CustomMiddlewareChain.PRE_INVOKE, + }); + if (finished) return; + const result = await this.invoke(route, args); + this.send(response, result); + ... + } +} +``` + +`application.ts` + +```ts title="application.ts" +import {ClientAuthenticationMiddlewareProvider} from 'loopback4-authentication'; +... +... + +// bind middleware with custom options +this.middleware(ClientAuthenticationMiddlewareProvider, { + chain: CustomMiddlwareChain.PRE_INVOKE +}); + +``` + ### Passport Auth0 In order to use it, run `npm install passport-auth0` and `npm install @types/passport-auth0`. diff --git a/docs/README.md b/docs/README.md index d0500bc..1dae3c1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2978,6 +2978,46 @@ this.component(AuthenticationComponent); This binding needs to be done before adding the Authentication component to your application. Apart from this all other steps for authentication for all strategies remain the same. +### Custom Sequence Support + +You can also configure `ClientAuthenticationMiddlewareProvider` and `UserAuthenticationMiddlewareProvider` options, which can be invoked using a custom sequence. See the sample below. + +`custom-sequence.ts` + +```ts title="custom-sequence.ts" +export class CustomSequence implements SequenceHandler { + @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) + protected invokeMiddleware: InvokeMiddleware = () => false; + ... + + async handle(context: RequestContext) { + ... + ... + // call custom registered middlewares in the pre-invoke chain + let finished = await this.invokeMiddleware(context, { + chain: CustomMiddlewareChain.PRE_INVOKE, + }); + if (finished) return; + const result = await this.invoke(route, args); + this.send(response, result); + ... + } +} +``` + +`application.ts` + +```ts title="application.ts" +import {ClientAuthenticationMiddlewareProvider} from 'loopback4-authentication'; +... +... +// bind middleware with custom options +this.middleware(ClientAuthenticationMiddlewareProvider, { + chain: CustomMiddlwareChain.PRE_INVOKE +}); + +``` + ### Passport Auth0 In order to use it, run `npm install passport-auth0` and `npm install @types/passport-auth0`. diff --git a/package-lock.json b/package-lock.json index 6cde87e..9f29c65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "loopback4-authentication", - "version": "12.1.0", + "version": "12.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "loopback4-authentication", - "version": "12.1.0", + "version": "12.1.1", "license": "MIT", "dependencies": { "@loopback/context": "^7.0.9", diff --git a/src/__tests__/fixtures/sequences/custom-middleware.sequence.ts b/src/__tests__/fixtures/sequences/custom-middleware.sequence.ts new file mode 100644 index 0000000..af3d54e --- /dev/null +++ b/src/__tests__/fixtures/sequences/custom-middleware.sequence.ts @@ -0,0 +1,68 @@ +import {inject} from '@loopback/context'; +import { + FindRoute, + InvokeMethod, + InvokeMiddleware, + ParseParams, + Reject, + RequestContext, + RestBindings, + Send, + SequenceHandler, +} from '@loopback/rest'; + +const SequenceActions = RestBindings.SequenceActions; +export const enum CustomMiddlewareChain { + PRE_INVOKE = 'pre-invoke', + POST_INVOKE = 'post-invoke', +} + +export class CustomSequence implements SequenceHandler { + @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) + protected invokeMiddleware: InvokeMiddleware = () => false; + + 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) protected send: Send, + @inject(SequenceActions.REJECT) protected reject: Reject, + ) {} + + 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]; + + // call custom registered middlewares in the pre-invoke chain + let finished = await this.invokeMiddleware(context, { + chain: CustomMiddlewareChain.PRE_INVOKE, + }); + if (finished) return; + + const result = await this.invoke(route, args); + + context.bind('invocation.result').to(result); + + // call custom registered middlewares in the post-invoke chain + finished = await this.invokeMiddleware(context, { + chain: CustomMiddlewareChain.POST_INVOKE, + }); + if (finished) return; + this.send(response, result); + } catch (error) { + if ( + error.code === 'AUTHENTICATION_STRATEGY_NOT_FOUND' || + error.code === 'USER_PROFILE_NOT_FOUND' + ) { + Object.assign(error, {statusCode: 401 /* Unauthorized */}); + } + this.reject(context, error); + return; + } + } +} diff --git a/src/__tests__/integration/custom-sequence/helpers/helpers.ts b/src/__tests__/integration/custom-sequence/helpers/helpers.ts new file mode 100644 index 0000000..16ce222 --- /dev/null +++ b/src/__tests__/integration/custom-sequence/helpers/helpers.ts @@ -0,0 +1,27 @@ +import {Application} from '@loopback/core'; +import {RestComponent, RestServer} from '@loopback/rest'; +import {AuthenticationComponent} from '../../../../component'; + +import { + ClientAuthenticationMiddlewareProvider, + UserAuthenticationMiddlewareProvider, +} from '../../../..'; +import {CustomMiddlewareChain} from '../../../fixtures/sequences/custom-middleware.sequence'; + +export function getApp(): Application { + const app = new Application(); + app.component(AuthenticationComponent); + app.component(RestComponent); + return app; +} + +export async function givenCustomMiddlewareServer(app: Application) { + const server = await app.getServer(RestServer); + server.middleware(ClientAuthenticationMiddlewareProvider, { + chain: CustomMiddlewareChain.PRE_INVOKE, + }); + server.middleware(UserAuthenticationMiddlewareProvider, { + chain: CustomMiddlewareChain.PRE_INVOKE, + }); + return server; +} diff --git a/src/__tests__/integration/custom-sequence/passport-apple-oauth2/apple-oauth2.integration.ts b/src/__tests__/integration/custom-sequence/passport-apple-oauth2/apple-oauth2.integration.ts new file mode 100644 index 0000000..ed82d89 --- /dev/null +++ b/src/__tests__/integration/custom-sequence/passport-apple-oauth2/apple-oauth2.integration.ts @@ -0,0 +1,85 @@ +import {Application, Provider} from '@loopback/core'; +import {get} from '@loopback/openapi-v3'; +import {Request, RestServer} from '@loopback/rest'; +import {Client, createClientForHandler} from '@loopback/testlab'; +import AppleStrategy, {DecodedIdToken} from 'passport-apple'; +import {authenticate} from '../../../../decorators'; +import {VerifyFunction} from '../../../../strategies'; +import {Strategies} from '../../../../strategies/keys'; +import {AppleAuthStrategyFactoryProvider} from '../../../../strategies/passport/passport-apple-oauth2'; +import {STRATEGY} from '../../../../strategy-name.enum'; +import {userWithoutReqObj} from '../../../fixtures/data/bearer-data'; +import {CustomSequence} from '../../../fixtures/sequences/custom-middleware.sequence'; +import {getApp, givenCustomMiddlewareServer} from '../helpers/helpers'; + +describe('getting apple oauth2 strategy using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + beforeEach(getAuthVerifier); + afterEach(closeServer); + + it('should return 302 when client id is passed and passReqToCallback is set true', async () => { + getAuthVerifier(); + class TestController { + @get('/test') + @authenticate(STRATEGY.APPLE_OAUTH2, { + clientID: 'string', + clientSecret: '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 givenCustomMiddlewareServer(app); + } + + function getAuthVerifier() { + app + .bind(Strategies.Passport.APPLE_OAUTH2_STRATEGY_FACTORY) + .toProvider(AppleAuthStrategyFactoryProvider); + app + .bind(Strategies.Passport.APPLE_OAUTH2_VERIFIER) + .toProvider(AppleAuthVerifyProvider); + } + + function closeServer() { + app.close(); + } + + function givenCustomSequence() { + // bind custom sequence + server.sequence(CustomSequence); + } +}); + +class AppleAuthVerifyProvider implements Provider { + constructor() {} + + value(): VerifyFunction.AppleAuthFn { + return async ( + accessToken: string, + refreshToken: string, + decodedIdToken: DecodedIdToken, + profile: AppleStrategy.Profile, + cd: AppleStrategy.VerifyCallback, + req?: Request, + ) => { + return userWithoutReqObj; + }; + } +} diff --git a/src/__tests__/integration/custom-sequence/passport-auth0/passport-auth0.integration.ts b/src/__tests__/integration/custom-sequence/passport-auth0/passport-auth0.integration.ts new file mode 100644 index 0000000..c233452 --- /dev/null +++ b/src/__tests__/integration/custom-sequence/passport-auth0/passport-auth0.integration.ts @@ -0,0 +1,89 @@ +import {Application, Provider} from '@loopback/core'; +import {get} from '@loopback/openapi-v3'; +import {Request, RestServer} from '@loopback/rest'; +import {Client, createClientForHandler} from '@loopback/testlab'; +import Auth0Strategy from 'passport-auth0'; +import {authenticate} from '../../../../decorators'; +import {VerifyFunction} from '../../../../strategies'; +import {Strategies} from '../../../../strategies/keys'; +import {Auth0StrategyFactoryProvider} from '../../../../strategies/passport/passport-auth0'; +import {Auth0} from '../../../../strategies/types/auth0.types'; +import {STRATEGY} from '../../../../strategy-name.enum'; +import {userWithoutReqObj} from '../../../fixtures/data/bearer-data'; + +import {CustomSequence} from '../../../fixtures/sequences/custom-middleware.sequence'; +import {getApp, givenCustomMiddlewareServer} from '../helpers/helpers'; + +describe('getting auth0 strategy using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + beforeEach(getAuthVerifier); + afterEach(closeServer); + + it('should return 302 when client id is passed and passReqToCallback is set true', async () => { + getAuthVerifier(); + class TestController { + @get('/test') + @authenticate(STRATEGY.AUTH0, { + clientID: 'string', + clientSecret: 'string', + callbackURL: 'string', + domain: 'string', + passReqToCallback: true, + state: false, + }) + 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 givenCustomMiddlewareServer(app); + } + + function getAuthVerifier() { + app + .bind(Strategies.Passport.AUTH0_STRATEGY_FACTORY) + .toProvider(Auth0StrategyFactoryProvider); + app + .bind(Strategies.Passport.AUTH0_VERIFIER) + .toProvider(Auth0VerifyProvider); + } + + function closeServer() { + app.close(); + } + + function givenCustomSequence() { + // bind custom sequence + server.sequence(CustomSequence); + } +}); + +class Auth0VerifyProvider implements Provider { + constructor() {} + + value(): VerifyFunction.Auth0Fn { + return async ( + accessToken: string, + refreshToken: string, + profile: Auth0Strategy.Profile, + cd: Auth0.VerifyCallback, + req?: Request, + ) => { + return userWithoutReqObj; + }; + } +} diff --git a/src/__tests__/integration/custom-sequence/passport-bearer/bearer-token-verify.integration.ts b/src/__tests__/integration/custom-sequence/passport-bearer/bearer-token-verify.integration.ts new file mode 100644 index 0000000..a00d14b --- /dev/null +++ b/src/__tests__/integration/custom-sequence/passport-bearer/bearer-token-verify.integration.ts @@ -0,0 +1,295 @@ +import {Application, inject} from '@loopback/core'; +import {get} from '@loopback/openapi-v3'; +import {RestServer} from '@loopback/rest'; +import {Client, createClientForHandler, expect} from '@loopback/testlab'; +import {authenticate} from '../../../../decorators'; +import {AuthenticationBindings} from '../../../../keys'; +import {Strategies} from '../../../../strategies/keys'; +import {STRATEGY} from '../../../../strategy-name.enum'; +import {IAuthUser} from '../../../../types'; +import {BearerTokenVerifyProvider} from '../../../fixtures/providers/bearer-passport.provider'; +import {CustomSequence} from '../../../fixtures/sequences/custom-middleware.sequence'; +import {getApp, givenCustomMiddlewareServer} from '../helpers/helpers'; + +/** + * Testing overall flow of authentication with bearer strategy + */ +describe('Bearer-token strategy using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + beforeEach(getAuthVerifier); + + it('should return 401 when token is not passed', async () => { + class BearerNoTokenController { + @get('/auth/bearer/no-token') + @authenticate(STRATEGY.BEARER) + test() { + return 'test successful'; + } + } + + app.controller(BearerNoTokenController); + + await whenIMakeRequestTo(server).get('/auth/bearer/no-token').expect(401); + }); + + it('should return status 200 when token is passed', async () => { + class BearerTokenController { + @get('/auth/bearer/token') + @authenticate(STRATEGY.BEARER) + test() { + return 'test successful'; + } + } + + app.controller(BearerTokenController); + + await whenIMakeRequestTo(server) + .get('/auth/bearer/token') + .set('Authorization', 'Bearer validtoken') + .expect(200); + }); + + it('should return the user passed via verifier when no options are passed', async () => { + class BearerNoOptionsController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + @get('/auth/bearer/no-options') + @authenticate(STRATEGY.BEARER) + async test() { + return this.user; + } + } + + app.controller(BearerNoOptionsController); + + const user = await whenIMakeRequestTo(server) + .get('/auth/bearer/no-options') + .set('Authorization', 'Bearer validtoken') + .expect(200); + + expect(user.body).to.have.property('id'); + expect(user.body.id).to.equal(1); + }); + + it('should return the user passed via verifier and options are passed with passRequestCallback true', async () => { + class BearerForCallbackController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + options = { + passRequestToCallback: false, + }; + + @get('/auth/bearer/callback') + @authenticate(STRATEGY.BEARER, {passReqToCallback: true}) + async test() { + return this.user; + } + } + + app.controller(BearerForCallbackController); + + const user = await whenIMakeRequestTo(server) + .get('/auth/bearer/callback') + .set('Authorization', 'Bearer validtoken') + .expect(200); + + expect(user.body).to.have.property('id'); + expect(user.body.id).to.equal(2); + }); + + it('should return the user passed via verifier and options are passed with passRequestCallback false', async () => { + class BearerNoCallbackController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + options = { + passRequestToCallback: false, + }; + + @get('/auth/bearer/no-callback') + @authenticate(STRATEGY.BEARER, {passReqToCallback: false}) + async test() { + return this.user; + } + } + + app.controller(BearerNoCallbackController); + + const user = await whenIMakeRequestTo(server) + .get('/auth/bearer/no-callback') + .set('Authorization', 'Bearer validtoken') + .expect(200); + + expect(user.body).to.have.property('id'); + expect(user.body.id).to.equal(1); + }); + + it('should return status 401 as Bearer is not sent in token', async () => { + class NoBearerInTokenController { + @get('/auth/bearer/no-bearer-in-token') + @authenticate(STRATEGY.BEARER) + test() { + return 'test successful'; + } + } + + app.controller(NoBearerInTokenController); + + await whenIMakeRequestTo(server) + .get('/auth/bearer/no-bearer-in-token') + .set('Authorization', 'sometoken') + .expect(401); + }); + + it('should return error as no user was returned from provider', async () => { + class BearerNoUserController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + @get('/auth/bearer/no-user') + @authenticate(STRATEGY.BEARER) + async test() { + return this.user; + } + } + + app.controller(BearerNoUserController); + + await whenIMakeRequestTo(server) + .get('/auth/bearer/no-user') + .set('Authorization', 'Bearer sometoken') + .expect(401); + }); + + it('should return error when passRequestCallback is true and provider is not returning user', async () => { + class BearerNoUserFromCallbackController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + options = { + passRequestToCallback: false, + }; + + @get('/auth/bearer/no-user-with-callback') + @authenticate(STRATEGY.BEARER, {passReqToCallback: true}) + async test() { + return this.user; + } + } + + app.controller(BearerNoUserFromCallbackController); + + await whenIMakeRequestTo(server) + .get('/auth/bearer/no-user-with-callback') + .set('Authorization', 'Bearer sometoken') + .expect(401); + }); + + it('should return error when options are passed with passRequestCallback false and provider does not return user', async () => { + class BearerCallbackFalseController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + options = { + passRequestToCallback: false, + }; + + @get('/auth/bearer/no-user-when-callback-false') + @authenticate(STRATEGY.BEARER, {passReqToCallback: false}) + async test() { + return this.user; + } + } + + app.controller(BearerCallbackFalseController); + + await whenIMakeRequestTo(server) + .get('/auth/bearer/no-user-when-callback-false') + .set('Authorization', 'Bearer sometoken') + .expect(401); + }); + + function whenIMakeRequestTo(restServer: RestServer): Client { + return createClientForHandler(restServer.requestHandler); + } + + async function givenAServer() { + app = getApp(); + server = await givenCustomMiddlewareServer(app); + } + + function getAuthVerifier() { + app + .bind(Strategies.Passport.BEARER_TOKEN_VERIFIER) + .toProvider(BearerTokenVerifyProvider); + } + + function givenCustomSequence() { + // bind user defined sequence + server.sequence(CustomSequence); + } +}); + +describe('integration test when no provider was implemented using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + + it('should return error as the verifier is not implemented', async () => { + class BearerNoVerifierController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + options = { + passRequestToCallback: false, + }; + + @get('/auth/bearer/no-verifier') + @authenticate(STRATEGY.BEARER, {passReqToCallback: false}) + async test() { + return this.user; + } + } + + app.controller(BearerNoVerifierController); + + await whenIMakeRequestTo(server) + .get('/auth/bearer/no-verifier') + .set('Authorization', 'Bearer sometoken') + .expect(401); + }); + + function whenIMakeRequestTo(restServer: RestServer): Client { + return createClientForHandler(restServer.requestHandler); + } + + async function givenAServer() { + app = getApp(); + server = await givenCustomMiddlewareServer(app); + } + + function givenCustomSequence() { + // bind user defined sequence + server.sequence(CustomSequence); + } +}); diff --git a/src/__tests__/integration/custom-sequence/passport-client-password/client-password-verify.integration.ts b/src/__tests__/integration/custom-sequence/passport-client-password/client-password-verify.integration.ts new file mode 100644 index 0000000..4e98d55 --- /dev/null +++ b/src/__tests__/integration/custom-sequence/passport-client-password/client-password-verify.integration.ts @@ -0,0 +1,201 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import {Application, inject} from '@loopback/core'; +import {post, requestBody} from '@loopback/openapi-v3'; +import {RestServer} from '@loopback/rest'; +import {Client, createClientForHandler, expect} from '@loopback/testlab'; +import {authenticateClient} from '../../../../decorators'; +import {AuthenticationBindings} from '../../../../keys'; +import {Strategies} from '../../../../strategies/keys'; +import {STRATEGY} from '../../../../strategy-name.enum'; +import {IAuthClient} from '../../../../types'; +import {ClientPasswordVerifyProvider} from '../../../fixtures/providers/passport-client.provider'; +import {CustomSequence} from '../../../fixtures/sequences/custom-middleware.sequence'; +import {getApp, givenCustomMiddlewareServer} from '../helpers/helpers'; + +describe('Client-password strategy using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + beforeEach(getAuthVerifier); + + it('should return status 200 when options.passRequestToCallback is set true', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_CLIENT) // tslint:disable-next-line: no-shadowed-variable + private readonly client: IAuthClient | undefined, + ) {} + + @authenticateClient(STRATEGY.CLIENT_PASSWORD, {passReqToCallback: true}) + @post('/test') + test(@requestBody() body: {client_id: string; client_secret: string}) { + return this.client; + } + } + + app.controller(TestController); + + const client = await whenIMakeRequestTo(server) + .post('/test') + .send({client_id: 'some id', client_secret: 'some secret'}) + .expect(200); + + expect(client.body).to.have.property('clientId'); + expect(client.body).to.have.property('clientSecret'); + expect(client.body.clientId).to.equal('some id'); + expect(client.body.clientSecret).to.equal('some secret'); + }); + + it('should return status 200 when options.passRequestToCallback is set false', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_CLIENT) // tslint:disable-next-line: no-shadowed-variable + private readonly client: IAuthClient | undefined, + ) {} + + @post('/test') + @authenticateClient(STRATEGY.CLIENT_PASSWORD, {passReqToCallback: false}) + test(@requestBody() body: {client_id: string; client_secret: string}) { + return this.client; + } + } + + app.controller(TestController); + + const client = await whenIMakeRequestTo(server) + .post('/test') + .send({client_id: 'some id', client_secret: 'some secret'}) + .expect(200); + + expect(client.body).to.have.property('clientId'); + expect(client.body).to.have.property('clientSecret'); + expect(client.body.clientId).to.equal('some id'); + expect(client.body.clientSecret).to.equal('some secret'); + }); + + it('should return status 401 when options.passRequestToCallback is set true', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_CLIENT) // tslint:disable-next-line: no-shadowed-variable + private readonly client: IAuthClient | undefined, + ) {} + + @post('/test') + @authenticateClient(STRATEGY.CLIENT_PASSWORD, {passReqToCallback: true}) + async test( + @requestBody() + body: { + client_id: string; + client_secret: string; + }, + ) { + return this.client; + } + } + + app.controller(TestController); + + await whenIMakeRequestTo(server) + .post('/test') + .send({client_id: '', client_secret: 'some secret'}) + .expect(401); + }); + + it('should return status 401 when options.passRequestToCallback is set false', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_CLIENT) // tslint:disable-next-line: no-shadowed-variable + private readonly client: IAuthClient | undefined, + ) {} + + @post('/test') + @authenticateClient(STRATEGY.CLIENT_PASSWORD, {passReqToCallback: false}) + async test( + @requestBody() + body: { + client_id: string; + client_secret: string; + }, + ) { + return this.client; + } + } + + app.controller(TestController); + + await whenIMakeRequestTo(server) + .post('/test') + .send({client_id: '', client_secret: 'some secret'}) + .expect(401); + }); + + function whenIMakeRequestTo(restServer: RestServer): Client { + return createClientForHandler(restServer.requestHandler); + } + + async function givenAServer() { + app = getApp(); + server = await givenCustomMiddlewareServer(app); + } + + function getAuthVerifier() { + app + .bind(Strategies.Passport.OAUTH2_CLIENT_PASSWORD_VERIFIER) + .toProvider(ClientPasswordVerifyProvider); + } + + function givenCustomSequence() { + // bind custom sequence + server.sequence(CustomSequence); + } +}); + +describe('integration test for client-password and no verifier using Middleware Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + + it('should return status 401 as this strategy is not implemented', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_CLIENT) // tslint:disable-next-line: no-shadowed-variable + private readonly client: IAuthClient | undefined, + ) {} + + @post('/test') + @authenticateClient(STRATEGY.CLIENT_PASSWORD, {passReqToCallback: true}) + test( + @requestBody() + body: { + client_id: string; + client_secret: string; + }, + ) { + return this.client; + } + } + + app.controller(TestController); + + await whenIMakeRequestTo(server) + .post('/test') + .send({client_id: 'some id', client_secret: 'some secret'}) + .expect(401); + }); + + function whenIMakeRequestTo(restServer: RestServer): Client { + return createClientForHandler(restServer.requestHandler); + } + + async function givenAServer() { + app = getApp(); + server = await givenCustomMiddlewareServer(app); + } + + function givenCustomSequence() { + // bind custom server + server.sequence(CustomSequence); + } +}); diff --git a/src/__tests__/integration/custom-sequence/passport-cognito-oauth2/cognito-oauth2.integration.ts b/src/__tests__/integration/custom-sequence/passport-cognito-oauth2/cognito-oauth2.integration.ts new file mode 100644 index 0000000..19900b8 --- /dev/null +++ b/src/__tests__/integration/custom-sequence/passport-cognito-oauth2/cognito-oauth2.integration.ts @@ -0,0 +1,80 @@ +import {Application, Provider} from '@loopback/core'; +import {get} from '@loopback/openapi-v3'; +import {Request, RestServer} from '@loopback/rest'; +import {Client, createClientForHandler} from '@loopback/testlab'; +import {authenticate} from '../../../../decorators'; +import {VerifyFunction} from '../../../../strategies'; +import {Strategies} from '../../../../strategies/keys'; +import {CognitoStrategyFactoryProvider} from '../../../../strategies/passport/passport-cognito-oauth2'; +import {STRATEGY} from '../../../../strategy-name.enum'; +import {Cognito} from '../../../../types'; +import {userWithoutReqObj} from '../../../fixtures/data/bearer-data'; +import {CustomSequence} from '../../../fixtures/sequences/custom-middleware.sequence'; +import {getApp, givenCustomMiddlewareServer} from '../helpers/helpers'; + +describe('getting cognito oauth2 strategy using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenAuthenticatedSequence); + beforeEach(getAuthVerifier); + + it('should return 302 when client id is passed and passReqToCallback is set true', async () => { + class TestController { + @get('/test') + @authenticate(STRATEGY.COGNITO_OAUTH2, { + clientID: 'string', + clientSecret: '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 givenCustomMiddlewareServer(app); + } + + function getAuthVerifier() { + app + .bind(Strategies.Passport.COGNITO_OAUTH2_VERIFIER) + .toProvider(CognitoAuthVerifyProvider); + app + .bind(Strategies.Passport.COGNITO_OAUTH2_STRATEGY_FACTORY) + .toProvider(CognitoStrategyFactoryProvider); + } + + function givenAuthenticatedSequence() { + // bind custom sequence + server.sequence(CustomSequence); + } +}); + +class CognitoAuthVerifyProvider + implements Provider +{ + constructor() {} + + value(): VerifyFunction.CognitoAuthFn { + return async ( + accessToken: string, + refreshToken: string, + profile: Cognito.Profile, + cd: Cognito.VerifyCallback, + req?: Request, + ) => { + return userWithoutReqObj; + }; + } +} diff --git a/src/__tests__/integration/custom-sequence/passport-google-oauth2/google-oauth2.integration.ts b/src/__tests__/integration/custom-sequence/passport-google-oauth2/google-oauth2.integration.ts new file mode 100644 index 0000000..106f276 --- /dev/null +++ b/src/__tests__/integration/custom-sequence/passport-google-oauth2/google-oauth2.integration.ts @@ -0,0 +1,80 @@ +import {Application, Provider} from '@loopback/core'; +import {get} from '@loopback/openapi-v3'; +import {Request, RestServer} from '@loopback/rest'; +import {Client, createClientForHandler} from '@loopback/testlab'; +import * as GoogleStrategy from 'passport-google-oauth20'; +import {authenticate} from '../../../../decorators'; +import {VerifyFunction} from '../../../../strategies'; +import {Strategies} from '../../../../strategies/keys'; +import {GoogleAuthStrategyFactoryProvider} from '../../../../strategies/passport/passport-google-oauth2'; +import {STRATEGY} from '../../../../strategy-name.enum'; +import {userWithoutReqObj} from '../../../fixtures/data/bearer-data'; +import {CustomSequence} from '../../../fixtures/sequences/custom-middleware.sequence'; +import {getApp, givenCustomMiddlewareServer} from '../helpers/helpers'; + +describe('getting google oauth2 strategy using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + beforeEach(getAuthVerifier); + + it('should return 302 when client id is passed and passReqToCallback is set true', async () => { + class TestController { + @get('/test') + @authenticate(STRATEGY.GOOGLE_OAUTH2, { + clientID: 'string', + clientSecret: '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 givenCustomMiddlewareServer(app); + } + + function getAuthVerifier() { + app + .bind(Strategies.Passport.GOOGLE_OAUTH2_STRATEGY_FACTORY) + .toProvider(GoogleAuthStrategyFactoryProvider); + app + .bind(Strategies.Passport.GOOGLE_OAUTH2_VERIFIER) + .toProvider(GoogleAuthVerifyProvider); + } + + function givenCustomSequence() { + // bind custom sequence + server.sequence(CustomSequence); + } +}); + +class GoogleAuthVerifyProvider + implements Provider +{ + constructor() {} + + value(): VerifyFunction.GoogleAuthFn { + return async ( + accessToken: string, + refreshToken: string, + profile: GoogleStrategy.Profile, + cd: GoogleStrategy.VerifyCallback, + req?: Request, + ) => { + return userWithoutReqObj; + }; + } +} diff --git a/src/__tests__/integration/custom-sequence/passport-instagram-oauth2/instagram-oauth2.integration.ts b/src/__tests__/integration/custom-sequence/passport-instagram-oauth2/instagram-oauth2.integration.ts new file mode 100644 index 0000000..deed188 --- /dev/null +++ b/src/__tests__/integration/custom-sequence/passport-instagram-oauth2/instagram-oauth2.integration.ts @@ -0,0 +1,80 @@ +import {Application, Provider} from '@loopback/core'; +import {get} from '@loopback/openapi-v3'; +import {Request, RestServer} from '@loopback/rest'; +import {Client, createClientForHandler} from '@loopback/testlab'; +import * as InstagramStrategy from 'passport-instagram'; +import {authenticate} from '../../../../decorators'; +import {VerifyCallback, VerifyFunction} from '../../../../strategies'; +import {Strategies} from '../../../../strategies/keys'; +import {InstagramAuthStrategyFactoryProvider} from '../../../../strategies/passport/passport-insta-oauth2'; +import {STRATEGY} from '../../../../strategy-name.enum'; +import {userWithoutReqObj} from '../../../fixtures/data/bearer-data'; +import {CustomSequence} from '../../../fixtures/sequences/custom-middleware.sequence'; +import {getApp, givenCustomMiddlewareServer} from '../helpers/helpers'; + +describe('getting instagram oauth2 strategy using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + beforeEach(getAuthVerifier); + + it('should return 302 when client id is passed and passReqToCallback is set true', async () => { + class TestController { + @get('/test') + @authenticate(STRATEGY.INSTAGRAM_OAUTH2, { + clientID: 'string', + clientSecret: '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 givenCustomMiddlewareServer(app); + } + + function getAuthVerifier() { + app + .bind(Strategies.Passport.INSTAGRAM_OAUTH2_STRATEGY_FACTORY) + .toProvider(InstagramAuthStrategyFactoryProvider); + app + .bind(Strategies.Passport.INSTAGRAM_OAUTH2_VERIFIER) + .toProvider(InstagramAuthVerifyProvider); + } + + function givenCustomSequence() { + // bind custom sequence + server.sequence(CustomSequence); + } +}); + +class InstagramAuthVerifyProvider + implements Provider +{ + constructor() {} + + value(): VerifyFunction.InstagramAuthFn { + return async ( + accessToken: string, + refreshToken: string, + profile: InstagramStrategy.Profile, + cd: VerifyCallback, + req?: Request, + ) => { + return userWithoutReqObj; + }; + } +} diff --git a/src/__tests__/integration/custom-sequence/passport-keycloak/keycloak.integration.ts b/src/__tests__/integration/custom-sequence/passport-keycloak/keycloak.integration.ts new file mode 100644 index 0000000..b9a3e25 --- /dev/null +++ b/src/__tests__/integration/custom-sequence/passport-keycloak/keycloak.integration.ts @@ -0,0 +1,86 @@ +import {Application, Provider} from '@loopback/core'; +import {get} from '@loopback/openapi-v3'; +import {Request, RestServer} from '@loopback/rest'; +import {Client, createClientForHandler} from '@loopback/testlab'; +import {authenticate} from '../../../../decorators'; +import {Keycloak, VerifyFunction} from '../../../../strategies'; +import {Strategies} from '../../../../strategies/keys'; +import {KeycloakStrategyFactoryProvider} from '../../../../strategies/passport/passport-keycloak'; +import {STRATEGY} from '../../../../strategy-name.enum'; +import {IAuthUser} from '../../../../types'; +import {userWithoutReqObj} from '../../../fixtures/data/bearer-data'; +import {CustomSequence} from '../../../fixtures/sequences/custom-middleware.sequence'; +import {getApp, givenCustomMiddlewareServer} from '../helpers/helpers'; + +describe('getting keycloak oauth2 strategy using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + beforeEach(getAuthVerifier); + + it('should return 302 when host and client id is passed and passReqToCallback is set true', async () => { + class TestController { + @get('/test') + @authenticate(STRATEGY.KEYCLOAK, { + host: 'localhost', + realm: 'localhost', + callbackURL: 'localhost', + authorizationURL: 'localhost', + tokenURL: 'localhost', + userInfoURL: 'localhost', + clientID: 'string', + clientSecret: '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 givenCustomMiddlewareServer(app); + } + + function getAuthVerifier() { + app + .bind(Strategies.Passport.KEYCLOAK_STRATEGY_FACTORY) + .toProvider(KeycloakStrategyFactoryProvider); + app + .bind(Strategies.Passport.KEYCLOAK_VERIFIER) + .toProvider(KeycloakAuthVerifyProvider); + } + + function givenCustomSequence() { + // bind user defined sequence + server.sequence(CustomSequence); + } +}); + +class KeycloakAuthVerifyProvider + implements Provider +{ + constructor() {} + + value(): VerifyFunction.KeycloakAuthFn { + return async ( + accessToken: string, + refreshToken: string, + profile: Keycloak.Profile, + cd: (err?: string | Error, user?: IAuthUser) => void, + req?: Request, + ) => { + return userWithoutReqObj; + }; + } +} diff --git a/src/__tests__/integration/custom-sequence/passport-local/local-passport.integration.ts b/src/__tests__/integration/custom-sequence/passport-local/local-passport.integration.ts new file mode 100644 index 0000000..59732ac --- /dev/null +++ b/src/__tests__/integration/custom-sequence/passport-local/local-passport.integration.ts @@ -0,0 +1,186 @@ +import {Application, inject} from '@loopback/core'; +import {post, requestBody} from '@loopback/openapi-v3'; +import {RestServer} from '@loopback/rest'; +import {Client, createClientForHandler, expect} from '@loopback/testlab'; +import {authenticate} from '../../../../decorators'; +import {AuthenticationBindings} from '../../../../keys'; +import {Strategies} from '../../../../strategies/keys'; +import {LocalPasswordStrategyFactoryProvider} from '../../../../strategies/passport/passport-local'; +import {STRATEGY} from '../../../../strategy-name.enum'; +import {IAuthUser} from '../../../../types'; +import {LocalVerifyProvider} from '../../../fixtures/providers/local-password.provider'; +import {CustomSequence} from '../../../fixtures/sequences/custom-middleware.sequence'; +import {UserCred} from '../../../fixtures/user-cred.model'; +import {getApp, givenCustomMiddlewareServer} from '../helpers/helpers'; +/** + * Testing overall flow of authentication with bearer strategy + */ +describe('Local passport strategy using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + beforeEach(getAuthVerifier); + + it('should return 400 bad request when no user data is passed', async () => { + class TestController { + @post('/auth/local/no-user-data-passed') + @authenticate(STRATEGY.LOCAL) + test( + @requestBody({required: true}) + body: UserCred, + ) { + return 'test successful'; + } + } + + app.controller(TestController); + + await whenIMakeRequestTo(server) + .post('/auth/local/no-user-data-passed') + .expect(400); + }); + + it('should return 422 bad request when invalid user data is passed', async () => { + class TestController { + @post('/auth/local/no-user-data-passed') + @authenticate(STRATEGY.LOCAL) + test( + @requestBody() + body: UserCred, + ) { + return 'test successful'; + } + } + + app.controller(TestController); + + await whenIMakeRequestTo(server) + .post('/auth/local/no-user-data-passed') + .send({}) + .expect(422); + }); + + it('should return status 200 for no options', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + @post('/auth/local/no-options') + @authenticate(STRATEGY.LOCAL) + test(@requestBody() body: UserCred) { + return this.user; + } + } + + app.controller(TestController); + + const user = await whenIMakeRequestTo(server) + .post('/auth/local/no-options') + .send({username: 'user name', password: 'password'}) + .expect(200); + + expect(user.body).to.have.property('username'); + expect(user.body.username).to.equal('user name'); + + expect(user.body).to.have.property('password'); + expect(user.body.password).to.equal('password'); + }); + + it('should return the user credentials are sent via body and options are passed with passRequestCallback true', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + @post('/auth/local/pass-req-callback-true') + @authenticate(STRATEGY.LOCAL, {passReqToCallback: true}) + async test(@requestBody() body: UserCred) { + return this.user; + } + } + + app.controller(TestController); + + const user = await whenIMakeRequestTo(server) + .post('/auth/local/pass-req-callback-true') + .send({username: 'name', password: 'password'}) + .expect(200); + + expect(user.body).to.have.property('username'); + expect(user.body.username).to.equal('name'); + + expect(user.body).to.have.property('password'); + expect(user.body.password).to.equal('password'); + }); + + it('should return the user which was passed via body and options are passed with passRequestCallback false', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + @post('/auth/local/pass-req-callback-false') + @authenticate(STRATEGY.LOCAL, {passReqToCallback: false}) + async test(@requestBody() body: UserCred) { + return this.user; + } + } + + app.controller(TestController); + + await whenIMakeRequestTo(server) + .post('/auth/local/pass-req-callback-false') + .send({username: 'username', password: 'password'}) + .expect(200); + }); + + it('should return 401 when provider returns null', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + @post('/auth/local/null-user') + @authenticate(STRATEGY.LOCAL) + async test(@requestBody() body: UserCred) { + return body; + } + } + + app.controller(TestController); + + await whenIMakeRequestTo(server) + .post('/auth/local/null-user') + .send({username: '', password: 'password'}) + .expect(401); + }); + + function whenIMakeRequestTo(restServer: RestServer): Client { + return createClientForHandler(restServer.requestHandler); + } + + async function givenAServer() { + app = getApp(); + server = await givenCustomMiddlewareServer(app); + } + + function getAuthVerifier() { + app + .bind(Strategies.Passport.LOCAL_STRATEGY_FACTORY) + .toProvider(LocalPasswordStrategyFactoryProvider); + app + .bind(Strategies.Passport.LOCAL_PASSWORD_VERIFIER) + .toProvider(LocalVerifyProvider); + } + + function givenCustomSequence() { + // bind using custom sequence + server.sequence(CustomSequence); + } +}); diff --git a/src/__tests__/integration/custom-sequence/passport-resource-owner-password/resource-owner-password.integration.ts b/src/__tests__/integration/custom-sequence/passport-resource-owner-password/resource-owner-password.integration.ts new file mode 100644 index 0000000..8382d10 --- /dev/null +++ b/src/__tests__/integration/custom-sequence/passport-resource-owner-password/resource-owner-password.integration.ts @@ -0,0 +1,217 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import {Application, inject} from '@loopback/core'; +import {post, requestBody} from '@loopback/openapi-v3'; +import {RestServer} from '@loopback/rest'; +import {Client, createClientForHandler, expect} from '@loopback/testlab'; +import {authenticate} from '../../../../decorators'; +import {AuthenticationBindings} from '../../../../keys'; +import {Strategies} from '../../../../strategies/keys'; +import {ResourceOwnerPasswordStrategyFactoryProvider} from '../../../../strategies/passport/passport-resource-owner-password'; +import {STRATEGY} from '../../../../strategy-name.enum'; +import {IAuthUser} from '../../../../types'; +import {ResourceOwnerVerifyProvider} from '../../../fixtures/providers/resource-owner.provider'; +import {CustomSequence} from '../../../fixtures/sequences/custom-middleware.sequence'; +import {getApp, givenCustomMiddlewareServer} from '../helpers/helpers'; + +describe('Resource-owner-password strategy using Custom Sequence', () => { + let app: Application; + let server: RestServer; + beforeEach(givenAServer); + beforeEach(givenCustomSequence); + beforeEach(getAuthVerifier); + + it('should return 422 bad request when no user data is sent', async () => { + class TestController { + @post('/auth/resource-owner-pass') + @authenticate(STRATEGY.OAUTH2_RESOURCE_OWNER_GRANT) + test(@requestBody() body: {username: string; password: string}) { + return 'test successful'; + } + } + + app.controller(TestController); + + await whenIMakeRequestTo(server) + .post('/auth/resource-owner-pass') + .send({}) + .expect(401); + }); + + it('should return status 200 for no options', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + @post('/auth/resource-owner-pass/no-options') + @authenticate(STRATEGY.OAUTH2_RESOURCE_OWNER_GRANT) + test( + @requestBody() + body: { + username: string; + password: string; + client_id: string; + client_secret: string; + }, + ) { + return this.user; + } + } + + app.controller(TestController); + + const res = await whenIMakeRequestTo(server) + .post('/auth/resource-owner-pass/no-options') + .send({ + username: 'username', + password: 'password', + client_id: 'client id', + client_secret: 'client secret', + }) + .expect(200); + + expect(res.body).to.have.property('username'); + expect(res.body.username).to.equal('username'); + expect(res.body).to.have.property('password'); + expect(res.body.password).to.equal('password'); + }); + + it('should return the user credentials are sent via body and options are passed with passRequestCallback true', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + @post('/auth/resource-owner/passReqToCallback') + @authenticate(STRATEGY.OAUTH2_RESOURCE_OWNER_GRANT, { + passReqToCallback: true, + }) + async test( + @requestBody() + body: { + username: string; + password: string; + client_id: string; + client_secret: string; + }, + ) { + return this.user; + } + } + + app.controller(TestController); + + const res = await whenIMakeRequestTo(server) + .post('/auth/resource-owner/passReqToCallback') + .send({ + username: 'user name', + password: 'password', + client_id: 'client id', + client_secret: 'client secret', + }) + .expect(200); + + expect(res.body).to.have.property('username'); + expect(res.body.username).to.equal('user name'); + expect(res.body).to.have.property('password'); + expect(res.body.password).to.equal('password'); + }); + + it('should return the user which was passed via body and options are passed with passRequestCallback false', async () => { + class TestController { + constructor( + @inject(AuthenticationBindings.CURRENT_USER) // tslint:disable-next-line: no-shadowed-variable + private readonly user: IAuthUser | undefined, + ) {} + + @post('/auth/resource-owner/passReqToCallback-false') + @authenticate(STRATEGY.OAUTH2_RESOURCE_OWNER_GRANT, { + passReqToCallback: false, + }) + async test( + @requestBody() + body: { + username: string; + password: string; + client_id: string; + client_secret: string; + }, + ) { + return this.user; + } + } + + app.controller(TestController); + + const res = await whenIMakeRequestTo(server) + .post('/auth/resource-owner/passReqToCallback-false') + .send({ + username: 'name', + password: 'password', + client_id: 'client id', + client_secret: 'client secret', + }) + .expect(200); + + expect(res.body).to.have.property('username'); + expect(res.body.username).to.equal('name'); + expect(res.body).to.have.property('password'); + expect(res.body.password).to.equal('password'); + }); + + it('should return the user passed via verifier when no options are passed', async () => { + class TestController { + @post('/test') + @authenticate(STRATEGY.OAUTH2_RESOURCE_OWNER_GRANT) + async test( + @requestBody() + body: { + username: string; + password: string; + client_id: string; + client_secret: string; + }, + ) { + return body; + } + } + + app.controller(TestController); + + await whenIMakeRequestTo(server) + .post('/test') + .send({ + username: '', + password: 'password', + client_id: '', + client_secret: 'client secret', + }) + .expect(401); + }); + + function whenIMakeRequestTo(restServer: RestServer): Client { + return createClientForHandler(restServer.requestHandler); + } + + async function givenAServer() { + app = getApp(); + server = await givenCustomMiddlewareServer(app); + } + + function getAuthVerifier() { + app + .bind(Strategies.Passport.RESOURCE_OWNER_STRATEGY_FACTORY) + .toProvider(ResourceOwnerPasswordStrategyFactoryProvider); + app + .bind(Strategies.Passport.RESOURCE_OWNER_PASSWORD_VERIFIER) + .toProvider(ResourceOwnerVerifyProvider); + } + + function givenCustomSequence() { + // bind using custom sequence + server.sequence(CustomSequence); + } +}); diff --git a/src/index.ts b/src/index.ts index 4cace01..ba6b453 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,3 +6,4 @@ export * from './keys'; export * from './strategy-adapter'; export * from './strategy-name.enum'; export * from './error-keys'; +export * from './middlewares';