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

feat: oidc implementation #187

Merged
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
30 changes: 17 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"@deriv-com/quill-ui": "^1.16.21",
"@deriv-com/analytics": "^1.22.1",
"@deriv-com/auth-client": "^1.0.29",
"@deriv-com/auth-client": "^1.3.1",
"@deriv/deriv-api": "^1.0.11",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-spring/web": "^9.7.3",
Expand Down
37 changes: 35 additions & 2 deletions src/components/UserNavbarItem/item.desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import useDeviceType from '@site/src/hooks/useDeviceType';

import { IUserNavbarItemProps } from './item.types';
import styles from './UserNavbarItem.module.scss';
import Cookies from 'js-cookie';
import { useHandleLogin } from '@site/src/hooks/useHandleLogin';

interface IActionProps {
handleClick: () => void;
Expand Down Expand Up @@ -62,12 +64,16 @@ const DashboardActions: React.FC<IActionProps> = ({ handleClick, isDesktop }) =>
const SignedInActions: React.FC<IActionProps> = ({ handleClick, isDesktop }) => {
const signedInButtonClasses = clsx('navbar__item', styles.UserNavbarItem, styles.SignedInButton);

const { handleLogin } = useHandleLogin({
onClickLogin: handleClick,
});

return (
<nav className='right-navigation'>
<Button
variant='secondary'
color='black'
onClick={handleClick}
onClick={handleLogin}
className={signedInButtonClasses}
data-testid='sa_login'
>
Expand All @@ -88,14 +94,41 @@ const SignedInActions: React.FC<IActionProps> = ({ handleClick, isDesktop }) =>
};

const UserNavbarDesktopItem = ({ authUrl, is_logged_in }: IUserNavbarItemProps) => {
const { logout } = useLogout();
const { deviceType } = useDeviceType();
const isDesktop = deviceType === 'desktop';

const handleClick = () => {
location.assign(authUrl);
};

const { handleLogin, isOAuth2Enabled } = useHandleLogin({
onClickLogin: handleClick,
});

const { logout } = useLogout();

const loggedState = Cookies.get('logged_state');

const loginAccountsSessionStorage = JSON.parse(sessionStorage.getItem('login-accounts'));

const isLoginAccountsPopulated =
loginAccountsSessionStorage && loginAccountsSessionStorage.length > 0;

React.useEffect(() => {
if (
loggedState === 'true' &&
isOAuth2Enabled &&
!isLoginAccountsPopulated &&
!window.location.pathname.includes('callback') &&
!window.location.pathname.includes('endpoint')
) {
handleLogin();
}
if (loggedState === 'false' && isOAuth2Enabled && isLoginAccountsPopulated) {
logout();
}
}, [isOAuth2Enabled, loggedState, logout, handleLogin, isLoginAccountsPopulated]);

return is_logged_in ? (
<DashboardActions handleClick={logout} isDesktop={isDesktop} />
) : (
Expand Down
7 changes: 6 additions & 1 deletion src/features/Apiexplorer/LoginDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import useLoginUrl from '@site/src/hooks/useLoginUrl';
import styles from './LoginDialog.module.scss';
import Translate, { translate } from '@docusaurus/Translate';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { useHandleLogin } from '@site/src/hooks/useHandleLogin';

type TLoginDialog = {
setToggleModal: React.Dispatch<React.SetStateAction<boolean>>;
Expand All @@ -25,6 +26,10 @@ export const LoginDialog = ({ setToggleModal }: TLoginDialog) => {
location.assign(getUrl(currentLocale));
};

const { handleLogin } = useHandleLogin({
onClickLogin: handleClick,
});

const handleSignUp = () => {
location.assign('https://deriv.com/signup/');
};
Expand Down Expand Up @@ -57,7 +62,7 @@ export const LoginDialog = ({ setToggleModal }: TLoginDialog) => {
<Button color='tertiary' onClick={handleSignUp} className={styles.btn}>
<Translate>Sign up</Translate>
</Button>
<Button color='primary' onClick={handleClick} className={styles.btn}>
<Button color='primary' onClick={handleLogin} className={styles.btn}>
<Translate>Log in</Translate>
</Button>
</div>
Expand Down
17 changes: 17 additions & 0 deletions src/features/Callback/CallbackPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { Callback } from '@deriv-com/auth-client';
import { transformAccountsFromResponseBody } from '@site/src/utils';
import useAuthContext from '@site/src/hooks/useAuthContext';
const CallbackPage = () => {
const { updateLoginAccounts } = useAuthContext();
return (
<Callback
onSignInSuccess={(tokens) => {
const accounts = transformAccountsFromResponseBody(tokens);
updateLoginAccounts(accounts);
window.location.href = '/';
}}
/>
);
};
export default CallbackPage;
2 changes: 2 additions & 0 deletions src/features/Callback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import CallbackPage from './CallbackPage';
export default CallbackPage;
8 changes: 7 additions & 1 deletion src/features/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useLoginUrl from '@site/src/hooks/useLoginUrl';
import Footer from '@site/src/components/Footer';
import Translate from '@docusaurus/Translate';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { useHandleLogin } from '@site/src/hooks/useHandleLogin';

export const Login = () => {
const { getUrl } = useLoginUrl();
Expand All @@ -15,6 +16,11 @@ export const Login = () => {
const handleClick = () => {
window.location.assign(getUrl(currentLocale));
};

const { handleLogin } = useHandleLogin({
onClickLogin: handleClick,
});

return (
<div>
<div className={styles.login} data-testid='login'>
Expand All @@ -26,7 +32,7 @@ export const Login = () => {
</Translate>
</Text>
<div className={styles.action}>
<Button color='primary' onClick={handleClick}>
<Button color='primary' onClick={handleLogin}>
<Translate>Log In</Translate>
</Button>
</div>
Expand Down
5 changes: 5 additions & 0 deletions src/features/dashboard/__tests__/dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ mockReactTable.mockImplementation(() => ({
headerGroups: [],
}));

jest.mock('@docusaurus/BrowserOnly', () => ({
__esModule: true,
default: ({ children }: { children: () => JSX.Element }) => children(),
}));

describe('AppManager', () => {
it('shows the login screen', () => {
mockUseAuthContext.mockImplementation(() => ({
Expand Down
3 changes: 2 additions & 1 deletion src/features/dashboard/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import useAuthContext from '@site/src/hooks/useAuthContext';
import useAppManager from '@site/src/hooks/useAppManager';
import ManageDashboard from './manage-dashboard';
import { Login } from '../Login/Login';
import BrowserOnly from '@docusaurus/BrowserOnly';

const Dashboard = () => {
const { is_logged_in } = useAuthContext();
Expand All @@ -16,7 +17,7 @@ const Dashboard = () => {
}, [setIsDashboard]);

if (is_logged_in) return <ManageDashboard />;
return <Login />;
return <BrowserOnly>{() => <Login />}</BrowserOnly>;
};

export default Dashboard;
35 changes: 35 additions & 0 deletions src/hooks/useHandleLogin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import useGrowthbookGetFeatureValue from '../useGrowthbookGetFeatureValue/';
import {
requestOidcAuthentication,
TOAuth2EnabledAppList,
useIsOAuth2Enabled,
} from '@deriv-com/auth-client';
/**
* Handles the new login flow for the user using OIDC.
*
* If the user is not logged in and OAuth2 is enabled, it will redirect the user to the
* OAuth2 authorization page from the OIDC config endpoint. If OAuth2 is not enabled it will
* redirect the user to the legacy oauth url coming from the onClickLogin callback.
*
* @param {Object} props - The props object.
* @param {Function} props.onClickLogin - The callback to be called when the user is logged in.
* @returns {Object} - An object with the `handleLogin` function.
*/
export const useHandleLogin = ({ onClickLogin }: { onClickLogin?: () => void }) => {
const [OAuth2EnabledApps, OAuth2EnabledAppsInitialised] =
useGrowthbookGetFeatureValue<TOAuth2EnabledAppList>({
featureFlag: 'hydra_be',
});
const isOAuth2Enabled = useIsOAuth2Enabled(OAuth2EnabledApps, OAuth2EnabledAppsInitialised);
const handleLogin = async () => {
if (isOAuth2Enabled) {
await requestOidcAuthentication({
redirectCallbackUri: `${window.location.origin}/callback`,
});
}
if (onClickLogin) {
onClickLogin();
}
};
return { handleLogin, isOAuth2Enabled };
};
13 changes: 13 additions & 0 deletions src/hooks/useLogout/__tests__/useLogout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ mockUseAuthContext.mockImplementation(() => ({
updateCurrentLoginAccount: mockUpdateCurrentLoginAccount,
}));

jest.mock('@deriv-com/auth-client', () => ({
OAuth2Logout: jest.fn((WSLogoutAndRedirect) => {
const mockIframe = document.createElement('iframe');
mockIframe.id = 'logout-iframe';
document.body.appendChild(mockIframe);
setTimeout(() => {
const event = new MessageEvent('message', { data: 'logout_complete' });
window.dispatchEvent(event);
}, 100);
WSLogoutAndRedirect();
}),
}));

const logout_response = {
logout: 1,
req_id: 1,
Expand Down
13 changes: 5 additions & 8 deletions src/hooks/useLogout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import apiManager from '@site/src/configs/websocket';
import { useCallback } from 'react';
import useAuthContext from '../useAuthContext';
import useGrowthbookGetFeatureValue from '../useGrowthbookGetFeatureValue';
import { useOAuth2, TOAuth2EnabledAppList } from '@deriv-com/auth-client';
import { OAuth2Logout } from '@deriv-com/auth-client';

const useLogout = () => {
const { updateLoginAccounts, updateCurrentLoginAccount } = useAuthContext();
const [OAuth2EnabledApps, OAuth2EnabledAppsInitialised] =
useGrowthbookGetFeatureValue<TOAuth2EnabledAppList>({
featureFlag: 'hydra_be',
});

// we clean up everything related to the user here, for now it's just user's account
// later on we should clear user tokens as well
Expand All @@ -23,9 +18,11 @@ const useLogout = () => {
});
}, [updateCurrentLoginAccount, updateLoginAccounts]);

const { OAuth2Logout } = useOAuth2({ OAuth2EnabledApps, OAuth2EnabledAppsInitialised }, logout);
const handleLogout = () => {
OAuth2Logout(logout);
};

return { logout: OAuth2Logout };
return { logout: handleLogout };
};

export default useLogout;
5 changes: 3 additions & 2 deletions src/pages/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useAuthParams from '../hooks/useAuthParams';
import { useEffect } from 'react';
import { Redirect, useLocation } from '@docusaurus/router';
import useAuthContext from '../hooks/useAuthContext';
import BrowserOnly from '@docusaurus/BrowserOnly';

export default function Auth(): JSX.Element {
const { search } = useLocation(); // to get the search params
Expand All @@ -19,7 +20,7 @@ export default function Auth(): JSX.Element {
useEffect(() => {
if (is_logged_in) {
const params = new URLSearchParams(search);
const redirect_route = params.get('route')?.replace(/%2F/g, '/') || '/';
const redirect_route = params.get('route')?.replace(/%2F/g, '/') || '/';
setRedirectRoute(redirect_route);
}
}, [is_logged_in, search]);
Expand All @@ -31,7 +32,7 @@ export default function Auth(): JSX.Element {
return (
<Layout title='Auth' description='Deriv API documentation'>
<main>
<Login />
<BrowserOnly>{() => <Login />}</BrowserOnly>
</main>
</Layout>
);
Expand Down
26 changes: 26 additions & 0 deletions src/pages/callback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { useEffect } from 'react';
import Layout from '@theme/Layout';
import CallbackPage from '../features/Callback';
const Callback = () => {
useEffect(() => {
const navbar = document.getElementsByClassName('navbar navbar--fixed-top')[0] as HTMLElement;
if (navbar) {
navbar.style.display = 'none';
}
const metaTag = document.createElement('meta');
metaTag.name = 'robots';
metaTag.content = 'noindex, nofollow';
document.head.appendChild(metaTag);
return () => {
document.head.removeChild(metaTag);
};
}, []);
return (
<Layout title='Callback' description=''>
<main>
<CallbackPage />
</main>
</Layout>
);
};
export default Callback;
Loading
Loading