Skip to content

Commit

Permalink
Improve refresh token flow
Browse files Browse the repository at this point in the history
  • Loading branch information
leighmacdonald committed Oct 26, 2023
1 parent b1db5eb commit 6e8c161
Show file tree
Hide file tree
Showing 18 changed files with 309 additions and 160 deletions.
6 changes: 3 additions & 3 deletions frontend/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
}
},
"plugins": [
"prettier",
//"prettier",
"@typescript-eslint",
"no-loops",
"jest",
Expand All @@ -22,13 +22,13 @@
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
//"prettier",
"plugin:jest/recommended",
"plugin:react-hooks/recommended",
"plugin:react/recommended"
],
"rules": {
"prettier/prettier": 2,
//"prettier/prettier": 2,
"@typescript-eslint/no-explicit-any": "warn",
//"no-console": 2,
"no-loops/no-loops": "warn",
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"ip-cidr": "^3.1.0",
"jest": "^29.6.4",
"js-cookie": "^3.0.5",
"jwt-decode": "^3.1.2",
"leaflet": "^1.9.4",
"lodash-es": "^4.17.21",
"marked": "^7.0.5",
Expand Down
47 changes: 22 additions & 25 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,11 @@ import { AdminPeople } from './page/AdminPeople';
import { Servers } from './page/Servers';
import { AdminServers } from './page/AdminServers';
import { Flash } from './component/Flashes';
import { LoginSteamSuccess } from './page/LoginSteamSuccess';
import { Profile } from './page/Profile';
import { Footer } from './component/Footer';
import { CurrentUserCtx, GuestProfile } from './contexts/CurrentUserCtx';
import { BanPage } from './page/BanPage';
import {
PermissionLevel,
readRefreshToken,
readAccessToken,
UserProfile,
writeAccessToken,
writeRefreshToken
} from './api';
import { PermissionLevel, UserProfile } from './api';
import { AdminBan } from './page/AdminBan';
import { TopBar } from './component/TopBar';
import { UserFlashCtx } from './contexts/UserFlashCtx';
Expand Down Expand Up @@ -58,6 +50,8 @@ import { ProfileSettingsPage } from './page/ProfileSettingsPage';
import { StatsWeaponOverallPage } from './page/StatsWeaponOverallPage';
import { StatsPage } from './page/StatsPage';
import { PlayerStatsPage } from './page/PlayerStatsPage';
import { LoginSteamSuccess } from './page/LoginSteamSuccess';
import { LogoutHandler } from './component/LogoutHandler';

export interface AppProps {
initialTheme: PaletteMode;
Expand All @@ -67,6 +61,13 @@ export const App = ({ initialTheme }: AppProps): JSX.Element => {
const [currentUser, setCurrentUser] =
useState<NonNullable<UserProfile>>(GuestProfile);
const [flashes, setFlashes] = useState<Flash[]>([]);
// const navigate = useNavigate();
//
// window.addEventListener('storage', (event: StorageEvent) => {
// if (event.key === 'logout') {
// navigate('/');
// }
// });

const saveUser = (profile: UserProfile) => {
setCurrentUser(profile);
Expand Down Expand Up @@ -125,11 +126,7 @@ export const App = ({ initialTheme }: AppProps): JSX.Element => {
<CurrentUserCtx.Provider
value={{
currentUser,
setCurrentUser: saveUser,
getToken: readAccessToken,
setToken: writeAccessToken,
getRefreshToken: readRefreshToken,
setRefreshToken: writeRefreshToken
setCurrentUser: saveUser
}}
>
<UserFlashCtx.Provider value={{ flashes, setFlashes, sendFlash }}>
Expand All @@ -141,6 +138,7 @@ export const App = ({ initialTheme }: AppProps): JSX.Element => {
<NotificationsProvider>
<React.StrictMode>
<UserInit />
<LogoutHandler />
<CssBaseline />
<Container maxWidth={'lg'}>
<TopBar />
Expand Down Expand Up @@ -173,7 +171,16 @@ export const App = ({ initialTheme }: AppProps): JSX.Element => {
</ErrorBoundary>
}
/>

<Route
path={
'/login/success'
}
element={
<ErrorBoundary>
<LoginSteamSuccess />
</ErrorBoundary>
}
/>
<Route
path={'/stats'}
element={
Expand Down Expand Up @@ -581,16 +588,6 @@ export const App = ({ initialTheme }: AppProps): JSX.Element => {
</ErrorBoundary>
}
/>
<Route
path={
'/login/success'
}
element={
<ErrorBoundary>
<LoginSteamSuccess />
</ErrorBoundary>
}
/>
<Route
path={
'/login/discord'
Expand Down
81 changes: 48 additions & 33 deletions frontend/src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { apiCall } from './common';
import { logErr } from '../util/errors';
import decodeJWT, { JwtPayload } from 'jwt-decode';

export const refreshKey = 'refresh';
export const tokenKey = 'token';
export const userKey = 'user';
export const logoutKey = 'logout';

export interface UserToken {
access_token: string;
Expand Down Expand Up @@ -33,12 +35,24 @@ export const refreshToken = async () => {
}
};

export const writeAccessToken = (token: string) => {
try {
return sessionStorage.setItem(tokenKey, token);
} catch (e) {
return '';
export const isTokenExpired = (token: string): boolean => {
if (!token || token == '') {
return true;
}

const claims: JwtPayload = decodeJWT(token);
if (!claims || !claims.exp) {
return true;
}

const expirationTimeInSeconds = claims.exp * 1000;
const now = new Date();

return expirationTimeInSeconds <= now.getTime();
};

export const writeAccessToken = (token: string) => {
sessionStorage.setItem(tokenKey, token);
};

export const readAccessToken = () => {
Expand All @@ -50,11 +64,7 @@ export const readAccessToken = () => {
};

export const writeRefreshToken = (token: string) => {
try {
return localStorage.setItem(refreshKey, token);
} catch (e) {
return '';
}
localStorage.setItem(refreshKey, token);
};

export const readRefreshToken = () => {
Expand All @@ -65,10 +75,16 @@ export const readRefreshToken = () => {
}
};

// export const handleOnLogout = (): void => {
// localStorage.clear();
// location.reload();
// };
export const writeLogoutKey = () => {
window.localStorage.setItem(logoutKey, Date.now().toString());
console.log('logout fired');
};

export const logout = (): void => {
writeAccessToken('');
writeRefreshToken('');
writeLogoutKey();
};

export const parseJwt = (token: string) => {
const base64Payload = token.split('.')[1];
Expand All @@ -90,31 +106,30 @@ export const handleOnLogin = (returnPath: string): string => {
returnUrl = `${returnUrl}:${window.location.port}`;
}
// Don't redirect loop to /login
const r = `${
const returnTo = `${
window.location.protocol
}//${returnUrl}/auth/callback?return_url=${
returnPath !== '/login' ? returnPath : '/'
}`;
return (
'https://steamcommunity.com/openid/login' +
'?openid.ns=' +
encodeURIComponent('http://specs.openid.net/auth/2.0') +
'&openid.mode=checkid_setup' +
'&openid.return_to=' +
encodeURIComponent(r) +
`&openid.realm=` +
encodeURIComponent(

return [
'https://steamcommunity.com/openid/login',
`?openid.ns=${encodeURIComponent('http://specs.openid.net/auth/2.0')}`,
'&openid.mode=checkid_setup',
`&openid.return_to=${encodeURIComponent(returnTo)}`,
`&openid.realm=${encodeURIComponent(
`${window.location.protocol}//${window.location.hostname}`
) +
'&openid.ns.sreg=' +
encodeURIComponent('http://openid.net/extensions/sreg/1.1') +
'&openid.claimed_id=' +
encodeURIComponent(
)}`,
`&openid.ns.sreg=${encodeURIComponent(
'http://openid.net/extensions/sreg/1.1'
)}`,
`&openid.claimed_id=${encodeURIComponent(
'http://specs.openid.net/auth/2.0/identifier_select'
) +
'&openid.identity=' +
encodeURIComponent('http://specs.openid.net/auth/2.0/identifier_select')
);
)}`,
`&openid.identity=${encodeURIComponent(
'http://specs.openid.net/auth/2.0/identifier_select'
)}`
].join('');
};

export const discordLoginURL = () => {
Expand Down
29 changes: 19 additions & 10 deletions frontend/src/api/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ReportStatus } from './report';
import { format, parseISO } from 'date-fns';
import { readAccessToken, readRefreshToken, refreshToken } from './auth';
import {
isTokenExpired,
readAccessToken,
readRefreshToken,
refreshToken
} from './auth';
import { parseDateTime } from '../util/text';
import { MatchResult } from './match';

Expand Down Expand Up @@ -45,15 +50,21 @@ export const apiCall = async <
credentials: 'include',
method: method.toUpperCase()
};
const token = readAccessToken();

let token = readAccessToken();
const refresh = readRefreshToken();
if (!token && refresh != '' && !isRefresh) {
if ((await refreshToken()) != '') {
return apiCall(url, method, body, true);
if (token == '' || isTokenExpired(token)) {
if (refresh != '' && !isTokenExpired(refresh)) {
token = await refreshToken();
}
}

if (token != '' && token != null) {
if (isRefresh) {
// Use the refresh token instead when performing a token refresh request
token = readRefreshToken();
}

if (token != '') {
headers['Authorization'] = `Bearer ${token}`;
}

Expand All @@ -62,15 +73,13 @@ export const apiCall = async <
}
opts.headers = headers;
const u = new URL(url, `${location.protocol}//${location.host}`);
if (u.port == '8080') {
u.port = '6006';
}
const resp = await fetch(u, opts);

if (resp.status == 401 && !isRefresh && refresh != '' && token != '') {
// Try and refresh the token once
if ((await refreshToken()) != '') {
// Successful token refresh, make a single recursive retry
return apiCall(url, method, body, true);
return apiCall(url, method, body, false);
}
}
if (resp.status === 403 && token != '') {
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/component/LogoutHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { logout, logoutKey } from '../api';

export const LogoutHandler = () => {
// Listen for storage events with the logout key and logout from all browser sessions/tabs when fired.
window.addEventListener('storage', (event) => {
if (event.key === logoutKey) {
logout();
document.location.reload();
}
});

return <></>;
};
7 changes: 2 additions & 5 deletions frontend/src/component/UserInit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { GuestProfile, useCurrentUserCtx } from '../contexts/CurrentUserCtx';
import { emptyOrNullString } from '../util/types';

export const UserInit = () => {
const { setCurrentUser, currentUser, setRefreshToken } =
useCurrentUserCtx();
const { setCurrentUser, currentUser } = useCurrentUserCtx();

useEffect(() => {
if (currentUser.steam_id != GuestProfile.steam_id) {
Expand All @@ -23,9 +22,7 @@ export const UserInit = () => {
setCurrentUser(GuestProfile);
});
}

// eslint-disable-next-line
}, [setRefreshToken]);
});

return <></>;
};
19 changes: 2 additions & 17 deletions frontend/src/contexts/CurrentUserCtx.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { createContext, useContext } from 'react';
import {
PermissionLevel,
readAccessToken,
userKey,
UserProfile,
writeAccessToken,
writeRefreshToken
} from '../api';
import { PermissionLevel, userKey, UserProfile } from '../api';

export const GuestProfile: UserProfile = {
updated_on: new Date(),
Expand All @@ -24,10 +17,6 @@ export const GuestProfile: UserProfile = {
export type CurrentUser = {
currentUser: UserProfile;
setCurrentUser: (profile: UserProfile) => void;
getToken: () => string;
setToken: (token: string) => void;
getRefreshToken: () => string;
setRefreshToken: (token: string) => void;
};

export const CurrentUserCtx = createContext<CurrentUser>({
Expand All @@ -38,11 +27,7 @@ export const CurrentUserCtx = createContext<CurrentUser>({
} catch (e) {
return;
}
},
getToken: readAccessToken,
setToken: writeAccessToken,
getRefreshToken: readAccessToken,
setRefreshToken: writeRefreshToken
}
});

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
Expand Down
Loading

0 comments on commit 6e8c161

Please sign in to comment.