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

Adrienne / Integrate Hydra authentication #821

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
753bb65
feat: added analytics to smarttrader
adrienne-deriv Sep 24, 2024
de69d69
feat: added auth-client library
adrienne-deriv Sep 24, 2024
be1724f
chore: generate test link
adrienne-deriv Sep 24, 2024
d28a2d8
chore: generate test link
adrienne-deriv Sep 24, 2024
893884d
chore: resolved eslint issues
adrienne-deriv Sep 24, 2024
5dee70e
chore: fixed issue with deriv analytics being part of mocha test
adrienne-deriv Sep 24, 2024
b05de9c
chore: skip tests to generate a test link temporarily
adrienne-deriv Sep 24, 2024
157fa60
chore: skip tests to generate a test link temporarily
adrienne-deriv Sep 24, 2024
fbe651f
chore: finally fixed test cases :)
adrienne-deriv Sep 25, 2024
690317d
chore: reverted prettier formatting
adrienne-deriv Sep 26, 2024
36bfba6
minor: added environment variables for analytics to github actions wo…
adrienne-deriv Sep 26, 2024
5a90f66
chore: used curly braces for secrets and envs in workflow
adrienne-deriv Sep 26, 2024
2601575
chore: used curly braces for secrets and envs in workflow
adrienne-deriv Sep 26, 2024
1105aa0
chore: resolved pr comments
adrienne-deriv Sep 30, 2024
10202f8
adrienne-deriv Oct 3, 2024
4e9ade8
Merge branch 'master' of github.com:deriv-com/smarttrader into setup-…
adrienne-deriv Oct 3, 2024
4286a60
chore: removed unrelated changes to workflow
adrienne-deriv Oct 3, 2024
bad20f6
chore: removed unrelated changes to workflow
adrienne-deriv Oct 3, 2024
b339eda
Merge branch 'setup-hydra-feature' of github.com:adrienne-deriv/smart…
adrienne-deriv Oct 3, 2024
b3df698
chore: add new line to regenerate test link
adrienne-deriv Oct 3, 2024
7c03bb1
chore: add new line to regenerate test link
adrienne-deriv Oct 3, 2024
0deec9e
chore: removed decryption key
adrienne-deriv Oct 3, 2024
4a93343
chore: removed decryption key
adrienne-deriv Oct 3, 2024
03d6000
Merge branch 'setup-hydra-feature' of github.com:adrienne-deriv/smart…
adrienne-deriv Oct 3, 2024
3b5332b
chore: update package-lock and default app id fallback
adrienne-deriv Oct 16, 2024
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
10 changes: 7 additions & 3 deletions build/webpack/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ const getPlugins = (app, grunt) => ([

new webpack.DefinePlugin({
'process.env': {
BUILD_HASH: JSON.stringify(CryptoJS.MD5(Date.now().toString()).toString()),
NODE_ENV : JSON.stringify('production'),
BUILD_HASH : JSON.stringify(CryptoJS.MD5(Date.now().toString()).toString()),
NODE_ENV : JSON.stringify('production'),
GROWTHBOOK_CLIENT_KEY : JSON.stringify(process.env.GROWTHBOOK_CLIENT_KEY),
RUDDERSTACK_KEY : JSON.stringify(process.env.RUDDERSTACK_KEY),
},
}),
]
Expand All @@ -68,7 +70,9 @@ const getPlugins = (app, grunt) => ([
]),
new webpack.DefinePlugin({
'process.env': {
BUILD_HASH: JSON.stringify(CryptoJS.MD5(Date.now().toString()).toString()),
BUILD_HASH : JSON.stringify(CryptoJS.MD5(Date.now().toString()).toString()),
GROWTHBOOK_CLIENT_KEY : JSON.stringify(process.env.GROWTHBOOK_CLIENT_KEY),
RUDDERSTACK_KEY : JSON.stringify(process.env.RUDDERSTACK_KEY),
},
}),
]
Expand Down
185 changes: 178 additions & 7 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"load-grunt-tasks": "3.5.2",
"mocha": "10.3.0",
"mock-local-storage": "1.1.7",
"mock-require": "^3.0.3",
"node-gettext": "3.0.0",
"postcss-scss": "4.0.9",
"react-render-html": "0.6.0",
Expand All @@ -105,7 +106,10 @@
"@binary-com/binary-document-uploader": "^2.4.4",
"@binary-com/binary-style": "^0.2.26",
"@binary-com/webtrader-charts": "^0.6.2",
"@deriv-com/analytics": "^1.18.0",
"@deriv-com/auth-client": "^1.0.15",
"@deriv-com/quill-ui": "^1.16.2",
"@deriv-com/utils": "^0.0.37",
"@deriv/deriv-api": "^1.0.15",
"@deriv/quill-icons": "^1.23.1",
"@livechat/customer-sdk": "4.0.2",
Expand Down
42 changes: 42 additions & 0 deletions src/javascript/_common/analytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const DerivAnalytics = require('@deriv-com/analytics');

