Skip to content

Commit

Permalink
Merge pull request #253 from woowacourse-teams/feat/135-refresh-when-…
Browse files Browse the repository at this point in the history
…access-token-expired

access token이 만료되었을 때 자동으로 refresh되도록 구현
  • Loading branch information
solo5star authored Aug 1, 2023
2 parents 8113488 + f57b2b9 commit 6ab4cca
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 8 deletions.
48 changes: 40 additions & 8 deletions client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,44 @@ export class ClientNetworkError extends Error {
}

class Client {
isAccessTokenRefreshing = false;

accessToken: string | null = null;

accessTokenRefreshListener: ((accessToken: string) => void) | null = null;

// FIXME: react <-> 외부 시스템(Client)와 상태 연동을 위해 양방향 바인딩을 걸었습니다.
// 향후 단방향으로 수정해야 합니다.
onAccessTokenRefresh(listener: (accessToken: string) => void) {
this.accessTokenRefreshListener = listener;
}

async fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
try {
const response = await fetch(`/api${input}`, {
...init,
headers: {
...init?.headers,
...(this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {}),
},
});
const fetchFn = () =>
fetch(`/api${input}`, {
...init,
headers: {
...init?.headers,
...(this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {}),
},
});

let response = await fetchFn();
if (!response.ok) {
throw response;
if (response.status === 401 && !this.isAccessTokenRefreshing) {
// this.refreshAccessToken() 을 호출하기 전,
// this.isAccessTokenRefreshing을 true로 설정해줘야 잠재적인 recursion loop를 방지할 수 있습니다.
this.isAccessTokenRefreshing = true;
const accessToken = await this.refreshAccessToken();
this.accessToken = accessToken;
this.accessTokenRefreshListener?.(accessToken);
this.isAccessTokenRefreshing = false;

response = await fetchFn();
}

if (!response.ok) throw response;
}
return response;
} catch (error) {
Expand Down Expand Up @@ -81,6 +106,13 @@ class Client {
);
}

/**
* accessToken이 만료되었을 때, 서버에서 accessToken을 다시 발급받는다.
*/
refreshAccessToken() {
return this.fetchJson<{ token: string }>(`/auth`).then((data) => data.token);
}

/**
* access token을 지워도 refresh token이 남아있으면 계속 access token이 발급되기
* 때문에, 완전한 로그아웃을 하려면 refresh token을 삭제해주어야 한다.
Expand Down
1 change: 1 addition & 0 deletions client/src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const AuthProvider = (props: AuthProviderProps) => {

const identity = useMemo(() => {
client.setAccessToken(accessToken);
client.onAccessTokenRefresh(setAccessToken);

if (!accessToken) return null;

Expand Down
22 changes: 22 additions & 0 deletions client/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,28 @@ export const handlers = [
);
}),

rest.get('/api/auth', async (req, res, ctx) => {
const token =
btoa(
JSON.stringify({
typ: 'JWT',
alg: 'HS256',
}),
) +
'.' +
btoa(
JSON.stringify({
sub: '1000',
iat: Date.now(),
exp: Date.now() + 1 * 60 * 60 * 1000,
} satisfies Identity),
) +
'.' +
'SUPERSECRET';

return res(ctx.status(200), ctx.json({ token }));
}),

rest.get('/api/auth/urls', async (req, res, ctx) => {
return res(
ctx.status(200),
Expand Down

0 comments on commit 6ab4cca

Please sign in to comment.