From ee1845149af8267f9eb74aa9fd9396d7d2bd996d Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 21 May 2020 19:45:24 -0700 Subject: [PATCH 1/4] chore(mock-oauth2-provider): publish the package to npm --- fixtures/mock-oauth2-provider/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fixtures/mock-oauth2-provider/package.json b/fixtures/mock-oauth2-provider/package.json index 356ade13a022..9a486af71100 100644 --- a/fixtures/mock-oauth2-provider/package.json +++ b/fixtures/mock-oauth2-provider/package.json @@ -2,7 +2,6 @@ "name": "@loopback/mock-oauth2-provider", "version": "0.0.3", "description": "mocks the oauth2 authorization flow", - "private": true, "engines": { "node": ">=10" }, @@ -47,5 +46,8 @@ "devDependencies": { "@loopback/build": "^5.4.1", "@loopback/eslint-config": "^7.0.1" + }, + "publishConfig": { + "access": "public" } } From 3cdd83d61adb5a7b4ce5b2b219d08ec768abc8b5 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 21 May 2020 21:10:52 -0700 Subject: [PATCH 2/4] fix(mock-oauth2-provider): add main/types entries in package.json --- fixtures/mock-oauth2-provider/index.js | 6 ------ fixtures/mock-oauth2-provider/index.ts | 8 -------- fixtures/mock-oauth2-provider/package.json | 2 ++ 3 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 fixtures/mock-oauth2-provider/index.js delete mode 100644 fixtures/mock-oauth2-provider/index.ts diff --git a/fixtures/mock-oauth2-provider/index.js b/fixtures/mock-oauth2-provider/index.js deleted file mode 100644 index 10d526870ca5..000000000000 --- a/fixtures/mock-oauth2-provider/index.js +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright IBM Corp. 2020. All Rights Reserved. -// Node module: @loopback/mock-oauth2-provider -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -module.exports = require('./dist'); diff --git a/fixtures/mock-oauth2-provider/index.ts b/fixtures/mock-oauth2-provider/index.ts deleted file mode 100644 index e2e3d746af7f..000000000000 --- a/fixtures/mock-oauth2-provider/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright IBM Corp. 2020. All Rights Reserved. -// Node module: @loopback/mock-oauth2-provider -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -// DO NOT EDIT THIS FILE -// Add any additional (re)exports to src/index.ts instead. -export * from './src'; diff --git a/fixtures/mock-oauth2-provider/package.json b/fixtures/mock-oauth2-provider/package.json index 9a486af71100..580c873edd55 100644 --- a/fixtures/mock-oauth2-provider/package.json +++ b/fixtures/mock-oauth2-provider/package.json @@ -5,6 +5,8 @@ "engines": { "node": ">=10" }, + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "lb-tsc", "clean": "lb-clean loopback-mock-oauth2-provider*.tgz dist *.tsbuildinfo package", From a9c2c98ea241b0f00cc1a5735903f00cd987e5fd Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 22 May 2020 08:54:51 -0700 Subject: [PATCH 3/4] feat(mock-oauth2-provider): improve the app with tests and scripts --- fixtures/mock-oauth2-provider/package.json | 13 +-- .../acceptance/mock-oauth2.acceptance.ts | 79 +++++++++++++++++++ fixtures/mock-oauth2-provider/src/index.ts | 5 ++ .../src/mock-oauth2-social-app.ts | 79 ++++++++++++------- .../src/user-repository.ts | 8 +- fixtures/mock-oauth2-provider/tsconfig.json | 6 +- 6 files changed, 151 insertions(+), 39 deletions(-) create mode 100644 fixtures/mock-oauth2-provider/src/__tests__/acceptance/mock-oauth2.acceptance.ts diff --git a/fixtures/mock-oauth2-provider/package.json b/fixtures/mock-oauth2-provider/package.json index 580c873edd55..82a4e3a01874 100644 --- a/fixtures/mock-oauth2-provider/package.json +++ b/fixtures/mock-oauth2-provider/package.json @@ -10,7 +10,9 @@ "scripts": { "build": "lb-tsc", "clean": "lb-clean loopback-mock-oauth2-provider*.tgz dist *.tsbuildinfo package", - "pretest": "npm run build", + "pretest": "npm run clean && npm run build", + "prestart": "npm run build", + "start": "node .", "test": "npm run mocha", "mocha": "lb-mocha \"dist/__tests__/**/*.js\"", "verify": "npm pack && tar xf loopback-mock-oauth2-provider*.tgz && tree package && npm run clean" @@ -31,6 +33,9 @@ "url": "https://github.com/strongloop/loopback-next.git", "directory": "fixtures/mock-oauth2-provider" }, + "publishConfig": { + "access": "public" + }, "dependencies": { "@types/body-parser": "^1.19.0", "@types/express": "^4.17.6", @@ -47,9 +52,7 @@ }, "devDependencies": { "@loopback/build": "^5.4.1", - "@loopback/eslint-config": "^7.0.1" - }, - "publishConfig": { - "access": "public" + "@loopback/eslint-config": "^7.0.1", + "@loopback/testlab": "^3.1.5" } } diff --git a/fixtures/mock-oauth2-provider/src/__tests__/acceptance/mock-oauth2.acceptance.ts b/fixtures/mock-oauth2-provider/src/__tests__/acceptance/mock-oauth2.acceptance.ts new file mode 100644 index 000000000000..2b1828a281dd --- /dev/null +++ b/fixtures/mock-oauth2-provider/src/__tests__/acceptance/mock-oauth2.acceptance.ts @@ -0,0 +1,79 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/mock-oauth2-provider +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {supertest} from '@loopback/testlab'; +import {Server} from 'http'; +import {MockTestOauth2SocialApp} from '../..'; + +/* eslint-disable @typescript-eslint/camelcase */ +describe('mock-oauth2-provider', () => { + let server: Server; + before(() => (server = MockTestOauth2SocialApp.startMock(0))); + after(MockTestOauth2SocialApp.stopMock); + + it('exposes GET /login', () => { + return supertest(server).get('/login').expect(200); + }); + + it('exposes GET /verify', () => { + return supertest(server) + .get('/verify?access_token=123') + .expect(401, {error: 'invalid token'}); + }); + + it('exposes GET /oauth/dialog', () => { + return supertest(server) + .get('/oauth/dialog') + .query({ + redirect_uri: 'http://localhost:3000/callback', + client_id: '1111', + }) + .expect(302) + .expect( + 'location', + '/login?client_id=1111&redirect_uri=http://localhost:3000/callback', + ); + }); + + it('exposes GET /oauth/dialog with scope', () => { + return supertest(server) + .get('/oauth/dialog') + .query({ + redirect_uri: 'http://localhost:3000/callback', + client_id: '1111', + scope: 'email', + }) + .expect(302) + .expect( + 'location', + '/login?client_id=1111&redirect_uri=http://localhost:3000/callback&scope=email', + ); + }); + + it('exposes GET /oauth/dialog - missing redirect_uri', () => { + return supertest(server) + .get('/oauth/dialog') + .expect(400, {error: 'missing redirect_uri'}); + }); + + it('exposes GET /oauth/dialog - missing client_id', () => { + return supertest(server) + .get('/oauth/dialog') + .query({redirect_uri: 'http://localhost:3000/callback'}) + .expect(400, {error: 'missing client_id'}); + }); + + it('exposes GET /oauth/token - invalid client id', () => { + return supertest(server) + .get('/oauth/token?client_id=123') + .expect(401, {error: 'invalid client id'}); + }); + + it('exposes GET /oauth/token', () => { + return supertest(server) + .get('/oauth/token?client_id=1111') + .expect(401, {error: 'invalid code'}); + }); +}); diff --git a/fixtures/mock-oauth2-provider/src/index.ts b/fixtures/mock-oauth2-provider/src/index.ts index b39106a80125..3ed4ee35fa5e 100644 --- a/fixtures/mock-oauth2-provider/src/index.ts +++ b/fixtures/mock-oauth2-provider/src/index.ts @@ -10,3 +10,8 @@ export namespace MockTestOauth2SocialApp { export const startMock = startApp; export const stopMock = stopApp; } + +if (require.main === module) { + const server = startApp(); + console.log('Mock oAuth2 provider is running at %s', server.address()); +} diff --git a/fixtures/mock-oauth2-provider/src/mock-oauth2-social-app.ts b/fixtures/mock-oauth2-provider/src/mock-oauth2-social-app.ts index 57630ce01012..dec3b14fa8aa 100644 --- a/fixtures/mock-oauth2-provider/src/mock-oauth2-social-app.ts +++ b/fixtures/mock-oauth2-provider/src/mock-oauth2-social-app.ts @@ -27,7 +27,7 @@ const app = express(); let server: Server; // to support json payload in body -app.use('parse', bodyParser.json()); +app.use(bodyParser.json()); // to support html form bodies app.use(bodyParser.text({type: 'text/html'})); // create application/x-www-form-urlencoded parser @@ -41,7 +41,7 @@ interface JWT { } /** - * datastructure for an app registration, also holds issued tokens for an app + * data structure for an app registration, also holds issued tokens for an app */ interface App { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -107,8 +107,8 @@ const users: MyUser[] = [ /** * find a user by a name and password - * @param {*} username - * @param {*} password + * @param username - User name + * @param password - Password */ function findUser(username: string, password: string) { return users.find( @@ -118,9 +118,9 @@ function findUser(username: string, password: string) { /** * create a jwt token - * @param {*} user - * @param {*} scopes - * @param {*} signingKey + * @param user - User + * @param scopes - Scopes + * @param signingKey - Signing key */ async function createJwt( user: MyUser, @@ -161,11 +161,12 @@ async function createJwt( * * check with given client id and token if token is valid * - * @param {*} req - * @param {*} token + * @param req - request + * @param token - token */ async function verifyToken(token: string) { const unwrappedJwt = jwt.decode(token, {json: true, complete: true}); + if (unwrappedJwt == null) throw new Error('invalid token'); const tokenId: string = unwrappedJwt?.payload.jti; const registeredApp: App = registeredApps[unwrappedJwt?.payload.client_id]; if (registeredApp) { @@ -193,22 +194,25 @@ async function verifyToken(token: string) { */ app.get('/oauth/dialog', function (req, res) { if (!req.query.redirect_uri) { - res.setHeader('Content-Type', 'application/json'); - res - .status(500) - .send(JSON.stringify({error: 'redirect_uri not sent in query'})); + res.status(400).send({error: 'missing redirect_uri'}); + return; + } + if (!req.query.client_id) { + res.status(400).send({error: 'missing client_id'}); return; } if (registeredApps[req.query.client_id as string]) { let params = '?client_id=' + req.query.client_id + - '&&redirect_uri=' + + '&redirect_uri=' + req.query.redirect_uri; - params = params + '&&scope=' + req.query.scope; + if (req.query.scope) { + params = params + '&scope=' + req.query.scope; + } res.redirect('/login' + params); } else { - res.send('invalid app'); + res.status(401).send({error: 'invalid client_id'}); } }); @@ -253,6 +257,10 @@ app.get('/login', function (req, response) { * 4. redirects to callback url with access code */ app.post('/login_submit', urlencodedParser, async function (req, res) { + if (!req.body.username) { + res.status(400).send({error: 'missing username'}); + return; + } const user: MyUser | undefined = findUser( req.body.username, req.body.password, @@ -288,18 +296,22 @@ app.post('/login_submit', urlencodedParser, async function (req, res) { * returns token in exchange for access code */ app.post('/oauth/token', urlencodedParser, function (req, res) { + if (!req.body.client_id) { + res.status(400).send({error: 'missing client_id'}); + return; + } if (registeredApps[req.body.client_id]) { //&& apps[req.query.client_id].client_secret === req.query.client_secret - const oauthstates = registeredApps[req.body.client_id].tokens; - if (oauthstates[req.body.code]) { + const oauthStates = registeredApps[req.body.client_id].tokens; + if (oauthStates[req.body.code]) { res.setHeader('Content-Type', 'application/json'); // eslint-disable-next-line @typescript-eslint/camelcase - res.send({access_token: oauthstates[req.body.code].token}); + res.send({access_token: oauthStates[req.body.code].token}); } else { - res.sendStatus(401); + res.status(401).send({error: 'invalid code'}); } } else { - res.sendStatus(401); + res.status(401).send({error: 'invalid client_id'}); } }); @@ -311,19 +323,23 @@ app.post('/oauth/token', urlencodedParser, function (req, res) { */ app.get('/oauth/token', function (req, res) { const clientId = req.query.client_id as string; + if (!clientId) { + res.status(400).send({error: 'missing client_id'}); + return; + } if (registeredApps[clientId]) { //&& apps[req.query.client_id].client_secret === req.query.client_secret - const oauthstates = registeredApps[clientId].tokens; + const oauthStates = registeredApps[clientId].tokens; const code = req.query.code as string; - if (oauthstates[code]) { + if (oauthStates[code]) { res.setHeader('Content-Type', 'application/json'); // eslint-disable-next-line @typescript-eslint/camelcase - res.send({access_token: oauthstates[code].token}); + res.send({access_token: oauthStates[code].token}); } else { - res.sendStatus(401); + res.status(401).send({error: 'invalid code'}); } } else { - res.sendStatus(401); + res.status(401).send({error: 'invalid client id'}); } }); @@ -337,18 +353,23 @@ app.get('/verify', async function (req, res) { try { const token = (req.query.access_token ?? req.header('Authorization')) as string; + if (!token) { + res.status(400).send({error: 'missing access_token'}); + return; + } const result = await verifyToken(token); const expirationTime = result.exp; res.setHeader('Content-Type', 'application/json'); res.send({...result, expirationTime: expirationTime}); } catch (err) { res.setHeader('Content-Type', 'application/json'); - res.status(401).send(JSON.stringify({error: err})); + res.status(401).send({error: err.message}); } }); -export function startApp() { - server = app.listen(9000); +export function startApp(port = 9000) { + server = app.listen(port); + return server; } export function stopApp() { diff --git a/fixtures/mock-oauth2-provider/src/user-repository.ts b/fixtures/mock-oauth2-provider/src/user-repository.ts index f4507784c75c..87981de96a75 100644 --- a/fixtures/mock-oauth2-provider/src/user-repository.ts +++ b/fixtures/mock-oauth2-provider/src/user-repository.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT 'use strict'; -const _ = require('lodash'); +import _ from 'lodash'; /** * A simple User model @@ -24,14 +24,14 @@ export interface MyUser { * Repository to store and access user objects */ export class UserRepository { - constructor(readonly list: {[key: string]: MyUser}) {} + constructor(readonly list: Record) {} /** * find by username * @param username */ - find(username: string): MyUser { - return _.filter(this.list, (user: MyUser) => user.username === username); + find(username: string): MyUser[] { + return _.filter(this.list, user => user.username === username); } /** diff --git a/fixtures/mock-oauth2-provider/tsconfig.json b/fixtures/mock-oauth2-provider/tsconfig.json index d735c5979bc6..b4047b555391 100644 --- a/fixtures/mock-oauth2-provider/tsconfig.json +++ b/fixtures/mock-oauth2-provider/tsconfig.json @@ -9,5 +9,9 @@ "include": [ "src" ], - "references": [] + "references": [ + { + "path": "../../packages/testlab/tsconfig.json" + } + ] } From 5f16eba0d6a23ac2099b8bce9001e0c7bb67e74f Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 22 May 2020 09:57:44 -0700 Subject: [PATCH 4/4] test: clean up tests that use mock-auth2-provider --- .../acceptance/passport-login.acceptance.ts | 40 +++++++-------- examples/passport-login/src/server.ts | 2 +- .../web-application/express-app.js | 2 +- ...port-strategy-oauth2-adapter.acceptance.ts | 49 +++++++++++-------- 4 files changed, 49 insertions(+), 44 deletions(-) diff --git a/examples/passport-login/src/__tests__/acceptance/passport-login.acceptance.ts b/examples/passport-login/src/__tests__/acceptance/passport-login.acceptance.ts index 808fceece188..baf83f08f222 100644 --- a/examples/passport-login/src/__tests__/acceptance/passport-login.acceptance.ts +++ b/examples/passport-login/src/__tests__/acceptance/passport-login.acceptance.ts @@ -38,23 +38,23 @@ describe('example-passport-login acceptance test', () => { * This test uses the mock social app from the @loopback/authentication-passport package, * as oauth2 profile endpoint. */ - before(MockTestOauth2SocialApp.startMock); + before(() => MockTestOauth2SocialApp.startMock()); after(MockTestOauth2SocialApp.stopMock); before(async function setupApplication(this: Mocha.Context) { this.timeout(6000); server = await startApplication(oauth2Providers); - client = supertest('http://127.0.0.1:3000'); + client = supertest(server.webApp); }); before(async function clearTestData() { - await supertest('') - .delete('http://localhost:3000/api/clear') + await client + .delete('/api/clear') .auth('admin', 'password', {type: 'basic'}); }); after(async function clearTestData() { - await supertest('') - .delete('http://localhost:3000/api/clear') + await client + .delete('/api/clear') .auth('admin', 'password', {type: 'basic'}); }); @@ -140,24 +140,22 @@ describe('example-passport-login acceptance test', () => { it('check if user was registered', async () => { const filter = 'filter={"where":{"email": "test@example.com"}}'; - const response = await supertest('') - .get('http://localhost:3000/api/users') - .query(filter); + const response = await client.get('/api/users').query(filter); const users = response.body as User[]; expect(users.length).to.eql(1); expect(users[0].email).to.eql('test@example.com'); }); it('able to invoke api endpoints with basic auth', async () => { - await supertest('') - .get('http://localhost:3000/api/profiles') + await client + .get('/api/profiles') .auth('test@example.com', 'password', {type: 'basic'}) .expect(204); }); it('basic auth fails for incorrect password', async () => { - await supertest('') - .get('http://localhost:3000/api/profiles') + await client + .get('/api/profiles') .auth('test@example.com', 'incorrect-password', {type: 'basic'}) .expect(401); }); @@ -212,8 +210,8 @@ describe('example-passport-login acceptance test', () => { }; // On successful login, the authorizing app redirects to the callback url // HTTP status code 302 is returned to the browser - const response = await supertest('') - .post('http://localhost:9000/login_submit') + const response = await supertest('http://localhost:9000') + .post('/login_submit') .send(qs.stringify(params)) .expect(302); callbackToLbApp = response.get('Location'); @@ -267,8 +265,8 @@ describe('example-passport-login acceptance test', () => { }); it('check if profile is linked to existing user', async () => { - const response = await supertest('') - .get('http://localhost:3000/api/profiles') + const response = await client + .get('/api/profiles') .auth('test@example.com', 'password', {type: 'basic'}); const profiles = response.body as UserIdentity[]; expect(profiles?.length).to.eql(1); @@ -336,8 +334,8 @@ describe('example-passport-login acceptance test', () => { }; // On successful login, the authorizing app redirects to the callback url // HTTP status code 302 is returned to the browser - const response = await supertest('') - .post('http://localhost:9000/login_submit') + const response = await supertest('http://localhost:9000') + .post('/login_submit') .send(qs.stringify(params)) .expect(302); callbackToLbApp = response.get('Location'); @@ -392,9 +390,7 @@ describe('example-passport-login acceptance test', () => { it('check if a new user was registered', async () => { const filter = 'filter={"where":{"email": "usr1@lb.com"}}'; - const response = await supertest('') - .get('http://localhost:3000/api/users') - .query(filter); + const response = await client.get('/api/users').query(filter); const users = response.body as User[]; expect(users.length).to.eql(1); expect(users[0].email).to.eql('usr1@lb.com'); diff --git a/examples/passport-login/src/server.ts b/examples/passport-login/src/server.ts index da9f5848935e..aebc83f17a61 100644 --- a/examples/passport-login/src/server.ts +++ b/examples/passport-login/src/server.ts @@ -68,7 +68,7 @@ export class ExpressServer { this.server = this.webApp.listen(port, host); await once(this.server, 'listening'); const add = this.server.address(); - this.url = `https://${add.address}:${add.port}`; + this.url = `http://${add.address}:${add.port}`; } /** diff --git a/examples/passport-login/web-application/express-app.js b/examples/passport-login/web-application/express-app.js index 15c98cd60c66..bb5b4af05560 100644 --- a/examples/passport-login/web-application/express-app.js +++ b/examples/passport-login/web-application/express-app.js @@ -31,7 +31,7 @@ app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); // to support json payload in body -app.use('parse', bodyParser.json()); +app.use(bodyParser.json()); // to support html form bodies app.use(bodyParser.text({type: 'text/html'})); // create application/x-www-form-urlencoded parser diff --git a/extensions/authentication-passport/src/__tests__/acceptance/authentication-with-passport-strategy-oauth2-adapter.acceptance.ts b/extensions/authentication-passport/src/__tests__/acceptance/authentication-with-passport-strategy-oauth2-adapter.acceptance.ts index 4624f8704d45..d7675c62b305 100644 --- a/extensions/authentication-passport/src/__tests__/acceptance/authentication-with-passport-strategy-oauth2-adapter.acceptance.ts +++ b/extensions/authentication-passport/src/__tests__/acceptance/authentication-with-passport-strategy-oauth2-adapter.acceptance.ts @@ -3,33 +3,37 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {UserProfileFactory, authenticate} from '@loopback/authentication'; -import { - Strategy as Oauth2Strategy, - StrategyOptions, - VerifyFunction, - VerifyCallback, -} from 'passport-oauth2'; -import {MyUser, userRepository} from '@loopback/mock-oauth2-provider'; +import {authenticate, UserProfileFactory} from '@loopback/authentication'; +import {inject} from '@loopback/core'; import { - simpleRestApplication, - configureApplication, -} from './fixtures/simple-rest-app'; -import {securityId, UserProfile, SecurityBindings} from '@loopback/security'; -import {StrategyAdapter} from '../../strategy-adapter'; + MockTestOauth2SocialApp, + MyUser, + userRepository, +} from '@loopback/mock-oauth2-provider'; import {get} from '@loopback/openapi-v3'; +import {Response, RestApplication, RestBindings} from '@loopback/rest'; +import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; import { Client, createClientForHandler, expect, supertest, } from '@loopback/testlab'; -import {RestApplication, RestBindings, Response} from '@loopback/rest'; -import {MockTestOauth2SocialApp} from '@loopback/mock-oauth2-provider'; -import * as url from 'url'; -import {inject} from '@loopback/core'; import axios from 'axios'; +import {AddressInfo} from 'net'; +import { + Strategy as Oauth2Strategy, + StrategyOptions, + VerifyCallback, + VerifyFunction, +} from 'passport-oauth2'; import qs from 'qs'; +import * as url from 'url'; +import {StrategyAdapter} from '../../strategy-adapter'; +import { + configureApplication, + simpleRestApplication, +} from './fixtures/simple-rest-app'; /** * This test consists of three main components -> the supertest client, the LoopBack app (simple-rest-app.ts) @@ -156,8 +160,13 @@ describe('Oauth2 authorization flow', () => { let app: RestApplication; let oauth2Strategy: StrategyAdapter; let client: Client; + let oauth2Client: Client; - before(MockTestOauth2SocialApp.startMock); + before(() => { + const server = MockTestOauth2SocialApp.startMock(); + const port = (server.address() as AddressInfo).port; + oauth2Client = supertest(`http://localhost:${port}`); + }); after(MockTestOauth2SocialApp.stopMock); before(givenLoopBackApp); @@ -205,8 +214,8 @@ describe('Oauth2 authorization flow', () => { }; // On successful login, the authorizing app redirects to the callback url // HTTP status code 302 is returned to the browser - const response = await supertest('') - .post('http://localhost:9000/login_submit') + const response = await oauth2Client + .post('/login_submit') .send(qs.stringify(params)) .expect(302); callbackToLbApp = response.get('Location');