Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added e2e tests for issuer proxy for OIE #1370

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions test/apps/app/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@

require('@okta/env').setEnvironmentVarsFromTestEnv(__dirname);

const createProxyMiddleware = require('./proxyMiddleware');
const loginMiddleware = require('./loginMiddleware');
const callbackMiddleware = require('./callbackMiddleware');
const renderWidget = require('./renderWidget');
const createProxyApp = require('./proxy');

const path = require('path');
const SIW_DIST = path.resolve(path.dirname(require.resolve('@okta/okta-signin-widget')), '..');
Expand All @@ -32,11 +32,6 @@ const app = express();
const config = require('../webpack.config.js');
const compiler = webpack(config);

// Set a proxy in front of Okta
const proxyMiddleware = createProxyMiddleware();
app.use('/oauth2', proxyMiddleware);
app.use('/app', proxyMiddleware);

// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
Expand All @@ -57,3 +52,10 @@ const port = config.devServer.port;
app.listen(port, function () {
console.log(`Test app running at http://localhost:${port}!\n`);
});

// Set a proxy in front of Okta
const proxyPort = config.devServer.proxyPort;
const proxyApp = createProxyApp({ proxyPort });
proxyApp.listen(proxyPort, function () {
console.log(`Test app running at http://localhost:${proxyPort}!\n`);
});
28 changes: 28 additions & 0 deletions test/apps/app/server/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +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 }) {
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;
};
55 changes: 50 additions & 5 deletions test/apps/app/server/proxyMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,55 @@
const { createProxyMiddleware } = require('http-proxy-middleware');
const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware');

module.exports = function proxyMiddlewareFactory(options) {
const { origin } = new URL(process.env.ISSUER);
return createProxyMiddleware(Object.assign({
// 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'],
['/', '\\x2F'],
['-', '\\x2D']
].reduce((str, [from, to]) => str.replace(new RegExp(from, 'g'), to), str);
}

function buildRegexForUri(str) {
return new RegExp(escapeUri(str).replace(/\\/g, '\\\\'), 'g');
}

module.exports = function proxyMiddlewareFactory({ proxyPort, origin }) {
return createProxyMiddleware({
target: origin,
secure: false,
changeOrigin: true,
}, options));
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;
if (req.url.includes('/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}`
);
}
return patchedResponse;
}),
});
};
28 changes: 25 additions & 3 deletions test/apps/app/src/testApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ function loginLinks(app: TestApp, onProtectedPage?: boolean): string {
<li class="pure-menu-item">
<a id="get-token" href="/" onclick="getToken(event)" class="pure-menu-link">Get Token (without prompt)</a>
</li>
<li class="pure-menu-item">
<a id="get-session" href="/" onclick="getSession(event)" class="pure-menu-link">Get Session</a>
</li>
<li class="pure-menu-item">
<a id="test-concurrent-login" href="/" onclick="testConcurrentLogin(event)" class="pure-menu-link">Test Concurrent Login</a>
</li>
Expand Down Expand Up @@ -169,6 +172,7 @@ function bindFunctions(testApp: TestApp, window: Window): void {
activateUser: testApp.activateUser.bind(testApp),
activateUserWithWidget: testApp.activateUserWithWidget.bind(testApp),
getToken: testApp.getToken.bind(testApp, {}),
getSession: testApp.getSession.bind(testApp, {}),
clearTokens: testApp.clearTokens.bind(testApp),
logoutRedirect: testApp.logoutRedirect.bind(testApp),
logoutXHR: testApp.logoutXHR.bind(testApp),
Expand Down Expand Up @@ -705,6 +709,13 @@ class TestApp {
});
}

async getSession(): Promise<void> {
return this.oktaAuth.session.get()
.then(res => {
document.getElementById('session-info').innerHTML = JSON.stringify(res);
});
}

