Skip to content

Commit 582de07

Browse files
.
. . . . . .
1 parent 37b1963 commit 582de07

File tree

4 files changed

+89
-31
lines changed

4 files changed

+89
-31
lines changed

test/apps/app/server/proxy.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
const express = require('express');
22
const createProxyMiddleware = require('./proxyMiddleware');
33

4+
// Proxy should use different (from your SPA) port or domain to have different local/session storage.
5+
// SIW initially clears transaction storage, so state saved in SPA could not be readen on login callback
6+
// and `handleRedirect` would produce the error:
7+
// AuthSdkError: Could not load PKCE codeVerifier from storage.
8+
// This may indicate the auth flow has already completed or multiple auth flows are executing concurrently.
9+
//
10+
// Okta org setup:
11+
// - Proxy URL should be added to Trusted Origins.
12+
// - `<proxy>/login/callback` should be added to Redirect URIs of app with CLIENT_ID
13+
414
module.exports = function createProxyApp({ proxyPort }) {
5-
// Proxy should use another port or domain to have different local/session storage.
6-
// SIW initially clears transaction storage, so state could not be readen on login callback.
7-
// AuthSdkError: Could not load PKCE codeVerifier from storage. This may indicate the auth flow has already completed or multiple auth flows are executing concurrently.
8-
const proxyApp = express();
9-
const { origin } = new URL(process.env.ISSUER);
10-
const proxyMiddleware = createProxyMiddleware({
11-
origin,
12-
proxyPort
13-
});
14-
proxyApp.use('/api', proxyMiddleware); // /api/v1/sessions/me
15-
proxyApp.use('/oauth2', proxyMiddleware); // /oauth2/v1
16-
proxyApp.use('/idp/idx', proxyMiddleware);
17-
proxyApp.use('/login/token/redirect', proxyMiddleware);
18-
proxyApp.use('/app', proxyMiddleware);
19-
proxyApp.use('/.well-known', proxyMiddleware);
20-
return proxyApp;
15+
const proxyApp = express();
16+
const { origin } = new URL(process.env.ISSUER);
17+
const proxyMiddleware = createProxyMiddleware({
18+
origin,
19+
proxyPort
20+
});
21+
proxyApp.use('/api', proxyMiddleware); // /api/v1/sessions/me
22+
proxyApp.use('/oauth2', proxyMiddleware); // /oauth2/v1
23+
proxyApp.use('/idp/idx', proxyMiddleware);
24+
proxyApp.use('/login/token/redirect', proxyMiddleware);
25+
proxyApp.use('/app', proxyMiddleware);
26+
proxyApp.use('/.well-known', proxyMiddleware);
27+
return proxyApp;
2128
};

test/apps/app/server/proxyMiddleware.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware');
22

