Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
.
.
.
.
.
.
  • Loading branch information
denysoblohin-okta committed Jan 23, 2023
1 parent 37b1963 commit 582de07
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 31 deletions.
39 changes: 23 additions & 16 deletions test/apps/app/server/proxy.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
const express = require('express');
const createProxyMiddleware = require('./proxyMiddleware');

// Proxy should use different (from your SPA) port or domain to have different local/session storage.
// SIW initially clears transaction storage, so state saved in SPA could not be readen on login callback
// and `handleRedirect` would produce the error:
// AuthSdkError: Could not load PKCE codeVerifier from storage.
// This may indicate the auth flow has already completed or multiple auth flows are executing concurrently.
//
// Okta org setup:
// - Proxy URL should be added to Trusted Origins.
// - `<proxy>/login/callback` should be added to Redirect URIs of app with CLIENT_ID

module.exports = function createProxyApp({ proxyPort }) {
// Proxy should use another port or domain to have different local/session storage.
// SIW initially clears transaction storage, so state could not be readen on login callback.
// AuthSdkError: Could not load PKCE codeVerifier from storage. This may indicate the auth flow has already completed or multiple auth flows are executing concurrently.
const proxyApp = express();
const { origin } = new URL(process.env.ISSUER);
const proxyMiddleware = createProxyMiddleware({
origin,
proxyPort
});
proxyApp.use('/api', proxyMiddleware); // /api/v1/sessions/me
proxyApp.use('/oauth2', proxyMiddleware); // /oauth2/v1
proxyApp.use('/idp/idx', proxyMiddleware);
proxyApp.use('/login/token/redirect', proxyMiddleware);
proxyApp.use('/app', proxyMiddleware);
proxyApp.use('/.well-known', proxyMiddleware);
return proxyApp;
const proxyApp = express();
const { origin } = new URL(process.env.ISSUER);
const proxyMiddleware = createProxyMiddleware({
origin,
proxyPort
});
proxyApp.use('/api', proxyMiddleware); // /api/v1/sessions/me
proxyApp.use('/oauth2', proxyMiddleware); // /oauth2/v1
proxyApp.use('/idp/idx', proxyMiddleware);
proxyApp.use('/login/token/redirect', proxyMiddleware);
proxyApp.use('/app', proxyMiddleware);
proxyApp.use('/.well-known', proxyMiddleware);
return proxyApp;
};
28 changes: 23 additions & 5 deletions test/apps/app/server/proxyMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware');

// Explanation for need of response rewrites in `onProxyRes`:
//
// HTML page `<proxy>/oauth2/v1/authorize` contains script with config for SIW with var `baseUrl`.
// `baseUrl` value equals to <origin>, it is used for IDX API requests.
// Need to replace <origin> to <proxy> in `baseUrl`.
// Otherwise response to `<origin>/idp/idx/identify` after successful login would contain redirect URL
// `<origin>/login/token/redirect?stateToken=xxx` which would render HTTP 403 error.
// The problem relates to `DT` cookie which is set on page `<proxy>/oauth2/v1/authorize`
// for domain <proxy>, but not <origin>.
// Since cookie for <origin> domain can't be set from <proxy> server response (unless they are in same domain)
// and there is no way to configure value of `baseUrl`, it should be intercepted and replaced in a response.
//
// <origin> should be replaced to <proxy> in IDX API responses, but not for `/.well-known`.
// Otherwise `handleRedirect` will produce error `AuthSdkError: The issuer [origin] does not match [proxy]`