const Analytics = (() => {
const init = () => {
if (process.env.RUDDERSTACK_KEY && process.env.GROWTHBOOK_CLIENT_KEY) {
DerivAnalytics.Analytics.initialise({
growthbookKey : process.env.GROWTHBOOK_CLIENT_KEY, // optional key to enable A/B tests
rudderstackKey: process.env.RUDDERSTACK_KEY,
});
}
};

const isGrowthbookLoaded = () => Boolean(DerivAnalytics.Analytics?.getInstances()?.ab);

const getGrowthbookFeatureValue = ({ defaultValue, featureFlag }) => {
const resolvedDefaultValue = defaultValue !== undefined ? defaultValue : false;
const isGBLoaded = isGrowthbookLoaded();

if (!isGBLoaded) return [null, false];

return [DerivAnalytics.Analytics?.getFeatureValue(featureFlag, resolvedDefaultValue), true];
};

const setGrowthbookOnChange = onChange => {
const isGBLoaded = isGrowthbookLoaded();
if (!isGBLoaded) return null;

const onChangeRenderer = DerivAnalytics.Analytics?.getInstances().ab.GrowthBook?.setRenderer(() => {
onChange();
});
return onChangeRenderer;
};

return {
init,
isGrowthbookLoaded,
getGrowthbookFeatureValue,
setGrowthbookOnChange,
};
})();

module.exports = Analytics;
125 changes: 125 additions & 0 deletions src/javascript/_common/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const {
AppIDConstants,
LocalStorageConstants,
LocalStorageUtils,
URLConstants,
WebSocketUtils,
} = require('@deriv-com/utils');
const Analytics = require('./analytics');

export const DEFAULT_OAUTH_LOGOUT_URL = 'https://oauth.deriv.com/oauth2/sessions/logout';

export const DEFAULT_OAUTH_ORIGIN_URL = 'https://oauth.deriv.com';

const LOGOUT_HANDLER_TIMEOUT = 10000;

const SocketURL = {
[URLConstants.derivP2pProduction]: 'blue.derivws.com',
[URLConstants.derivP2pStaging] : 'red.derivws.com',
};

export const getServerInfo = () => {
const origin = window.location.origin;
const hostname = window.location.hostname;

const existingAppId = LocalStorageUtils.getValue(LocalStorageConstants.configAppId);
const existingServerUrl = LocalStorageUtils.getValue(LocalStorageConstants.configServerURL);
// since we don't have official app_id for staging,
// we will use the red server with app_id=62019 for the staging-p2p.deriv.com for now
// to fix the login issue
if (origin === URLConstants.derivP2pStaging && (!existingAppId || !existingServerUrl)) {
LocalStorageUtils.setValue(LocalStorageConstants.configServerURL, SocketURL[origin]);
LocalStorageUtils.setValue(LocalStorageConstants.configAppId, `${AppIDConstants.domainAppId[hostname]}`);
}

const serverUrl = LocalStorageUtils.getValue(LocalStorageConstants.configServerURL) || localStorage.getItem('config.server_url') || 'oauth.deriv.com';

const defaultAppId = WebSocketUtils.getAppId();
const appId = LocalStorageUtils.getValue(LocalStorageConstants.configAppId) || defaultAppId;
const lang = LocalStorageUtils.getValue(LocalStorageConstants.i18nLanguage) || 'en';

return {
appId,
lang,
serverUrl,
};
};

export const getOAuthLogoutUrl = () => {
const { appId, serverUrl } = getServerInfo();

const oauthUrl = appId && serverUrl ? `https://${serverUrl}/oauth2/sessions/logout` : DEFAULT_OAUTH_LOGOUT_URL;

return oauthUrl;
};

export const getOAuthOrigin = () => {
const { appId, serverUrl } = getServerInfo();

const oauthUrl = appId && serverUrl ? `https://${serverUrl}` : DEFAULT_OAUTH_ORIGIN_URL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Q] whats appId check used here for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its just an extra check to ensure we have the app ID and server URL before returning the origin url