3+
// Explanation for need of response rewrites in `onProxyRes`:
4+
//
5+
// HTML page `<proxy>/oauth2/v1/authorize` contains script with config for SIW with var `baseUrl`.
6+
// `baseUrl` value equals to <origin>, it is used for IDX API requests.
7+
// Need to replace <origin> to <proxy> in `baseUrl`.
8+
// Otherwise response to `<origin>/idp/idx/identify` after successful login would contain redirect URL
9+
// `<origin>/login/token/redirect?stateToken=xxx` which would render HTTP 403 error.
10+
// The problem relates to `DT` cookie which is set on page `<proxy>/oauth2/v1/authorize`
11+
// for domain <proxy>, but not <origin>.
12+
// Since cookie for <origin> domain can't be set from <proxy> server response (unless they are in same domain)
13+
// and there is no way to configure value of `baseUrl`, it should be intercepted and replaced in a response.
14+
//
15+
// <origin> should be replaced to <proxy> in IDX API responses, but not for `/.well-known`.
16+
// Otherwise `handleRedirect` will produce error `AuthSdkError: The issuer [origin] does not match [proxy]`
17+
318
function escapeUri(str) {
419
return [
520
[':', '\\x3A'],
@@ -18,14 +33,17 @@ module.exports = function proxyMiddlewareFactory({ proxyPort, origin }) {
1833
secure: false,
1934
changeOrigin: true,
2035
selfHandleResponse: true,
36+
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
2137
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
2238
const response = responseBuffer.toString('utf8');
2339
let patchedResponse = response;
24-
patchedResponse = patchedResponse.replace(
25-
buildRegexForUri(origin),
26-
escapeUri(`http://localhost:${proxyPort}`)
27-
);
28-
if (!req.url.includes('/.well-known') ) {
40+
if (req.url.includes('/oauth2/v1/authorize') ) {
41+
patchedResponse = patchedResponse.replace(
42+
buildRegexForUri(origin),
43+
escapeUri(`http://localhost:${proxyPort}`)
44+
);
45+
}
46+
if (req.url.includes('/idp/idx/') ) {
2947
patchedResponse = patchedResponse.replace(
3048
new RegExp(origin, 'g'),
3149
`http://localhost:${proxyPort}`

test/e2e/specs/proxy.js

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,54 @@
1313
import assert from 'assert';
1414
import TestApp from '../pageobjects/TestApp';
1515
import { openPKCE } from '../util/appUtils';
16-
import { loginRedirect } from '../util/loginUtils';
16+
import { loginRedirect, loginRedirectWithSso } from '../util/loginUtils';
1717
import { getIssuer, getBaseUrl } from '../util/browserUtils';
1818

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

21-
describe('E2E through proxy', () => {
22-
beforeEach(async function bootstrap() {
23-
await openPKCE({
24-
issuer: proxyIssuer
25-
});
26-
await TestApp.issuer.then(el => el.getValue()).then(val => {
27-
assert(val.indexOf('http://localhost') === 0);
28-
});
29-
await loginRedirect('pkce');
21+
async function bootstrap(options, openInNewWindow) {
22+
await openPKCE({
23+
issuer: proxyIssuer,
24+
...(options || {})
25+
}, openInNewWindow);
26+
await TestApp.issuer.then(el => el.getValue()).then(val => {
27+
assert(val.indexOf('http://localhost') === 0);
3028
});
29+
}
3130

31+
describe('E2E through proxy', () => {
3232
afterEach(async function teardown() {
3333
if (await TestApp.isAuthenticated()) {
3434
await TestApp.logoutRedirect();
3535
}
3636
});
3737

3838
it('can login and receive tokens', async () => {
39+
await bootstrap();
40+
await loginRedirect('pkce');
41+
await TestApp.assertLoggedIn();
42+
});
43+
44+
it('should use SSO session', async () => {
45+
// Open tab 1 and sign in
46+
// Use `sessionStorage` to not share tokens between tabs
47+
await bootstrap({ storage: 'sessionStorage' });
48+
await loginRedirect('pkce');
49+
await TestApp.assertLoggedIn();
50+
51+
// Open tab 2
52+
// SSO session should exist, but should be not logged in to app
53+
await bootstrap({ storage: 'sessionStorage' }, true);
54+
await TestApp.waitForLoginBtn();
55+
56+
// Should be able to sign in without entering credentials
57+
await loginRedirectWithSso('pkce');
3958
await TestApp.assertLoggedIn();
4059
});
4160

4261
it('should end SSO session on logout', async () => {
62+
await bootstrap();
63+
await loginRedirect('pkce');
4364
await TestApp.assertLoggedIn();
4465

4566
// SSO session should exist
@@ -58,6 +79,10 @@ describe('E2E through proxy', () => {
5879
if (!process.env.REFRESH_TOKEN) {
5980
return;
6081
}
82+
83+
await bootstrap();
84+
await loginRedirect('pkce');
85+
6186
const prev = {
6287
idToken: await TestApp.idToken.then(el => el.getText()),
6388
accessToken: await TestApp.accessToken.then(el => el.getText())
@@ -77,6 +102,9 @@ describe('E2E through proxy', () => {
77102
// TODO: is this possible?
78103
// eslint-disable-next-line jasmine/no-disabled-tests
79104
xit('can refresh all tokens using getWithoutPrompt', async () => {
105+
await bootstrap();
106+
await loginRedirect('pkce');
107+
80108
const prev = {
81109
idToken: await TestApp.idToken.then(el => el.getText()),
82110
accessToken: await TestApp.accessToken.then(el => el.getText())

test/e2e/util/loginUtils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export async function loginRedirect(flow, responseMode) {
6464
return handleCallback(flow, responseMode);
6565
}
6666

67+
export async function loginRedirectWithSso(flow, responseMode) {
68+
await TestApp.loginRedirect();
69+
return handleCallback(flow, responseMode);
70+
}
71+
6772
export async function loginDirect(setCredentials = true) {
6873
if (setCredentials) {
6974
await TestApp.username.then(el => el.setValue(USERNAME));

0 commit comments

Comments
 (0)