async refreshSession(): Promise<object> {
return this.oktaAuth.session.refresh();
}
Expand Down Expand Up @@ -931,7 +942,7 @@ class TestApp {
if (idToken || accessToken) {
// Authenticated user home page
return `
<strong>Authenticated</strong>
<strong id="is-authenticated">Authenticated</strong>
<div class="flex-row">
<div class="left-column">
<div class="actions authenticated pure-menu">
Expand All @@ -948,6 +959,9 @@ class TestApp {
<li class="pure-menu-item">
<a id="get-token" href="/" onclick="getToken(event)" class="pure-menu-link">Get Token (without prompt)</a>
</li>
<li class="pure-menu-item">
<a id="get-session" href="/" onclick="getSession(event)" class="pure-menu-link">Get Session</a>
</li>
<li class="pure-menu-item">
<a id="clear-tokens" href="/" onclick="clearTokens(event)" class="pure-menu-link">Clear Tokens</a>
</li>
Expand Down Expand Up @@ -980,6 +994,7 @@ class TestApp {
</div>
<div class="right-column">
<div id="user-info"></div>
<div id="session-info"></div>
${ tokensHTML({idToken, accessToken, refreshToken})}
</div>
</div>
Expand All @@ -989,9 +1004,16 @@ class TestApp {
// Unauthenticated user, Login page
return `
<div class="box">
<strong>Unauthenticated</strong>
<strong id="is-authenticated">Unauthenticated</strong>
</div>
<div class="flex-row">
<div class="left-column">
${loginLinks(this)}
</div>
<div class="right-column">
<div id="session-info"></div>
</div>
</div>
${loginLinks(this)}
`;
}

Expand Down
2 changes: 2 additions & 0 deletions test/apps/app/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require('@okta/env').setEnvironmentVarsFromTestEnv(__dirname);
const path = require('path');
const webpack = require('webpack');
const PORT = process.env.PORT || 8080;
const PROXY_PORT = process.env.PROXY_PORT || 8082;

const babelOptions = {
presets: [
Expand Down Expand Up @@ -54,6 +55,7 @@ module.exports = {
devServer: {
contentBase: path.join(__dirname, 'public'),
port: PORT,
proxyPort: PROXY_PORT,
},
devtool: 'source-map',
module: {
Expand Down
42 changes: 42 additions & 0 deletions test/e2e/pageobjects/TestApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class TestApp {
get rootSelector() { return $('#root'); }
get readySelector() { return $('#root.rendered.loaded'); }
get landingSelector() { return $('body.oidc-app.landing'); }
get isAuthenticatedText() { return $('#is-authenticated'); }

// Authenticated landing
get logoutRedirectBtn() { return $('#logout-redirect'); }
Expand All @@ -32,7 +33,9 @@ class TestApp {
get getTokenBtn() { return $('#get-token'); }
get clearTokensBtn() { return $('#clear-tokens'); }
get getUserInfoBtn() { return $('#get-userinfo'); }
get getSessionInfoBtn() { return $('#get-session'); }
get userInfo() { return $('#user-info'); }
get sessionInfo() { return $('#session-info'); }
get sessionExpired() { return $('#session-expired'); }
get testConcurrentGetTokenBtn() { return $('#test-concurrent-get-token'); }
get loginWithAcrBtn() { return $('#login-acr'); }
Expand Down Expand Up @@ -96,6 +99,12 @@ class TestApp {
await browser.waitUntil(async () => this.readySelector.then(el => el.isExisting()), 5000, 'wait for ready selector');
}

async isAuthenticated() {
await this.waitForIsAuthenticatedText();
const isAuthenticatedText = (await (await this.isAuthenticatedText).getText()).trim();
return isAuthenticatedText === 'Authenticated';
}

async showLoginWidget() {
await this.waitForLoginBtn();
await this.loginWidgetBtn.then(el => el.click());
Expand Down Expand Up @@ -171,6 +180,17 @@ class TestApp {
}, 5000, 'wait for get user info btn');
}

async getSessionInfo() {
await browser.waitUntil(async () => {
const el = await this.getSessionInfoBtn;

if (el.isDisplayed()) {
await browser.execute('arguments[0].click();', el);
return true;
}
}, 5000, 'wait for get session info btn');
}

async getIdToken() {
return this.idToken.then(el => el.getText()).then(txt => {
try {
Expand Down Expand Up @@ -248,6 +268,10 @@ class TestApp {
await this.pkceOptionOff.then(el=> el.click());
}

async waitForIsAuthenticatedText() {
return browser.waitUntil(async () => this.isAuthenticatedText.then(el => el.isDisplayed()), 5000, 'wait for is authenticated text');
}

async waitForLoginBtn() {
return browser.waitUntil(async () => this.loginRedirectBtn.then(el => el.isDisplayed()), 5000, 'wait for login button');
}
Expand All @@ -268,6 +292,10 @@ class TestApp {
return browser.waitUntil(async () => this.userInfo.then(el => el.isDisplayed()), 5000, 'wait for user info');
}

async waitForSessionInfo() {
return browser.waitUntil(async () => this.sessionInfo.then(el => el.isDisplayed()), 5000, 'wait for session info');
}

async waitForSigninWidget() {
return browser.waitUntil(async () => this.signinWidget.then(el => el.isDisplayed()), 5000, 'wait for signin widget');
}
Expand Down Expand Up @@ -336,6 +364,20 @@ class TestApp {
});
}

async assertSessionExists() {
await this.waitForSessionInfo();
await this.sessionInfo.then(el => el.getText()).then(txt => {
assert(txt.indexOf('"ACTIVE"') > 0);
});
}

async assertSessionNotExists() {
await this.waitForSessionInfo();
await this.sessionInfo.then(el => el.getText()).then(txt => {
assert(txt.indexOf('"INACTIVE"') > 0);
});
}

async assertIdToken() {
await this.idToken.then(el => el.getText()).then(txt => {
assert(txt.indexOf('claims') > 0);
Expand Down
Loading