return oauthUrl;
};

export const isOAuth2Enabled = () => {
const [OAuth2EnabledApps, OAuth2EnabledAppsInitialised] = Analytics.getGrowthbookFeatureValue({
featureFlag: 'hydra_be',
});
const appId = WebSocketUtils.getAppId();

if (OAuth2EnabledAppsInitialised) {
const FEHydraAppIds = OAuth2EnabledApps?.length
? OAuth2EnabledApps[OAuth2EnabledApps.length - 1]?.enabled_for ?? []
: [];
return FEHydraAppIds.includes(+appId);
}

return false;
};

export const getLogoutHandler = onWSLogoutAndRedirect => {
const isAuthEnabled = isOAuth2Enabled();

if (!isAuthEnabled) {
return onWSLogoutAndRedirect;
}

const onMessage = async event => {
const allowedOrigin = getOAuthOrigin();
if (allowedOrigin === event.origin) {
if (event.data === 'logout_complete') {
try {
await onWSLogoutAndRedirect();
} catch (err) {
// eslint-disable-next-line no-console
console.error(`logout was completed successfully on oauth hydra server, but logout handler returned error: ${err}`);
}
}
}
};

window.addEventListener('message', onMessage);

const oAuth2Logout = () => {
if (!isAuthEnabled) {
onWSLogoutAndRedirect();
return;
}

let iframe = document.getElementById('logout-iframe');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'logout-iframe';
iframe.style.display = 'none';
document.body.appendChild(iframe);

setTimeout(() => {
onWSLogoutAndRedirect();
}, LOGOUT_HANDLER_TIMEOUT);
}

iframe.src = getOAuthLogoutUrl();
};

return oAuth2Logout;
};
7 changes: 7 additions & 0 deletions src/javascript/_common/base/__tests__/client_base.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@