function escapeUri(str) {
return [
[':', '\\x3A'],
Expand All @@ -18,14 +33,17 @@ module.exports = function proxyMiddlewareFactory({ proxyPort, origin }) {
secure: false,
changeOrigin: true,
selfHandleResponse: true,
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
const response = responseBuffer.toString('utf8');
let patchedResponse = response;
patchedResponse = patchedResponse.replace(
buildRegexForUri(origin),
escapeUri(`http://localhost:${proxyPort}`)
);
if (!req.url.includes('/.well-known') ) {
if (req.url.includes('/oauth2/v1/authorize') ) {
patchedResponse = patchedResponse.replace(
buildRegexForUri(origin),
escapeUri(`http://localhost:${proxyPort}`)
);
}
if (req.url.includes('/idp/idx/') ) {
patchedResponse = patchedResponse.replace(
new RegExp(origin, 'g'),
`http://localhost:${proxyPort}`
Expand Down
48 changes: 38 additions & 10 deletions test/e2e/specs/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,54 @@
import assert from 'assert';
import TestApp from '../pageobjects/TestApp';
import { openPKCE } from '../util/appUtils';
import { loginRedirect } from '../util/loginUtils';
import { loginRedirect, loginRedirectWithSso } from '../util/loginUtils';
import { getIssuer, getBaseUrl } from '../util/browserUtils';

const proxyIssuer = getIssuer().replace(getBaseUrl(), 'http://localhost:8082');

describe('E2E through proxy', () => {
beforeEach(async function bootstrap() {
await openPKCE({
issuer: proxyIssuer
});
await TestApp.issuer.then(el => el.getValue()).then(val => {
assert(val.indexOf('http://localhost') === 0);
});
await loginRedirect('pkce');
async function bootstrap(options, openInNewWindow) {
await openPKCE({
issuer: proxyIssuer,
...(options || {})
}, openInNewWindow);
await TestApp.issuer.then(el => el.getValue()).then(val => {
assert(val.indexOf('http://localhost') === 0);
});
}

describe('E2E through proxy', () => {
afterEach(async function teardown() {
if (await TestApp.isAuthenticated()) {
await TestApp.logoutRedirect();
}
});

it('can login and receive tokens', async () => {
await bootstrap();
await loginRedirect('pkce');
await TestApp.assertLoggedIn();
});

it('should use SSO session', async () => {
// Open tab 1 and sign in
// Use `sessionStorage` to not share tokens between tabs
await bootstrap({ storage: 'sessionStorage' });
await loginRedirect('pkce');
await TestApp.assertLoggedIn();

// Open tab 2
// SSO session should exist, but should be not logged in to app
await bootstrap({ storage: 'sessionStorage' }, true);
await TestApp.waitForLoginBtn();

// Should be able to sign in without entering credentials
await loginRedirectWithSso('pkce');
await TestApp.assertLoggedIn();
});

it('should end SSO session on logout', async () => {
await bootstrap();
await loginRedirect('pkce');
await TestApp.assertLoggedIn();

// SSO session should exist
Expand All @@ -58,6 +79,10 @@ describe('E2E through proxy', () => {
if (!process.env.REFRESH_TOKEN) {
return;
}

await bootstrap();
await loginRedirect('pkce');

const prev = {
idToken: await TestApp.idToken.then(el => el.getText()),
accessToken: await TestApp.accessToken.then(el => el.getText())
Expand All @@ -77,6 +102,9 @@ describe('E2E through proxy', () => {
// TODO: is this possible?
// eslint-disable-next-line jasmine/no-disabled-tests
xit('can refresh all tokens using getWithoutPrompt', async () => {
await bootstrap();
await loginRedirect('pkce');

const prev = {
idToken: await TestApp.idToken.then(el => el.getText()),
accessToken: await TestApp.accessToken.then(el => el.getText())
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/util/loginUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export async function loginRedirect(flow, responseMode) {
return handleCallback(flow, responseMode);
}

export async function loginRedirectWithSso(flow, responseMode) {
await TestApp.loginRedirect();
return handleCallback(flow, responseMode);
}

export async function loginDirect(setCredentials = true) {
if (setCredentials) {
await TestApp.username.then(el => el.setValue(USERNAME));
Expand Down

0 comments on commit 582de07

Please sign in to comment.