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

Refresh accessToken when JWT access token has expired #1120

Merged
merged 9 commits into from
Aug 11, 2023
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ yarn

### 3. Create Discord Bot

Create and setup the Discord bot. Be sure to take not of ENV variables during setup as these will be needed during the next step. You need to have administrative access to a Discord server in order to create a bot. Creating a server is free, we recommend setting up a personal server to use for testing purposes.
Create and setup the Discord bot. Be sure to take not of ENV variables during setup as these will be needed during the next step. You need to have administrative access to a Discord server in order to create a bot. Creating a server is free, we recommend setting up a personal server to use for testing purposes.

[Create the Praise Discord bot](https://givepraise.xyz/docs/server-setup/create-discord-bot)

Expand All @@ -70,6 +70,12 @@ Run mongo:
yarn mongodb:start
```

Finishing ENV setup

```
yarn run setup
```

### 6. Build and start api backend

Api, discord-bot and frontend can also be started from the Visual Studio Code Launch menu.
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"analyze": "yarn run build && source-map-explorer --gzip 'build/static/js/*.js'",
"load-env": "env-cmd --silent --no-override -f ../../.env env-cmd --silent --no-override",
"start": "PORT=$(grep FRONTEND_PORT ../../.env | cut -d '=' -f2) TAILWIND_MODE=watch yarn run load-env craco start",
"lint": "eslint . --ext .ts --ext .tsx",
"lint": "eslint . --ext .ts --ext .tsx --fix",
"test": "jest",
"test:watch": "jest --watch"
},
Expand Down
10 changes: 10 additions & 0 deletions packages/frontend/src/model/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export const AccessToken = selector<string | undefined>({
},
});

export const RefreshToken = selector<string | undefined>({
key: 'RefreshToken',
get: ({ get }) => {
const activeTokenSet = get(ActiveTokenSet);
if (!activeTokenSet) return;

return activeTokenSet.refreshToken;
},
});

export const DecodedAccessToken = selector<JwtTokenData | undefined>({
key: 'DecodedAccessToken',
get: ({ get }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { components } from 'api-types';

export type RefreshTokenInputDto = components['schemas']['GenerateTokenDto'];
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export interface TokenSet {
accessToken: string;
refreshToken: string;
}
23 changes: 22 additions & 1 deletion packages/frontend/src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ActivateInputDto } from '@/model/activate/dto/activate-input.dto';
import { TokenSet } from '@/model/auth/interfaces/token-set.interface';
import { LoginResponseDto } from '@/model/auth/dto/login-response.dto';
import { NonceResponseDto } from '@/model/auth/dto/nonce-response.dto';
import { RefreshTokenInputDto } from '@/model/auth/dto/refresh-token-input-dto';

export const requestApiAuth = async (
params: LoginInputDto
Expand All @@ -15,10 +16,30 @@ export const requestApiAuth = async (
const response = await apiClient.post('/auth/eth-signature/login', params);
if (!response) throw Error('Failed to request authorization');

const { accessToken } = response.data as unknown as LoginResponseDto;
const { accessToken, refreshToken } =
response.data as unknown as LoginResponseDto;

setRecoil(ActiveTokenSet, {
accessToken,
refreshToken,
});

return getRecoil(ActiveTokenSet);
};

export const requestApiRefreshToken = async (
params: RefreshTokenInputDto
): Promise<TokenSet | undefined> => {
const apiClient = makeApiClient();
const response = await apiClient.post('/auth/eth-signature/refresh', params);
if (!response) throw Error('Failed to request authorization');

const { accessToken, refreshToken } =
response.data as unknown as LoginResponseDto;

setRecoil(ActiveTokenSet, {
accessToken,
refreshToken,
});

return getRecoil(ActiveTokenSet);
Expand Down
49 changes: 44 additions & 5 deletions packages/frontend/src/utils/axios.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios, { AxiosError, AxiosInstance } from 'axios';
import { toast } from 'react-hot-toast';
import { requestApiRefreshToken } from '@/utils/auth';

const isJsonBlob = (data): data is Blob =>
data instanceof Blob && data.type === 'application/json';
Expand All @@ -9,10 +10,48 @@ const isJsonBlob = (data): data is Blob =>
*
* @param err
*/
export const handleErrors = (
err: AxiosError,
export const handleErrors = async (
err: AxiosError<{
code: number;
message: string;
statusCode: number;
}>,
handleErrorsAutomatically = true
): AxiosError => {
): Promise<
AxiosError<{
code: number;
message: string;
statusCode: number;
}>
> => {
let refreshToken;
const recoilPersist = localStorage.getItem('recoil-persist');
Copy link
Member

Choose a reason for hiding this comment

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

Use getRecoil(ActiveTokenSet), see auth.ts for example.

if (recoilPersist) {
refreshToken = JSON.parse(recoilPersist)?.ActiveTokenSet?.refreshToken;
}

if (
refreshToken &&
recoilPersist &&
err?.response?.status === 401 &&
// 1092, 1107 are the error codes for invalid jwt token that defined in backend
(err?.response?.data?.code === 1092 || err?.response?.data?.code === 1107)
Copy link
Member

Choose a reason for hiding this comment

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

1092 means unauthenticated user trying to access authenticated resource. This should not trigger a refresh.

) {
// delete the old token from localStorage to prevent infinite loop
delete recoilPersist['ActiveTokenSet'];
localStorage.setItem(JSON.stringify(recoilPersist), 'recoil-persist');
try {
await requestApiRefreshToken({ refreshToken });
toast('Please try again');
return err;
} catch (error) {
console.log(
'refresh accessToken error',
(error as AxiosError)?.response?.data
);
}
}

// Handling errors automatically means the error will be displayed to the user with a toast.
// If not handled automatically, the error will just be logged to the console and returned.
if (!handleErrorsAutomatically) {
Expand All @@ -27,8 +66,8 @@ export const handleErrors = (
const json = JSON.parse(text);
toast.error(json.message);
});
} else if ((err.response.data as Error).message) {
toast.error((err.response.data as Error).message);
} else if (err.response.data.message) {
toast.error(err.response.data.message);
} else {
toast.error('Something went wrong');
}
Expand Down