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

Logging out of Gitlab and clearing session storage #793

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
86e4590
Fix invalid hook call
atomicgamedeveloper May 30, 2024
b992717
Simplify invalid hook fix
atomicgamedeveloper May 30, 2024
8fdf091
Refactoring signOut
atomicgamedeveloper Jun 4, 2024
bbecb64
Fixes logout
atomicgamedeveloper Jun 13, 2024
c3c6c52
Clean cookies and session storage post logout
atomicgamedeveloper Jun 14, 2024
8b2268e
Goto backend login using signoutSilent and reload
atomicgamedeveloper Jun 17, 2024
a48b3ec
Changed alert into Error
atomicgamedeveloper Jun 17, 2024
292d882
Simplify by removing revokeTokens, removeUser and clearStaleState
atomicgamedeveloper Jun 17, 2024
b071d52
Update test README.md and package.js command
atomicgamedeveloper Jun 18, 2024
083e558
Add Authentication.test.ts
atomicgamedeveloper Jun 18, 2024
0ea5e4b
Add throw test upon bad logout redirect URI
atomicgamedeveloper Jun 19, 2024
26c7c4e
Add sessionStorage clear, fetch and error tests
atomicgamedeveloper Jun 20, 2024
b688730
Fix URL in signOut
atomicgamedeveloper Jun 20, 2024
9beb3b4
Add negative signOutSilent test
atomicgamedeveloper Jun 20, 2024
8f69fe1
Add and improve tests
atomicgamedeveloper Jun 21, 2024
34f1563
Downgrade typescript and ts-jest
atomicgamedeveloper Jun 23, 2024
ed13620
Reinstate refresh and access token revocation
atomicgamedeveloper Jun 24, 2024
ecf6367
Add additional tests for latest signOut changes
atomicgamedeveloper Jun 25, 2024
9f163f1
Remove eslint-disable line frmo Authentication.ts
atomicgamedeveloper Jun 25, 2024
796324a
Add 30 second fetch-timeout, tests and reload to avoid oauth bug
atomicgamedeveloper Jun 25, 2024
48b3ad8
Add 3 second timeout before reloading page and useAppURL
atomicgamedeveloper Jun 28, 2024
b1003df
Merge branch 'INTO-CPS-Association:feature/distributed-demo' into 479…
atomicgamedeveloper Jul 2, 2024
bda4a1b
Fix build error by exporting useAPPURL
atomicgamedeveloper Jul 3, 2024
cf28d89
Update Authentication tests
atomicgamedeveloper Jul 4, 2024
19bd8c5
Fix invalid hook call
atomicgamedeveloper May 30, 2024
560d0bb
Simplify invalid hook fix
atomicgamedeveloper May 30, 2024
085f753
Refactoring signOut
atomicgamedeveloper Jun 4, 2024
e91ee19
Fixes logout
atomicgamedeveloper Jun 13, 2024
6739321
Clean cookies and session storage post logout
atomicgamedeveloper Jun 14, 2024
4c0c2f4
Goto backend login using signoutSilent and reload
atomicgamedeveloper Jun 17, 2024
478d88c
Changed alert into Error
atomicgamedeveloper Jun 17, 2024
5af3c68
Simplify by removing revokeTokens, removeUser and clearStaleState
atomicgamedeveloper Jun 17, 2024
0f9cdb2
Update test README.md and package.js command
atomicgamedeveloper Jun 18, 2024
0d206a8
Add Authentication.test.ts
atomicgamedeveloper Jun 18, 2024
1395146
Add throw test upon bad logout redirect URI
atomicgamedeveloper Jun 19, 2024
f96b553
Add sessionStorage clear, fetch and error tests
atomicgamedeveloper Jun 20, 2024
8ef315c
Fix URL in signOut
atomicgamedeveloper Jun 20, 2024
f916d10
Add negative signOutSilent test
atomicgamedeveloper Jun 20, 2024
521932a
Add and improve tests
atomicgamedeveloper Jun 21, 2024
5d094ed
Downgrade typescript and ts-jest
atomicgamedeveloper Jun 23, 2024
4bc6ce5
Reinstate refresh and access token revocation
atomicgamedeveloper Jun 24, 2024
3e0acf6
Add additional tests for latest signOut changes
atomicgamedeveloper Jun 25, 2024
3b79ffe
Remove eslint-disable line frmo Authentication.ts
atomicgamedeveloper Jun 25, 2024
9445d76
Add 30 second fetch-timeout, tests and reload to avoid oauth bug
atomicgamedeveloper Jun 25, 2024
d788bc9
Add 3 second timeout before reloading page and useAppURL
atomicgamedeveloper Jun 28, 2024
be8f7e7
Fix build error by exporting useAPPURL
atomicgamedeveloper Jul 3, 2024
6364924
Update Authentication tests
atomicgamedeveloper Jul 4, 2024
5402b78
Merge branch '479-proper-gitlab-log-out' of https://github.com/atomic…
atomicgamedeveloper Jul 5, 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
4 changes: 2 additions & 2 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"stop": "npx kill-port 4000",
"syntax": "npx eslint . --fix",
"test:all": "yarn test:unit && yarn test:int && yarn test:e2e",
"test:e2e": "yarn build && yarn configapp:test && npx kill-port 4000 && yarn start >/dev/null & playwright test && npx kill-port 4000",
"test:e2e": "yarn build && yarn config:test && npx kill-port 4000 && yarn start >/dev/null & playwright test && npx kill-port 4000",
"test:int": "jest -c ./jest.integration.config.json ../test/integration --coverage",
"test:unit": "jest -c ./jest.config.json ../test/unitTests --coverage"
},
Expand All @@ -51,7 +51,7 @@
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"dotenv": "^16.1.4",
"eslint": "^8.54.0",
"eslint": "8.54.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.29.0",
Expand Down
2 changes: 1 addition & 1 deletion client/src/page/MenuToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function MenuToolbar({

const handleSignOut = async () => {
if (auth) {
await signOut();
await signOut(auth);
}
};

Expand Down
33 changes: 23 additions & 10 deletions client/src/util/auth/Authentication.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { User } from 'oidc-client-ts';
import { useDispatch } from 'react-redux';
import { setUserName } from 'store/auth.slice';
import { useAuth } from 'react-oidc-context';
import { getLogoutRedirectURI } from '../envUtil';
import { AuthContextProps } from 'react-oidc-context';
import { getLogoutRedirectURI, useAppURL, cleanURL } from 'util/envUtil';

export interface CustomAuthContext {
signoutRedirect: () => Promise<void>;
removeUser: () => Promise<void>;
signoutSilent: () => Promise<void>;
revokeTokens: () => Promise<void>;
user?: User | null | undefined;
}

Expand All @@ -20,20 +22,31 @@ export function getAndSetUsername(auth: CustomAuthContext) {
}
}

export async function signOut() {
const auth = useAuth();
export async function signOut(auth: AuthContextProps) {
prasadtalasila marked this conversation as resolved.
Show resolved Hide resolved
prasadtalasila marked this conversation as resolved.
Show resolved Hide resolved
if (!auth.user) {
return;
}
const LOGOUT_URL = getLogoutRedirectURI() ?? '';

if (auth.user) {
const idToken = auth.user.id_token; // camelCase for variable name
localStorage.clear();
sessionStorage.clear();

const APP_URL = cleanURL(useAppURL());
const idToken = auth.user.id_token;
try {
await auth.revokeTokens();
await auth.removeUser();
await auth.clearStaleState();
sessionStorage.clear();
document.cookie = '_xsrf=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
await auth.signoutRedirect({
post_logout_redirect_uri: LOGOUT_URL.toString(),
id_token_hint: idToken,
});
await fetch(`${APP_URL}/_oauth/logout`, {
signal: AbortSignal.timeout(30000),
});
setTimeout(() => {
window.location.reload();
}, 3000);
} catch (e) {
throw new Error(`Error occurred during logout: ${e}`);
}
}

Expand Down
2 changes: 1 addition & 1 deletion client/src/util/envUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function useURLforLIB(): string {
return useUserLink(useAppURL(), window.env.REACT_APP_URL_LIBLINK);
}

function useAppURL(): string {
export function useAppURL(): string {
return `${cleanURL(window.env.REACT_APP_URL)}/${useURLbasename()}`;
}

Expand Down
4 changes: 2 additions & 2 deletions client/test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The E2E tests require a functional gitlab oauth setup, traefik gateway and a
live react client website. The E2E tests do not launch the react client website.
So it is important to launch the website using `yarn start`. Keep this server
running while performing the E2E tests with `yarn test -e` command.
running while performing the E2E tests with `yarn test:e2e` command.

The E2E tests use playwright test runner. You also need to have the software
installed. If it is not installed, you can install with the following command.
Expand Down Expand Up @@ -174,7 +174,7 @@ gateway and the client website hosted behind traefik.
Once you've properly set up your .env file, you can now run the end-to-end tests.

```bash
yarn test -e
yarn test:e2e
```

This command launches the test runner and executes all end-to-end tests.
Expand Down
156 changes: 156 additions & 0 deletions client/test/unitTests/Util/Auth/Authentication.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { signOut } from 'util/auth/Authentication';
import { useAuth } from 'react-oidc-context';
import { getLogoutRedirectURI, useAppURL, cleanURL } from 'util/envUtil';

jest.mock('react-oidc-context');
Copy link
Contributor

@prasadtalasila prasadtalasila Jun 19, 2024

Choose a reason for hiding this comment

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

jest.mock('util/envUtil', () => ({
getLogoutRedirectURI: jest.fn(),
useAppURL: jest.fn(),
cleanURL: jest.fn(),
}));
jest.useFakeTimers();

describe('signOut', () => {
const mockUser = { id_token: 'token' };
const mockSignoutRedirect = jest.fn();
const mockRevokeTokens = jest.fn();
const mockClearStaleState = jest.fn();
const mockremoveUser = jest.fn();
const mockClear = jest.fn();

Object.defineProperty(AbortSignal, 'timeout', {
value: jest.fn(),
writable: false,
});

Object.defineProperty(global, 'fetch', {
value: jest.fn(async (URL, signal) => {
signal();
switch (URL) {
case 'https://foo.com/_oauth/logout':
return {
ok: true,
status: 401,
json: async () => {},
};
default: {
throw new Error(`Unhandled request: ${URL}`);
}
}
}),
writable: true,
});

beforeEach(() => {
(useAuth as jest.Mock).mockReturnValue({
signoutRedirect: mockSignoutRedirect,
revokeTokens: mockRevokeTokens,
clearStaleState: mockClearStaleState,
removeUser: mockremoveUser,
user: mockUser,
});
(getLogoutRedirectURI as jest.Mock).mockReturnValue(
'https://logoutredirecturi.com/',
);
(useAppURL as jest.Mock).mockReturnValue('https://foo.com/');
(cleanURL as jest.Mock).mockReturnValue('https://foo.com');
Object.defineProperty(window, 'document', {
value: {
cookie: '',
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
},
writable: true,
});
Object.defineProperty(window, 'sessionStorage', {
value: {
clear: mockClear,
},
writable: true,
});
Object.defineProperty(window, 'location', {
value: { reload: jest.fn() },
writable: true,
});
});

afterEach(() => {
jest.clearAllMocks();
});

it('expires _xsrf cookie', async () => {
const auth = useAuth();
await signOut(auth);

expect(window.document.cookie).toBe(
'_xsrf=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;',
);
});

it('does not signoutRedirect if auth.user is null', async () => {
const auth = useAuth();
auth.user = null;

const signOutResult = await signOut(auth);

expect(signOutResult).toBeUndefined();
expect(mockSignoutRedirect).not.toHaveBeenCalled();
});

it('signsOutRedirect, clearStaleState, removeTokens and removeUer if user is authorized', async () => {
const auth = useAuth();
await signOut(auth);
expect(useAppURL).toHaveBeenCalled();
expect(cleanURL).toHaveBeenCalled();
expect(mockSignoutRedirect).toHaveBeenCalled();
expect(mockClearStaleState).toHaveBeenCalled();
expect(mockRevokeTokens).toHaveBeenCalled();
expect(mockremoveUser).toHaveBeenCalled();
});

it('fetches the URI from window.env', async () => {
const auth = useAuth();
const fetchBody = { signal: AbortSignal.timeout(30000) };
await signOut(auth);
expect(global.fetch).toHaveBeenCalledWith(
'https://foo.com/_oauth/logout',
fetchBody,
);
});

it('throws an error if fetch rejects', async () => {
const auth = useAuth();
global.fetch = jest.fn().mockRejectedValueOnce('foo');
await expect(signOut(auth)).rejects.toThrow(
Error('Error occurred during logout: foo'),
);
});

it('throws an error if signoutRedirect rejects', async () => {
const auth = useAuth();
auth.signoutRedirect = jest
.fn()
.mockRejectedValueOnce(new Error('signoutRedirect rejected'));
await expect(signOut(auth)).rejects.toThrow(
Error('Error occurred during logout: Error: signoutRedirect rejected'),
);
});

it('reloads the page', async () => {
const auth = useAuth();
await signOut(auth);
await waitMiliseconds(3000);
expect(window.location.reload).toHaveBeenCalled();
});

it('clears sessionStorage', async () => {
const auth = useAuth();
await signOut(auth);

expect(mockClear).toHaveBeenCalled();
});
});

async function waitMiliseconds(ms: number) {
jest.advanceTimersByTime(ms);
}
Loading
Loading