Skip to content

Commit

Permalink
feat(middleware): export client and user authentication middlewares
Browse files Browse the repository at this point in the history
256
  • Loading branch information
rashisf committed Jan 7, 2025
1 parent f971a75 commit 2e1f22b
Show file tree
Hide file tree
Showing 14 changed files with 1,497 additions and 2 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 68 additions & 0 deletions src/__tests__/fixtures/sequences/custom-middleware.sequence.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
27 changes: 27 additions & 0 deletions src/__tests__/integration/custom-sequence/helpers/helpers.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<VerifyFunction.AppleAuthFn> {
constructor() {}

value(): VerifyFunction.AppleAuthFn {
return async (
accessToken: string,
refreshToken: string,
decodedIdToken: DecodedIdToken,
profile: AppleStrategy.Profile,
cd: AppleStrategy.VerifyCallback,
req?: Request,
) => {
return userWithoutReqObj;
};
}
}
Original file line number Diff line number Diff line change
@@ -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<VerifyFunction.Auth0Fn> {
constructor() {}

value(): VerifyFunction.Auth0Fn {
return async (
accessToken: string,
refreshToken: string,
profile: Auth0Strategy.Profile,
cd: Auth0.VerifyCallback,
req?: Request,
) => {
return userWithoutReqObj;
};
}
}
Loading

0 comments on commit 2e1f22b

Please sign in to comment.