React Apollo: JWT & Refresh Token
Tags: react, graphql, apollo, tutorial
Published: 2022-05-19 14:54:03

These days, a lot of services choose `JWT(JSON Web Token)` as their authentication. When you implement JWT, you would issue an access token and a refresh token.

---

## AccessToken and RefreshToken

- AccessToken has a short expiration time(like 10~15min) and represents the authorization to access APIs.
- RefreshToken is used for issuing a new access token and has a longer expiration time than the access token.

Thanks to refresh tokens, you can manage more safe access tokens.
You might ask that 'What if a refresh token is leaked?'. There are many strategies that make us safer. like RTR(Refresh Token Rotation).
To put it simply, refresh API issues an access token and a refresh token and expires the refresh token. they assume tokens must've leaked if refresh tokens are used more than once. 

I recommend reading this documentation [auth0-refresh-token-rotation](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation).

I won't talk about JWT in this post anymore, let's move on.

---

## Refresh Token Implementation

I made a test server using `NestJS`. There are three resolvers and two guards.

### Guards

- JwtAuthGuard: Authorize if the access token is valid in the `Authorization` header.
- JwtRefreshAuthGuard: Authorize if the refresh token is valid in the `Authorization` header.

Both tokens will be passed in the `Authorization` header in each request and will be stored in localStorage.
For better security, you can use `cookie`, with the [httpOnly](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) attribute and the [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) attribute.

### APIs

- createToken: issues an access token and a refresh token.
- ping: returns true if an access token is verified otherwise returns `401 error`.
- refreshToken: returns an access token if a refresh token is verified otherwise returns `401 error`

#### DTOs

```typescript
import { ObjectType, Field } from '@nestjs/graphql';

@ObjectType()
export class CreateTokenResponse {
  @Field()
  accessToken: string;

  @Field()
  refreshToken: string;
}

@ObjectType()
export class RefreshTokenResponse {
  @Field()
  accessToken: string;
}

```

#### Resolvers

```typescript
@Resolver()
export class AuthResolver {
  constructor(private readonly authService: AuthService) {}

  @Mutation(() => CreateTokenResponse)
  async createToken(): Promise<CreateTokenResponse> {
    return this.authService.createToken();
  }

  @UseGuards(JwtAuthGuard)
  @Query(() => Boolean)
  async ping() {
    return true;
  }

  @UseGuards(JwtRefreshAuthGuard)
  @Mutation(() => RefreshTokenResponse)
  async refreshToken(): Promise<RefreshTokenResponse> {
    return this.authService.refreshToken();
  }
}
```

### Scenario


![Explanation of the scenario](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hvmg8fx8ytvw83j9io9e.jpg)

In this scenario, there are six steps.

1. Request createToken and get an access token and a refresh token from the server
2. Request pass with an expired access token and get 401 error
3. Request refreshToken
4. Get a new access token
5. Retry the failed request
6. Success!

For the scenario, I set the expiration time of the access token to be 5s.

---

### React Apollo Client

#### Types and Queries

```typescript

/**
 * Types
 */
interface Tokens {
  accessToken: string;
  refreshToken: string;
}

interface AccessToken {
  accessToken: string;
}

/**
 * Queries
 */
const CREATE_TOKEN = gql`
  mutation createToken {
    createToken {
      accessToken
      refreshToken
    }
  }
`;

const REFRESH_TOKEN = gql`
  mutation refreshToken {
    refreshToken {
      accessToken
    }
  }
`;

const PING = gql`
  query ping {
    ping
  }
`;
```

