Skip to content

Commit

Permalink
Update logout/login redirect for different users
Browse files Browse the repository at this point in the history
* Logout as User A and Login as User B redirects to `/home'
* Logout as User A and Login as User A redirects to `/home'
* Allow session to timeout as User A and Login as User A redirects to User A's last location

See: #11167
  • Loading branch information
nixocio committed Jun 2, 2022
1 parent b548ad2 commit 21813cf
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 14 deletions.
4 changes: 2 additions & 2 deletions awx/ui/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ const AuthorizedRoutes = ({ routeConfig }) => {
export function ProtectedRoute({ children, ...rest }) {
const {
authRedirectTo,
setAuthRedirectTo,
loginRedirectOverride,
isUserBeingLoggedOut,
loginRedirectOverride,
setAuthRedirectTo,
} = useSession();
const location = useLocation();

Expand Down
1 change: 1 addition & 0 deletions awx/ui/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export const JOB_TYPE_URL_SEGMENTS = {

export const SESSION_TIMEOUT_KEY = 'awx-session-timeout';
export const SESSION_REDIRECT_URL = 'awx-redirect-url';
export const SESSION_USER_ID = 'awx-session-user-id';
11 changes: 6 additions & 5 deletions awx/ui/src/contexts/Session.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { DateTime } from 'luxon';
import { RootAPI, MeAPI } from 'api';
import { isAuthenticated } from 'util/auth';
import useRequest from 'hooks/useRequest';
import { SESSION_TIMEOUT_KEY } from '../constants';
import { SESSION_TIMEOUT_KEY, SESSION_USER_ID } from '../constants';

// The maximum supported timeout for setTimeout(), in milliseconds,
// is the highest number you can represent as a signed 32bit
Expand Down Expand Up @@ -101,6 +101,7 @@ function SessionProvider({ children }) {
setIsUserBeingLoggedOut(true);
if (!isSessionExpired.current) {
setAuthRedirectTo('/logout');
window.sessionStorage.setItem(SESSION_USER_ID, null);
}
await RootAPI.logout();
setSessionTimeout(0);
Expand Down Expand Up @@ -166,21 +167,21 @@ function SessionProvider({ children }) {

const sessionValue = useMemo(
() => ({
isUserBeingLoggedOut,
loginRedirectOverride,
authRedirectTo,
handleSessionContinue,
isSessionExpired,
isUserBeingLoggedOut,
loginRedirectOverride,
logout,
sessionCountdown,
setAuthRedirectTo,
}),
[
isUserBeingLoggedOut,
loginRedirectOverride,
authRedirectTo,
handleSessionContinue,
isSessionExpired,
isUserBeingLoggedOut,
loginRedirectOverride,
logout,
sessionCountdown,
setAuthRedirectTo,
Expand Down
32 changes: 28 additions & 4 deletions awx/ui/src/screens/Login/Login.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Redirect, withRouter } from 'react-router-dom';

import { t } from '@lingui/macro';
Expand Down Expand Up @@ -31,7 +31,8 @@ import { AuthAPI, RootAPI } from 'api';
import AlertModal from 'components/AlertModal';
import ErrorDetail from 'components/ErrorDetail';
import { useSession } from 'contexts/Session';
import { SESSION_REDIRECT_URL } from '../../constants';
import { getCurrentUserId } from 'util/auth';
import { SESSION_REDIRECT_URL, SESSION_USER_ID } from '../../constants';

const loginLogoSrc = 'static/media/logo-login.svg';

Expand All @@ -43,6 +44,8 @@ const Login = styled(PFLogin)`

function AWXLogin({ alt, isAuthenticated }) {
const { authRedirectTo, isSessionExpired, setAuthRedirectTo } = useSession();
const isNewUser = useRef(true);
const hasVerifiedUser = useRef(false);

const {
isLoading: isCustomLoginInfoLoading,
Expand Down Expand Up @@ -112,8 +115,29 @@ function AWXLogin({ alt, isAuthenticated }) {
if (isCustomLoginInfoLoading) {
return null;
}
if (isAuthenticated(document.cookie)) {
return <Redirect to={authRedirectTo || '/'} />;

if (isAuthenticated(document.cookie) && !hasVerifiedUser.current) {
const currentUserId = getCurrentUserId(document.cookie);
const verifyIsNewUser = () => {
const previousUserId = JSON.parse(
window.sessionStorage.getItem(SESSION_USER_ID)
);
if (previousUserId === null) {
return true;
}
return currentUserId.toString() !== previousUserId.toString();
};
isNewUser.current = verifyIsNewUser();
hasVerifiedUser.current = true;
window.sessionStorage.setItem(
SESSION_USER_ID,
JSON.stringify(currentUserId)
);
}

if (isAuthenticated(document.cookie) && hasVerifiedUser.current) {
const redirect = isNewUser.current ? '/' : authRedirectTo;
return <Redirect to={redirect} />;
}

let helperText;
Expand Down
81 changes: 79 additions & 2 deletions awx/ui/src/screens/Login/Login.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils';
import { AuthAPI, RootAPI } from 'api';
import {
Expand All @@ -7,9 +8,16 @@ import {
} from '../../../testUtils/enzymeHelpers';

import AWXLogin from './Login';
import { getCurrentUserId } from 'util/auth';

import { SESSION_USER_ID } from '../../constants';

jest.mock('../../api');

jest.mock('util/auth', () => ({
getCurrentUserId: jest.fn(),
}));

RootAPI.readAssetVariables.mockResolvedValue({
data: {
BRAND_NAME: 'AWX',
Expand Down Expand Up @@ -72,6 +80,13 @@ describe('<Login />', () => {
custom_logo: 'images/foo.jpg',
},
});
Object.defineProperty(window, 'sessionStorage', {
value: {
getItem: jest.fn(() => '42'),
setItem: jest.fn(() => null),
},
writable: true,
});
});

afterEach(() => {
Expand Down Expand Up @@ -276,15 +291,77 @@ describe('<Login />', () => {
expect(RootAPI.login).toHaveBeenCalledWith('un', 'pw');
});

test('render Redirect to / when already authenticated', async () => {
test('render Redirect to / when already authenticated as a new user', async () => {
getCurrentUserId.mockReturnValue('1');
const history = createMemoryHistory({
initialEntries: ['/login'],
});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => true} />);
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => true} />, {
context: {
router: { history },
session: {
authRedirectTo: '/projects',
handleSessionContinue: () => {},
isSessionExpired: false,
isUserBeingLoggedOut: false,
loginRedirectOverride: null,
logout: () => {},
sessionCountdown: 60,
setAuthRedirectTo: () => {},
},
},
});
});
expect(window.sessionStorage.getItem).toHaveBeenCalledWith(SESSION_USER_ID);
expect(window.sessionStorage.setItem).toHaveBeenCalledWith(
SESSION_USER_ID,
'"1"'
);
await waitForElement(wrapper, 'Redirect', (el) => el.length === 1);
await waitForElement(wrapper, 'Redirect', (el) => el.props().to === '/');
});

test('render redirect to authRedirectTo when authenticated as a previous user', async () => {
getCurrentUserId.mockReturnValue('42');
const history = createMemoryHistory({
initialEntries: ['/login'],
});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => true} />, {
context: {
router: { history },
session: {
authRedirectTo: '/projects',
handleSessionContinue: () => {},
isSessionExpired: false,
isUserBeingLoggedOut: false,
loginRedirectOverride: null,
logout: () => {},
sessionCountdown: 60,
setAuthRedirectTo: () => {},
},
},
});
});

wrapper.update();
expect(window.sessionStorage.getItem).toHaveBeenCalledWith(SESSION_USER_ID);
expect(window.sessionStorage.setItem).toHaveBeenCalledWith(
SESSION_USER_ID,
'"42"'
);
wrapper.update();
await waitForElement(wrapper, 'Redirect', (el) => el.length === 1);
await waitForElement(
wrapper,
'Redirect',
(el) => el.props().to === '/projects'
);
});

test('GitHub auth buttons shown', async () => {
AuthAPI.read.mockResolvedValue({
data: {
Expand Down
31 changes: 31 additions & 0 deletions awx/ui/src/util/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,34 @@ export function isAuthenticated(cookie) {
}
return false;
}

export function getCurrentUserId(cookie) {
if (!isAuthenticated(cookie)) {
return null;
}
const name = 'current_user';
let userId = null;
if (cookie && cookie !== '') {
const cookies = cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const parsedCookie = cookies[i].trim();
if (parsedCookie.substring(0, name.length + 1) === `${name}=`) {
userId = parseUserId(
decodeURIComponent(parsedCookie.substring(name.length + 1))
);
break;
}
}
}
return userId;
}

function parseUserId(decodedUserData) {
const items = decodedUserData.split(',');
for (let i = 0; i < items.length; i++) {
if (items[i].includes('id') && items[i].includes(':')) {
return items[i].split(':')[1];
}
}
return null;
}
16 changes: 15 additions & 1 deletion awx/ui/src/util/auth.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isAuthenticated } from './auth';
import { isAuthenticated, getCurrentUserId } from './auth';

const invalidCookie = 'invalid';
const validLoggedOutCookie =
Expand All @@ -19,3 +19,17 @@ describe('isAuthenticated', () => {
expect(isAuthenticated(validLoggedInCookie)).toEqual(true);
});
});

describe('getCurrentUserId', () => {
test('returns null for invalid cookie', () => {
expect(getCurrentUserId(invalidCookie)).toEqual(null);
});

test('returns null for expired cookie', () => {
expect(getCurrentUserId(validLoggedOutCookie)).toEqual(null);
});

test('returns current user id for valid authenticated cookie', () => {
expect(getCurrentUserId(validLoggedInCookie)).toEqual('1');
});
});

0 comments on commit 21813cf

Please sign in to comment.