diff --git a/awx/ui/src/App.js b/awx/ui/src/App.js
index bf1701eec90d..5f35ecfbc2d7 100644
--- a/awx/ui/src/App.js
+++ b/awx/ui/src/App.js
@@ -101,9 +101,9 @@ const AuthorizedRoutes = ({ routeConfig }) => {
export function ProtectedRoute({ children, ...rest }) {
const {
authRedirectTo,
- setAuthRedirectTo,
- loginRedirectOverride,
isUserBeingLoggedOut,
+ loginRedirectOverride,
+ setAuthRedirectTo,
} = useSession();
const location = useLocation();
diff --git a/awx/ui/src/constants.js b/awx/ui/src/constants.js
index 303061e3ea40..d4d7259a4df8 100644
--- a/awx/ui/src/constants.js
+++ b/awx/ui/src/constants.js
@@ -11,3 +11,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 PERSISTENT_FILTER_KEY = 'awx-persistent-filter';
+export const SESSION_USER_ID = 'awx-session-user-id';
diff --git a/awx/ui/src/contexts/Session.js b/awx/ui/src/contexts/Session.js
index 3a33fb3f6294..af3494831346 100644
--- a/awx/ui/src/contexts/Session.js
+++ b/awx/ui/src/contexts/Session.js
@@ -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
@@ -101,6 +101,7 @@ function SessionProvider({ children }) {
setIsUserBeingLoggedOut(true);
if (!isSessionExpired.current) {
setAuthRedirectTo('/logout');
+ window.localStorage.setItem(SESSION_USER_ID, null);
}
sessionStorage.clear();
await RootAPI.logout();
@@ -167,21 +168,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,
diff --git a/awx/ui/src/screens/Login/Login.js b/awx/ui/src/screens/Login/Login.js
index aae185155927..c19d02d9b168 100644
--- a/awx/ui/src/screens/Login/Login.js
+++ b/awx/ui/src/screens/Login/Login.js
@@ -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';
@@ -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';
@@ -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,
@@ -112,8 +115,26 @@ function AWXLogin({ alt, isAuthenticated }) {
if (isCustomLoginInfoLoading) {
return null;
}
- if (isAuthenticated(document.cookie)) {
- return ;
+
+ if (isAuthenticated(document.cookie) && !hasVerifiedUser.current) {
+ const currentUserId = getCurrentUserId(document.cookie);
+ const verifyIsNewUser = () => {
+ const previousUserId = JSON.parse(
+ window.localStorage.getItem(SESSION_USER_ID)
+ );
+ if (previousUserId === null) {
+ return true;
+ }
+ return currentUserId.toString() !== previousUserId.toString();
+ };
+ isNewUser.current = verifyIsNewUser();
+ hasVerifiedUser.current = true;
+ window.localStorage.setItem(SESSION_USER_ID, JSON.stringify(currentUserId));
+ }
+
+ if (isAuthenticated(document.cookie) && hasVerifiedUser.current) {
+ const redirect = isNewUser.current ? '/' : authRedirectTo;
+ return ;
}
let helperText;
diff --git a/awx/ui/src/screens/Login/Login.test.js b/awx/ui/src/screens/Login/Login.test.js
index bc168b5e3f10..7768b2ae4b84 100644
--- a/awx/ui/src/screens/Login/Login.test.js
+++ b/awx/ui/src/screens/Login/Login.test.js
@@ -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 {
@@ -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',
@@ -72,6 +80,13 @@ describe('', () => {
custom_logo: 'images/foo.jpg',
},
});
+ Object.defineProperty(window, 'localStorage', {
+ value: {
+ getItem: jest.fn(() => '42'),
+ setItem: jest.fn(() => null),
+ },
+ writable: true,
+ });
});
afterEach(() => {
@@ -276,15 +291,77 @@ describe('', () => {
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( true} />);
+ wrapper = mountWithContexts( true} />, {
+ context: {
+ router: { history },
+ session: {
+ authRedirectTo: '/projects',
+ handleSessionContinue: () => {},
+ isSessionExpired: false,
+ isUserBeingLoggedOut: false,
+ loginRedirectOverride: null,
+ logout: () => {},
+ sessionCountdown: 60,
+ setAuthRedirectTo: () => {},
+ },
+ },
+ });
});
+ expect(window.localStorage.getItem).toHaveBeenCalledWith(SESSION_USER_ID);
+ expect(window.localStorage.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( true} />, {
+ context: {
+ router: { history },
+ session: {
+ authRedirectTo: '/projects',
+ handleSessionContinue: () => {},
+ isSessionExpired: false,
+ isUserBeingLoggedOut: false,
+ loginRedirectOverride: null,
+ logout: () => {},
+ sessionCountdown: 60,
+ setAuthRedirectTo: () => {},
+ },
+ },
+ });
+ });
+
+ wrapper.update();
+ expect(window.localStorage.getItem).toHaveBeenCalledWith(SESSION_USER_ID);
+ expect(window.localStorage.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: {
diff --git a/awx/ui/src/util/auth.js b/awx/ui/src/util/auth.js
index e8d9f4dea77c..394737587e98 100644
--- a/awx/ui/src/util/auth.js
+++ b/awx/ui/src/util/auth.js
@@ -6,3 +6,29 @@ 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 userData = JSON.parse(decodedUserData);
+ return userData.id;
+}
diff --git a/awx/ui/src/util/auth.test.js b/awx/ui/src/util/auth.test.js
index 01d15b0b0f0e..db0d2bf24523 100644
--- a/awx/ui/src/util/auth.test.js
+++ b/awx/ui/src/util/auth.test.js
@@ -1,4 +1,4 @@
-import { isAuthenticated } from './auth';
+import { isAuthenticated, getCurrentUserId } from './auth';
const invalidCookie = 'invalid';
const validLoggedOutCookie =
@@ -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);
+ });
+});