Skip to content

Commit 28de573

Browse files
Merge pull request #821 from adrienne-deriv/setup-hydra-feature
Adrienne / Integrate Hydra authentication
2 parents 02c199b + 3b5332b commit 28de573

File tree

14 files changed

+407
-19
lines changed

14 files changed

+407
-19
lines changed

build/webpack/plugins.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ const getPlugins = (app, grunt) => ([
5252

5353
new webpack.DefinePlugin({
5454
'process.env': {
55-
BUILD_HASH: JSON.stringify(CryptoJS.MD5(Date.now().toString()).toString()),
56-
NODE_ENV : JSON.stringify('production'),
55+
BUILD_HASH : JSON.stringify(CryptoJS.MD5(Date.now().toString()).toString()),
56+
NODE_ENV : JSON.stringify('production'),
57+
GROWTHBOOK_CLIENT_KEY : JSON.stringify(process.env.GROWTHBOOK_CLIENT_KEY),
58+
RUDDERSTACK_KEY : JSON.stringify(process.env.RUDDERSTACK_KEY),
5759
},
5860
}),
5961
]
@@ -68,7 +70,9 @@ const getPlugins = (app, grunt) => ([
6870
]),
6971
new webpack.DefinePlugin({
7072
'process.env': {
71-
BUILD_HASH: JSON.stringify(CryptoJS.MD5(Date.now().toString()).toString()),
73+
BUILD_HASH : JSON.stringify(CryptoJS.MD5(Date.now().toString()).toString()),
74+
GROWTHBOOK_CLIENT_KEY : JSON.stringify(process.env.GROWTHBOOK_CLIENT_KEY),
75+
RUDDERSTACK_KEY : JSON.stringify(process.env.RUDDERSTACK_KEY),
7276
},
7377
}),
7478
]

package-lock.json

+178-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"load-grunt-tasks": "3.5.2",
8484
"mocha": "10.3.0",
8585
"mock-local-storage": "1.1.7",
86+
"mock-require": "^3.0.3",
8687
"node-gettext": "3.0.0",
8788
"postcss-scss": "4.0.9",
8889
"react-render-html": "0.6.0",
@@ -105,7 +106,10 @@
105106
"@binary-com/binary-document-uploader": "^2.4.4",
106107
"@binary-com/binary-style": "^0.2.26",
107108
"@binary-com/webtrader-charts": "^0.6.2",
109+
"@deriv-com/analytics": "^1.18.0",
110+
"@deriv-com/auth-client": "^1.0.15",
108111
"@deriv-com/quill-ui": "^1.16.2",
112+
"@deriv-com/utils": "^0.0.37",
109113
"@deriv/deriv-api": "^1.0.15",
110114
"@deriv/quill-icons": "^1.23.1",
111115
"@livechat/customer-sdk": "4.0.2",

src/javascript/_common/analytics.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const DerivAnalytics = require('@deriv-com/analytics');
2+
3+
const Analytics = (() => {
4+
const init = () => {
5+
if (process.env.RUDDERSTACK_KEY && process.env.GROWTHBOOK_CLIENT_KEY) {
6+
DerivAnalytics.Analytics.initialise({
7+
growthbookKey : process.env.GROWTHBOOK_CLIENT_KEY, // optional key to enable A/B tests
8+
rudderstackKey: process.env.RUDDERSTACK_KEY,
9+
});
10+
}
11+
};
12+
13+
const isGrowthbookLoaded = () => Boolean(DerivAnalytics.Analytics?.getInstances()?.ab);
14+
15+
const getGrowthbookFeatureValue = ({ defaultValue, featureFlag }) => {
16+
const resolvedDefaultValue = defaultValue !== undefined ? defaultValue : false;
17+
const isGBLoaded = isGrowthbookLoaded();
18+
19+
if (!isGBLoaded) return [null, false];
20+
21+
return [DerivAnalytics.Analytics?.getFeatureValue(featureFlag, resolvedDefaultValue), true];
22+
};
23+
24+
const setGrowthbookOnChange = onChange => {
25+
const isGBLoaded = isGrowthbookLoaded();
26+
if (!isGBLoaded) return null;
27+
28+
const onChangeRenderer = DerivAnalytics.Analytics?.getInstances().ab.GrowthBook?.setRenderer(() => {
29+
onChange();
30+
});
31+
return onChangeRenderer;
32+
};
33+
34+
return {
35+
init,
36+
isGrowthbookLoaded,
37+
getGrowthbookFeatureValue,
38+
setGrowthbookOnChange,
39+
};
40+
})();
41+
42+
module.exports = Analytics;