const Mock = require('mock-require');
Mock('../../auth', {
isOAuth2Enabled: function() {
return false
}
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to resolve test case issues where @deriv-com/analytics is using Growthbook module which uses MutationObserver, in which Mocha doesn't recognise and is unable to mock it
Screenshot 2024-09-26 at 11 53 04 AM

const Client = require('../client_base');
const setCurrencies = require('../currency_base').setCurrencies;
const { api, expect, setURL } = require('../../__tests__/tests_common');
Expand Down
4 changes: 4 additions & 0 deletions src/javascript/_common/base/client_base.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const moment = require('moment');
const isCryptocurrency = require('./currency_base').isCryptocurrency;
const SocketCache = require('./socket_cache');
const AuthClient = require('../auth');
const localize = require('../localize').localize;
const LocalStore = require('../storage').LocalStore;
const State = require('../storage').State;
Expand Down Expand Up @@ -489,6 +490,9 @@ const ClientBase = (() => {
};

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

Expand Down
14 changes: 11 additions & 3 deletions src/javascript/app/base/header.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// const BinaryPjax = require('./binary_pjax');
const Client = require('./client');
const BinarySocket = require('./socket');
const AuthClient = require('../../_common/auth');
const showHidePulser = require('../common/account_opening').showHidePulser;
const updateTotal = require('../pages/user/update_total');
const isAuthenticationAllowed = require('../../_common/base/client_base').isAuthenticationAllowed;
Expand All @@ -23,6 +24,7 @@ const template = require('../../_common/utility').template;
const Language = require('../../_common/language');
const mapCurrencyName = require('../../_common/base/currency_base').mapCurrencyName;
const isEuCountry = require('../common/country_base').isEuCountry;
const DerivIFrame = require('../pages/deriv_iframe.jsx');

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

const onLoad = () => {
DerivIFrame.init();
populateAccountsList();
populateWalletAccounts();
bindSvg();
Expand Down Expand Up @@ -303,7 +306,6 @@ const Header = (() => {
el.removeEventListener('click', logoutOnClick);
el.addEventListener('click', logoutOnClick);
});

// Mobile menu
const mobile_menu_overlay = getElementById('mobile__container');
const mobile_menu = getElementById('mobile__menu');
Expand Down Expand Up @@ -487,6 +489,7 @@ const Header = (() => {
}
};

// Some note here
appstore_menu.addEventListener('click', () => {
showMobileSubmenu(false);
});
Expand Down Expand Up @@ -624,8 +627,13 @@ const Header = (() => {
Login.redirectToLogin();
};

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

adrienne-deriv marked this conversation as resolved.
Show resolved Hide resolved
onLogoutWithOauth();
};

const populateWalletAccounts = () => {
Expand Down
2 changes: 2 additions & 0 deletions src/javascript/app/base/logged_in.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ const removeCookies = require('../../_common/storage').removeCookies;
const paramsHash = require('../../_common/url').paramsHash;
const urlFor = require('../../_common/url').urlFor;
const getPropertyValue = require('../../_common/utility').getPropertyValue;
const DerivIFrame = require('../pages/deriv_iframe.jsx');

const LoggedInHandler = (() => {
const onLoad = () => {
SocketCache.clear();
parent.window.is_logging_in = 1; // this flag is used in base.js to prevent auto-reloading this page
let redirect_url;
const params = paramsHash(window.location.href);
DerivIFrame.init();
BinarySocket.send({ authorize: params.token1 }).then((response) => {
const account_list = getPropertyValue(response, ['authorize', 'account_list']);
if (isStorageSupported(localStorage) && isStorageSupported(sessionStorage) && account_list) {
Expand Down
2 changes: 2 additions & 0 deletions src/javascript/app/base/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const State = require('../../_common/storage').State;
const scrollToTop = require('../../_common/scroll').scrollToTop;
const toISOFormat = require('../../_common/string_util').toISOFormat;
const Url = require('../../_common/url');
const Analytics = require('../../_common/analytics');
const createElement = require('../../_common/utility').createElement;
const isLoginPages = require('../../_common/utility').isLoginPages;
const isProduction = require('../../config').isProduction;
Expand All @@ -35,6 +36,7 @@ const Page = (() => {
Elevio.init();
onDocumentReady();
Crowdin.init();
Analytics.init();
};

const onDocumentReady = () => {
Expand Down
19 changes: 19 additions & 0 deletions src/javascript/app/pages/deriv_iframe.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { isOAuth2Enabled } from '../../_common/auth';

const DerivIFrame = () => (
<iframe
id='localstorage-sync'
style={{ display: 'none', visibility: 'hidden' }}
sandbox='allow-same-origin allow-scripts'
/>
);

export const init = () => {
const isAuthEnabled = isOAuth2Enabled();

if (!isAuthEnabled) ReactDOM.render(<DerivIFrame />, document.getElementById('deriv_iframe'));
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the DerivIframe component to the javascript folder so that it can be conditionally rendered to be included/excluded based on the feature flag returned by isOauth2Enabled.
If the Hydra authentication is enabled, we will not be using the Deriv iframe to sync login states anymore

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we want to disable syncing in certain scenarios?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only disable it when the Hydra authentication feature flag is enabled, which we turn on/off from Growthbook

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because one of our aim for this feature is to remove the use of Deriv iframe sync entirely and migrate these syncing logic into the Hydra service, which is on BE's side


export default init;
6 changes: 4 additions & 2 deletions src/javascript/app/pages/trade/tradepage.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const Defaults = require('./defaults');
const TradingEvents = require('./event');
const Price = require('./price');
const Process = require('./process');
const AuthClient = require('../../../_common/auth');
const ViewPopup = require('../user/view_popup/view_popup');
const Client = require('../../base/client');
const Header = require('../../base/header');
Expand All @@ -25,10 +26,11 @@ const TradePage = (() => {
const onLoad = () => {

const iframe_target_origin = getAllowedLocalStorageOrigin();
const isOauthEnabled = AuthClient.isOAuth2Enabled();
BinarySocket.wait('authorize').then(() => {
if (iframe_target_origin) {
if (iframe_target_origin && !isOauthEnabled) {
const el_iframe = document.getElementById('localstorage-sync');
el_iframe.src = `${iframe_target_origin}/localstorage-sync.html`;
if (el_iframe) el_iframe.src = `${iframe_target_origin}/localstorage-sync.html`;
}
init();
});
Expand Down
3 changes: 1 addition & 2 deletions src/templates/_common/_layout/layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Head from './head.jsx';
import Header from './header.jsx';
// import MobileMenu from './mobile_menu.jsx';
import WalletHeader from './wallet-header.jsx';
import DerivIFrame from '../includes/deriv-iframe.jsx';
// import Elevio from '../includes/elevio.jsx';
import Gtm from '../includes/gtm.jsx';
import LiveChat from '../includes/livechat.jsx';
Expand Down Expand Up @@ -77,7 +76,7 @@ const Layout = () => {
</div>
<Topbar />
</div>
<DerivIFrame />
<div id='deriv_iframe' />
{/* <Elevio /> */}
<LanguageMenuModal />
</body>
Expand Down
Loading
Loading