#### Page
```typescript

/**
 * React Components
 */

function App() {
  const [createToken, { data: createTokenData }] = useMutation<{
    createToken: Tokens;
  }>(CREATE_TOKEN);
  const [ping] = useLazyQuery(PING, {
    fetchPolicy: 'network-only',
  });

  const requestToken = () => {
    createToken();
  };

  const sendPing = () => {
    ping();
  };

  useEffect(() => {
    if (!createTokenData) return;

    const { accessToken, refreshToken } = createTokenData.createToken;

    // Save tokens in localStorage
    localStorage.setItem('accessToken', accessToken);
    localStorage.setItem('refreshToken', refreshToken);
  }, [createTokenData]);

  return (
    <Container>
      <button type="button" onClick={requestToken}>
        login
      </button>
      <button type="button" onClick={sendPing}>
        ping
      </button>
    </Container>
  );
}

function ApolloWrapper() {
  return (
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  );
}

/**
 * Styles
 */

const Container = styled.div`
  display: flex;
  flex-direction: column;
  row-gap: 12px;
  padding: 24px;

  > button {
    width: 200px;
    height: 24px;
  }
`;

export default ApolloWrapper;
```

There are two buttons. one is for `createToken` and another one is for `pass`.

#### Requesting refreshToken and retrying failed request

```typescript

/**
 * Apollo Setup
 */

function isRefreshRequest(operation: GraphQLRequest) {
  return operation.operationName === 'refreshToken';
}

// Returns accesstoken if opoeration is not a refresh token request
function returnTokenDependingOnOperation(operation: GraphQLRequest) {
  if (isRefreshRequest(operation))
    return localStorage.getItem('refreshToken') || '';
  else return localStorage.getItem('accessToken') || '';
}

const httpLink = createHttpLink({
  uri: 'http://localhost:3000/graphql',
});

const authLink = setContext((operation, { headers }) => {
  let token = returnTokenDependingOnOperation(operation);

  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    },
  };
});

const errorLink = onError(
  ({ graphQLErrors, networkError, operation, forward }) => {
    if (graphQLErrors) {
      for (let err of graphQLErrors) {
        switch (err.extensions.code) {
          case 'UNAUTHENTICATED':
            // ignore 401 error for a refresh request
            if (operation.operationName === 'refreshToken') return;

            const observable = new Observable<FetchResult<Record<string, any>>>(
              (observer) => {
                // used an annonymous function for using an async function
                (async () => {
                  try {
                    const accessToken = await refreshToken();

                    if (!accessToken) {
                      throw new GraphQLError('Empty AccessToken');
                    }

                    // Retry the failed request
                    const subscriber = {
                      next: observer.next.bind(observer),
                      error: observer.error.bind(observer),
                      complete: observer.complete.bind(observer),
                    };

                    forward(operation).subscribe(subscriber);
                  } catch (err) {
                    observer.error(err);
                  }
                })();
              }
            );

            return observable;
        }
      }
    }

    if (networkError) console.log(`[Network error]: ${networkError}`);
  }
);

const client = new ApolloClient({
  link: ApolloLink.from([errorLink, authLink, httpLink]),
  cache: new InMemoryCache(),
});

// Request a refresh token to then stores and returns the accessToken.
const refreshToken = async () => {
  try {
    const refreshResolverResponse = await client.mutate<{
      refreshToken: AccessToken;
    }>({
      mutation: REFRESH_TOKEN,
    });

    const accessToken = refreshResolverResponse.data?.refreshToken.accessToken;
    localStorage.setItem('accessToken', accessToken || '');
    return accessToken;
  } catch (err) {
    localStorage.clear();
    throw err;
  }
};
```

It distinguishes if a request is for `refreshToken` or not through  `operation.operationName`.
The point is that you can implement the retrying request logic in `onError` with `Observable`.
Return an `Observable` object in `onError` then in the function, get a new access token and retry a request using [forward](https://www.apollographql.com/docs/react/api/link/introduction/#the-forward-function)
Make sure the order of links is right as you want.

---

You can see the result as a gif image and code in [this repository](https://github.com/hsk-kr/apollo-graphql-refresh-token).

That's it, I hope it'll be helpful for someone.

Happy Coding!