src/javascript/_common/auth.js

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
const {
2+
AppIDConstants,
3+
LocalStorageConstants,
4+
LocalStorageUtils,
5+
URLConstants,
6+
WebSocketUtils,
7+
} = require('@deriv-com/utils');
8+
const Analytics = require('./analytics');
9+
10+
export const DEFAULT_OAUTH_LOGOUT_URL = 'https://oauth.deriv.com/oauth2/sessions/logout';
11+
12+
export const DEFAULT_OAUTH_ORIGIN_URL = 'https://oauth.deriv.com';
13+
14+
const LOGOUT_HANDLER_TIMEOUT = 10000;
15+
16+
const SocketURL = {
17+
[URLConstants.derivP2pProduction]: 'blue.derivws.com',
18+
[URLConstants.derivP2pStaging] : 'red.derivws.com',
19+
};
20+
21+
export const getServerInfo = () => {
22+
const origin = window.location.origin;
23+
const hostname = window.location.hostname;
24+
25+
const existingAppId = LocalStorageUtils.getValue(LocalStorageConstants.configAppId);
26+
const existingServerUrl = LocalStorageUtils.getValue(LocalStorageConstants.configServerURL);
27+
// since we don't have official app_id for staging,
28+
// we will use the red server with app_id=62019 for the staging-p2p.deriv.com for now
29+
// to fix the login issue
30+
if (origin === URLConstants.derivP2pStaging && (!existingAppId || !existingServerUrl)) {
31+
LocalStorageUtils.setValue(LocalStorageConstants.configServerURL, SocketURL[origin]);
32+
LocalStorageUtils.setValue(LocalStorageConstants.configAppId, `${AppIDConstants.domainAppId[hostname]}`);
33+
}
34+
35+
const serverUrl = LocalStorageUtils.getValue(LocalStorageConstants.configServerURL) || localStorage.getItem('config.server_url') || 'oauth.deriv.com';
36+
37+
const defaultAppId = WebSocketUtils.getAppId();
38+
const appId = LocalStorageUtils.getValue(LocalStorageConstants.configAppId) || defaultAppId;
39+
const lang = LocalStorageUtils.getValue(LocalStorageConstants.i18nLanguage) || 'en';
40+
41+
return {
42+
appId,
43+
lang,
44+
serverUrl,
45+
};
46+
};
47+
48+
export const getOAuthLogoutUrl = () => {
49+
const { appId, serverUrl } = getServerInfo();
50+
51+
const oauthUrl = appId && serverUrl ? `https://${serverUrl}/oauth2/sessions/logout` : DEFAULT_OAUTH_LOGOUT_URL;
52+
53+
return oauthUrl;
54+
};
55+
56+
export const getOAuthOrigin = () => {
57+
const { appId, serverUrl } = getServerInfo();
58+
59+
const oauthUrl = appId && serverUrl ? `https://${serverUrl}` : DEFAULT_OAUTH_ORIGIN_URL;
60+
61+
return oauthUrl;
62+
};
63+
64+
export const isOAuth2Enabled = () => {
65+
const [OAuth2EnabledApps, OAuth2EnabledAppsInitialised] = Analytics.getGrowthbookFeatureValue({
66+
featureFlag: 'hydra_be',
67+
});
68+
const appId = WebSocketUtils.getAppId();
69+
70+
if (OAuth2EnabledAppsInitialised) {
71+
const FEHydraAppIds = OAuth2EnabledApps?.length
72+
? OAuth2EnabledApps[OAuth2EnabledApps.length - 1]?.enabled_for ?? []
73+
: [];
74+
return FEHydraAppIds.includes(+appId);
75+
}
76+
77+
return false;
78+
};
79+
80+
export const getLogoutHandler = onWSLogoutAndRedirect => {
81+
const isAuthEnabled = isOAuth2Enabled();
82+
83+
if (!isAuthEnabled) {
84+
return onWSLogoutAndRedirect;
85+
}
86+
87+
const onMessage = async event => {
88+
const allowedOrigin = getOAuthOrigin();
89+
if (allowedOrigin === event.origin) {
90+
if (event.data === 'logout_complete') {
91+
try {
92+
await onWSLogoutAndRedirect();
93+
} catch (err) {
94+
// eslint-disable-next-line no-console
95+
console.error(`logout was completed successfully on oauth hydra server, but logout handler returned error: ${err}`);
96+
}
97+
}
98+
}
99+
};
100+
101+
window.addEventListener('message', onMessage);
102+
103+
const oAuth2Logout = () => {
104+
if (!isAuthEnabled) {
105+
onWSLogoutAndRedirect();
106+
return;
107+
}
108+
109+
let iframe = document.getElementById('logout-iframe');
110+
if (!iframe) {
111+
iframe = document.createElement('iframe');
112+
iframe.id = 'logout-iframe';
113+
iframe.style.display = 'none';
114+
document.body.appendChild(iframe);
115+
116+
setTimeout(() => {
117+
onWSLogoutAndRedirect();
118+
}, LOGOUT_HANDLER_TIMEOUT);
119+
}
120+
121+
iframe.src = getOAuthLogoutUrl();
122+
};
123+
124+
return oAuth2Logout;
125+
};

