How to implement auto refresh token in graphql for jwt based authentication?

WitVault picture WitVault · Jul 10, 2018 · Viewed 8.3k times · Source

I am trying to figure out this scenario for my JWT based authentication in Apollo based graphql server (2.0) .

Basically after login a user gets accessToken and refreshToken from server.

AccessToken gets expired after certain period of time and server sends an error message indicating that token expired (TokenExpiredError) and then client need to communicate with server for new accessToken via passing refreshToken.

Flow is as following -

  1. TokenExpiredError occurs
  2. Get that error on client side
  3. Queue all requests with old accessToken(so that server is not flooded with too many refreshToken calls and many accessTokens are generated by server)
  4. Call refreshToken api on graphql server to get new accessToken
  5. update accessToken for all authorised calls with new accessToken
  6. Logout user incase refreshToken itself is expired
  7. Prevent any kind of race condition b/w calls

I have already implemented refreshToken mutation on client side but can't figure out about when error occurs stop all requests -> request new token -> make all pending request again and if refresh token is expired logout user.

Answer

WitVault picture WitVault · Jul 20, 2018

I followed this approach to solve my problem finally

Posting my approach for others

// @flow
import { ApolloLink, Observable } from 'apollo-link';
import type { ApolloClient } from 'apollo-client';
import type { Operation, NextLink } from 'apollo-link';

import { refreshToken2, getToken } from './token-service';
import { GraphQLError } from 'graphql';

export class AuthLink extends ApolloLink {
     tokenRefreshingPromise: Promise<boolean> | null;

injectClient = (client: ApolloClient): void => {
    this.client = client;
};

refreshToken = (): Promise<boolean> => {
    //if (!this.tokenRefreshingPromise) this.tokenRefreshingPromise = refreshToken(this.client);
    if (!this.tokenRefreshingPromise) this.tokenRefreshingPromise = refreshToken2();
    return this.tokenRefreshingPromise;
};

setTokenHeader = (operation: Operation): void => {
    const token = getToken();
    if (token) operation.setContext({ headers: { authorization: `Bearer ${token}` } });
};

request(operation: Operation, forward: NextLink) {
    // set token in header
    this.setTokenHeader(operation);
    // try refreshing token once if it has expired
    return new Observable(observer => {
        let subscription, innerSubscription, inner2Subscription;
        try {
            subscription = forward(operation).subscribe({
                next: result => {
                    if (result.errors) {
                        console.log("---->", JSON.stringify(result.errors))
                        for (let err of result.errors) {
                            switch (err.extensions.code) {
                              case 'E140':
                                console.log('E140', result)
                                observer.error(result.errors)
                                break;
                              case 'G130':
                                    this.refreshToken().then(response => {
                                        if (response.data && !response.errors) {
                                            this.setTokenHeader(operation);
                                            innerSubscription = forward(operation).subscribe(observer);
                                        } else {
                                            console.log("After refresh token", JSON.stringify(response));
                                            observer.next(response)
                                        }
                                    }).catch(console.log);
                                break;
                            }
                          }
                    } 
                    observer.next(result)

                  },
                complete: observer.complete.bind(observer),
                error: netowrkError => {
                    observer.error(netowrkError);
                  }
                },
            });
        } catch (e) {
            observer.error(e);
        }
        return () => {
            if (subscription) subscription.unsubscribe();
            if (innerSubscription) innerSubscription.unsubscribe();
            if (inner2Subscription) inner2Subscription.unsubscribe();
        };
    });
}
}