Correct implementation of refreshtoken with ApolloClient

Viewed 634

I am trying to refresh the authentication token when it is near the expiration time, however I end up with an endless loop. The expectation is that the code that checks the expiration time in App.js should work and reset the auth token and the refresh token, however is starts looping endlessly.

I have the following Auth helper functions:

const jwt = require("jsonwebtoken");
const User = require("../models/User");
const { AuthenticationError } = require("apollo-server-express");
const config = require("config");
const jwtSecret = config.get("jwt_secret");
const jwtRefreshTokenSecret = config.get("jwt_refresh_token");

const authFunctions = {
  checkSignedIn: async (req, requireAuth = true) => {
    const header = req.headers.authorization;
    if (header) {
      const token = header.replace("Bearer ", "");
      const decoded = jwt.verify(token, jwtSecret);
      console.log(decoded);
      let user = await User.findById(decoded.id);
      if (!user) {
        throw new AuthenticationError("Invalid user credentials.");
      }
      return user;
    }
    if (requireAuth) {
      throw new AuthenticationError("You must be logged in.");
    }
    return null;
  },
  issueToken: async (user) => {
    let token = "Bearer " + (await authFunctions.createToken(user));
    let refreshToken = await authFunctions.createToken(user, 100);
    return { token, refreshToken };
  },
  issueNewToken: async (req) => {
    try {
      const token = req.headers.refreshtoken;
      if (token) {
        const decoded = await jwt.verify(token, jwtRefreshTokenSecret);
        let user = await User.findById(decoded.id);
        console.log(user);
        if (!user) {
          throw new AuthenticationError("No user found.");
        }
        let tokens = await authFunctions.issueToken(user);
        return { ...tokens, user };
      }
    } catch (err) {
      throw new AuthenticationError("Invalid Refresh Token.");
    }
  },
  createToken: async ({ id, address }, expiresIn = 60) => {
    let secret = expiresIn === 60 ? jwtSecret : jwtRefreshTokenSecret;
    return await jwt.sign({ id, address }, secret, { expiresIn });
  },
};

module.exports = authFunctions;

The schema:

const { gql } = require("apollo-server");

const typeDefs = gql`
  scalar Date
  scalar MongoId

  type User {
    _id: MongoId!
    address: String!
    createdAt: Date
  }

  type Auth {
    user: User!
    token: String!
    refreshToken: String!
  }

  input LoginInput {
    address: String!
  }

  type Query {
    refreshTokens: Auth!
  }

  type Mutation {
    createOrGetUser(loginInput: LoginInput): Auth!
  }
`;

module.exports = typeDefs;

The resolvers:

const User = require("../../models/User");
const {
  issueToken,
  issueNewToken,
  checkSignedIn,
} = require("../../middleware/Auth");

const resolvers = {
  Query: {
    refreshTokens: async (root, args, { req }, info) =>
      await issueNewToken(req),
  },

  Mutation: {
    createOrGetUser: async (root, args, { req }, info) => {
      try {
        const existingUser = await User.findOne({
          address: args.loginInput.address,
        })
          .populate({
            path: "orders",
            model: Order,
          })
          .exec();

        if (existingUser) {
          let tokens = await issueToken(existingUser);

          return {
            user: existingUser,
            ...tokens,
          };
        }
        const user = await new User({
          address: args.loginInput.address,
        });
        const result = await user.save();

        let tokens = await issueToken(result);

        return {
          user,
          ...tokens,
        };
      } catch (err) {
        throw err;
      }
    },
  },
};

module.exports = resolvers;

And the App.js

import React, { Fragment } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import {
  ApolloClient,
  ApolloLink,
  createHttpLink,
  useReactiveVar,
  concat,
} from "@apollo/client";
import { ApolloProvider } from "@apollo/client/react";
import { accessTokenVar, isLoggedInVar } from "./cache";
import cache from "./cache.js";
import jwtDecode from "jwt-decode";

// Styling
import "./styles/App.css";

// Components
import Routes from "./components/routing/Routes";
import Navbar from "./components/navbar/Navbar";
import { REFRESH_TOKENS } from "./queries/User";

const httpLink = createHttpLink({
  uri: "/graphql",
});

const refreshTokens = () => {
  return client.query({ query: REFRESH_TOKENS }).then((response) => {
    console.log(response);
    // Need to set the token to cache here, but the query doesn't get executed it seems

    //accessTokenVar(response.data)
    return;
  });
};

const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  let token = accessTokenVar();
  if (token) {
    token = token.replace("Bearer ", "");
    const { exp } = jwtDecode(token);
    console.log(exp);
    // Refresh the token a minute early to avoid latency issues
    const expirationTime = exp - 30;
    if (Date.now() / 1000 >= expirationTime) {
      refreshTokens(); // this keeps going on a loop
    }
  }

  operation.setContext(({ headers = {} }) => ({
    headers: {
      ...headers,
      authorization: token ? token : "",
    },
  }));

  return forward(operation);
});

const client = new ApolloClient({
  cache,
  link: concat(authMiddleware, httpLink),
  connectToDevTools: true,
  credentials: "include",
});

const App = () => {
  const accessToken = useReactiveVar(accessTokenVar);
  const isLoggedIn = useReactiveVar(isLoggedInVar);

  //let isLoggedIn;
  accessToken ? isLoggedInVar(true) : isLoggedInVar(false);

  return (
    <ApolloProvider client={client}>
      <Router>
        <Fragment>
          <Navbar isLoggedIn={isLoggedIn} />
        </Fragment>
      </Router>
    </ApolloProvider>
  );
};

export default App;

Note: When createOrGetUser is executed the first time, the flow works. Then, I wait for 30 seconds and send a protected query, after which it gets in an endless loop.

Overall, I feel this flow is broken somehow, but I can't figure out what exactly. Would appreciate any help with this!

0 Answers
Related