src/javascript/_common/base/__tests__/client_base.js

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
2+
const Mock = require('mock-require');
3+
Mock('../../auth', {
4+
isOAuth2Enabled: function() {
5+
return false
6+
}
7+
});
18
const Client = require('../client_base');
29
const setCurrencies = require('../currency_base').setCurrencies;
310
const { api, expect, setURL } = require('../../__tests__/tests_common');

src/javascript/_common/base/client_base.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const moment = require('moment');
22
const isCryptocurrency = require('./currency_base').isCryptocurrency;
33
const SocketCache = require('./socket_cache');
4+
const AuthClient = require('../auth');
45
const localize = require('../localize').localize;
56
const LocalStore = require('../storage').LocalStore;
67
const State = require('../storage').State;
@@ -489,6 +490,9 @@ const ClientBase = (() => {
489490
};
490491

491492
const syncWithDerivApp = (active_loginid, client_accounts) => {
493+
// If the OAuth2 new authentication is enabled, all apps should not use localstorage-sync anymore
494+
const isOAuth2Enabled = AuthClient.isOAuth2Enabled();
495+
if (isOAuth2Enabled) return;
492496
const iframe_window = document.getElementById('localstorage-sync');
493497
const origin = getAllowedLocalStorageOrigin();
494498

src/javascript/app/base/header.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// const BinaryPjax = require('./binary_pjax');
22
const Client = require('./client');
33
const BinarySocket = require('./socket');
4+
const AuthClient = require('../../_common/auth');
45
const showHidePulser = require('../common/account_opening').showHidePulser;
56
const updateTotal = require('../pages/user/update_total');
67
const isAuthenticationAllowed = require('../../_common/base/client_base').isAuthenticationAllowed;
@@ -23,6 +24,7 @@ const template = require('../../_common/utility').template;
2324
const Language = require('../../_common/language');
2425
const mapCurrencyName = require('../../_common/base/currency_base').mapCurrencyName;
2526
const isEuCountry = require('../common/country_base').isEuCountry;
27+
const DerivIFrame = require('../pages/deriv_iframe.jsx');
2628

2729
const header_icon_base_path = '/images/pages/header/';
2830
const wallet_header_icon_base_path = '/images/pages/header/wallets/';
@@ -40,6 +42,7 @@ const Header = (() => {
4042
};
4143

4244
const onLoad = () => {
45+
DerivIFrame.init();
4346
populateAccountsList();
4447
populateWalletAccounts();
4548
bindSvg();
@@ -303,7 +306,6 @@ const Header = (() => {
303306
el.removeEventListener('click', logoutOnClick);
304307
el.addEventListener('click', logoutOnClick);
305308
});
306-
307309
// Mobile menu
308310
const mobile_menu_overlay = getElementById('mobile__container');
309311
const mobile_menu = getElementById('mobile__menu');
@@ -487,6 +489,7 @@ const Header = (() => {
487489
}
488490
};
489491

492+
// Some note here
490493
appstore_menu.addEventListener('click', () => {
491494
showMobileSubmenu(false);
492495
});
@@ -624,8 +627,13 @@ const Header = (() => {
624627
Login.redirectToLogin();
625628
};
626629

627-
const logoutOnClick = () => {
628-
Client.sendLogoutRequest();
630+
const logoutOnClick = async () => {
631+
// This will wrap the logout call Client.sendLogoutRequest with our own logout iframe, which is to inform Hydra that the user is logging out
632+
// and the session should be cleared on Hydra's side. Once this is done, it will call the passed-in logout handler Client.sendLogoutRequest.
633+
// If Hydra authentication is not enabled, the logout handler Client.sendLogoutRequest will just be called instead.
634+
const onLogoutWithOauth = await AuthClient.getLogoutHandler(Client.sendLogoutRequest);
635+
636+
onLogoutWithOauth();
629637
};
630638

631639
const populateWalletAccounts = () => {

src/javascript/app/base/logged_in.js

+2
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ const removeCookies = require('../../_common/storage').removeCookies;
1212
const paramsHash = require('../../_common/url').paramsHash;
1313
const urlFor = require('../../_common/url').urlFor;
1414
const getPropertyValue = require('../../_common/utility').getPropertyValue;
15+
const DerivIFrame = require('../pages/deriv_iframe.jsx');
1516

1617
const LoggedInHandler = (() => {
1718
const onLoad = () => {
1819
SocketCache.clear();
1920
parent.window.is_logging_in = 1; // this flag is used in base.js to prevent auto-reloading this page
2021
let redirect_url;
2122
const params = paramsHash(window.location.href);
23+
DerivIFrame.init();
2224
BinarySocket.send({ authorize: params.token1 }).then((response) => {
2325
const account_list = getPropertyValue(response, ['authorize', 'account_list']);
2426
if (isStorageSupported(localStorage) && isStorageSupported(sessionStorage) && account_list) {

src/javascript/app/base/page.js

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const State = require('../../_common/storage').State;
2121
const scrollToTop = require('../../_common/scroll').scrollToTop;
2222
const toISOFormat = require('../../_common/string_util').toISOFormat;
2323
const Url = require('../../_common/url');
24+
const Analytics = require('../../_common/analytics');
2425
const createElement = require('../../_common/utility').createElement;
2526
const isLoginPages = require('../../_common/utility').isLoginPages;
2627
const isProduction = require('../../config').isProduction;
@@ -35,6 +36,7 @@ const Page = (() => {
3536
Elevio.init();
3637
onDocumentReady();
3738
Crowdin.init();
39+
Analytics.init();
3840
};
3941

4042
const onDocumentReady = () => {
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import { isOAuth2Enabled } from '../../_common/auth';
4+
5+
const DerivIFrame = () => (
6+
<iframe
7+
id='localstorage-sync'
8+
style={{ display: 'none', visibility: 'hidden' }}
9+
sandbox='allow-same-origin allow-scripts'
10+
/>
11+
);
12+
13+
export const init = () => {
14+
const isAuthEnabled = isOAuth2Enabled();
15+
16+
if (!isAuthEnabled) ReactDOM.render(<DerivIFrame />, document.getElementById('deriv_iframe'));
17+
};
18+
19+
export default init;

src/javascript/app/pages/trade/tradepage.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const Defaults = require('./defaults');
77
const TradingEvents = require('./event');
88
const Price = require('./price');
99
const Process = require('./process');
10+
const AuthClient = require('../../../_common/auth');
1011
const ViewPopup = require('../user/view_popup/view_popup');
1112
const Client = require('../../base/client');
1213
const Header = require('../../base/header');
@@ -25,10 +26,11 @@ const TradePage = (() => {
2526
const onLoad = () => {
2627

2728
const iframe_target_origin = getAllowedLocalStorageOrigin();
29+
const isOauthEnabled = AuthClient.isOAuth2Enabled();
2830
BinarySocket.wait('authorize').then(() => {
29-
if (iframe_target_origin) {
31+
if (iframe_target_origin && !isOauthEnabled) {
3032
const el_iframe = document.getElementById('localstorage-sync');
31-
el_iframe.src = `${iframe_target_origin}/localstorage-sync.html`;
33+
if (el_iframe) el_iframe.src = `${iframe_target_origin}/localstorage-sync.html`;
3234
}
3335
init();
3436
});

src/templates/_common/_layout/layout.jsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Head from './head.jsx';
33
import Header from './header.jsx';
44
// import MobileMenu from './mobile_menu.jsx';
55
import WalletHeader from './wallet-header.jsx';
6-
import DerivIFrame from '../includes/deriv-iframe.jsx';
76
// import Elevio from '../includes/elevio.jsx';
87
import Gtm from '../includes/gtm.jsx';
98
import LiveChat from '../includes/livechat.jsx';
@@ -77,7 +76,7 @@ const Layout = () => {
7776
</div>
7877
<Topbar />
7978
</div>
80-
<DerivIFrame />
79+
<div id='deriv_iframe' />
8180
{/* <Elevio /> */}
8281
<LanguageMenuModal />
8382
</body>

0 commit comments

Comments
 (0)