Giter VIP home page Giter VIP logo

apollo-link-token-refresh's Introduction

Token Refresh Link npm version

Purpose

An Apollo Link that performs renew expired JWT (access tokens)

Installation

npm install apollo-link-token-refresh --save

Usage

Token Refresh Link is non-terminating link, which means that this link shouldn't be the last link in the composed chain.

Warning

If you need the Apollo v2 support, please use release 0.2.x

import { TokenRefreshLink } from "apollo-link-token-refresh";

const link = new TokenRefreshLink({
    accessTokenField: 'accessToken',
    isTokenValidOrUndefined: (operation: Operation) => Promise<boolean>,
    fetchAccessToken: () => Promise<Response>,
    handleFetch: (accessToken: string, operation: Operation) => void,
    handleResponse? : (operation: Operation, accessTokenField) => response => any,
    handleError? : (err: Error, operation: Operation) => void,
});

Options

The Token Refresh Link takes an object with four options on it to customize the behavior of the link.

name value explanation
accessTokenField? string Default: access_token. This is a name of access token field in response. In some scenarios we want to pass additional payload with access token, i.e. new refresh token, so this field could be the object's name
isTokenValidOrUndefined (operation: Operation, ...args: any[]) => Promise<boolean> Indicates the current state of access token expiration. If the token is not yet expired or the user does not require a token (guest), then true should be returned
fetchAccessToken (...args: any[]) => Promise<Response> Function covers fetch call with request fresh access token
handleFetch (accessToken: string, operation: Operation) => Promise<void> Callback which receives a fresh token from Response. From here we can save token to the storage
handleResponse? (operation, accessTokenField) => response => any This is optional. It could be used to override internal function to manually parse and extract your token from server response
handleError? (err: Error, operation: Operation) => void Token fetch error callback. Allows to run additional actions like logout. Don't forget to handle Error if you are using this option

Example

import { TokenRefreshLink } from 'apollo-link-token-refresh';

link: ApolloLink.from([
  new TokenRefreshLink({
    isTokenValidOrUndefined: async () => !isTokenExpired() || typeof getAccessToken() !== 'string',
    fetchAccessToken: () => {
      return fetch(getEndpoint('getAccessTokenPath'), {
        method: 'GET',
        headers: {
          Authorization: `Bearer ${getAccessToken()}`,
          'refresh-token': getRefreshToken()
        }
      });
    },
    handleFetch: accessToken => {
      const accessTokenDecrypted = jwtDecode(accessToken);
      setAccessToken(accessToken);
      setExpiresIn(parseExp(accessTokenDecrypted.exp).toString());
    },
    handleResponse: (operation, accessTokenField) => response => {
      // here you can parse response, handle errors, prepare returned token to
      // further operations

      // returned object should be like this:
      // {
      //    access_token: 'token string here'
      // }
    },
    handleError: err => {
       // full control over handling token fetch Error
       console.warn('Your refresh token is invalid. Try to relogin');
       console.error(err);
       
       // When the browser is offline and an error occurs we don’t want the user to be logged out of course.
       // We also don’t want to delete a JWT token from the `localStorage` in this case of course.
       if (
         !navigator.onLine ||
         (err instanceof TypeError &&
           err.message === "Network request failed")
       ) {
         console.log("Offline -> do nothing 🍵")
       } else {
         console.log("Online -> log out 👋")

         // your custom action here
         user.logout();
      }       
    }
  }),
  errorLink,
  requestLink,
  ...
])

Custom access token payload

In a scenario where you're using Typescript and your the return of your refresh token is a custom object rather then a single string you can construct the link using a generic type, i.e. :

  new TokenRefreshLink<{token, refreshToken}>({
    // rest omitted for brevity
    handleFetch: newTokens => {
      const {token, refreshToken} = newTokens;
      const accessTokenDecrypted = jwtDecode(token);
      setAccessToken(token);
      setRefreshToken(refreshToken);
      setExpiresIn(parseExp(accessTokenDecrypted.exp).toString());
    },
  })

Storing access token in Redux

If access token is stored in Redux state, operation object allows to reach the state and dispatch needed actions, i.e. :

    new TokenRefreshLink({
        // rest omitted for brevity
        isTokenValidOrUndefined: async (operation) => {
            const { getState } = operation.getContext();
            const accessToken = accessTokenSelector(getState());
            // validate access token and return true/false
        },
        handleFetch: (accessToken, operation) => {
            const { dispatch } = operation.getContext();
            dispatch(setAccessToken(accessToken));
        },
        ...
    });

Context

The Token Refresh Link does not use the context for anything.

apollo-link-token-refresh's People

Contributors

alansikora avatar aminsaedi avatar argo0003 avatar faithfinder avatar googol7 avatar hellcattc avatar icco avatar justenaujokaityterevel avatar masesisaac avatar newsiberian avatar seanonthenet avatar zbraiterman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

apollo-link-token-refresh's Issues

GraphQL based API

Is there an example on how to implement this via a Graph QL API ?

How exactly shoud I get the new acces token in handleResponse function?

I'm trying to extract the new token that is generated when the old one has expired. But when I try to access it, it is undefined:

handleResponse: (operation, accessTokenField) => response => {
    console.log({ ac: response.refreshToken }); // -> {ac: undefined}
},

how should I extract it?

Retry a failed query after token is refreshed

What's the best way to retry a query that 401 failed when the token needs to be refreshed as a result of isTokenValidOrUndefined. I'm also using @apollo/client/link/retry but doesn't seem to handle it. Does anyone have a good example?

consumeQueue on Error

What is the best way to forward the Operation to the next link in case of any error. This might be necessary if I need the original graphql request promise be rejected in case there is an error in refreshing the token.

handleFetch not always needed

If a consumer provides handleResponse, they generally don't need to provide handleFetch:

// ERROR: missing property handleFetch
// even though there is no need for it here
new TokenRefreshLink({
  fetchAccessToken: () => {
    const refreshedSession = refreshSession();
    const fakeReq = new Request('uri://fake');
    return Object.assign(fakeReq, refreshedSession);
  },
  handleResponse: (_, __) => response => response.text()
      .then(JSON.parse)
      .then(res => {
        saveToken(res.accessToken);
      }),
  },
  handleError: console.error,
});

Expected behavior:
The constructor should enforce that at least one of handleFetch and handleResponse are present

Access Operation inside isTokenValidOrUndefined and handleFetch/handleError

When using this library we ran into issue of not being able to access Redux state or dispatch actions inside the TokenRefreshLink because Operation was not accessible inside of it. We had to tweek it a bit so that these functions would pass Operation object.

I believe this use case is quite common because if we store accessToken inside Redux and after refreshing it want to change the state, there's no possible way to that without having access to context.

This requires minimal changes to the existing code though helps a lot, so it would be nice to have this functionality as part of this library.

export type HandleResponse = (operation: Operation, accessTokenField: string) => void;
export type HandleError = (operation: Operation, err: Error) => void;
export type IsTokenValidOrUndefined = (operation: Operation, ...args: any[]) => boolean;
if (!this.fetching) {
            this.fetching = true;
            this.fetchAccessToken()
                .then(this.handleResponse(operation, this.accessTokenField))
                .then(body => {
                    const token = this.extractToken(body);

                    if (!token) {
                        throw new Error('[Token Refresh Link]: Unable to retrieve new access token');
                    }
                    return token;
                })
                .then(payload => this.handleFetch(operation, payload))
                .catch(error => this.handleError(operation, error))
                .finally(() => {
                    this.fetching = false;
                    this.queue.consumeQueue();
                });
        }

TypeError: response.text is not a function

Hey, this is my first time doing something like this. Sorry in advance if I miss something important out.

So basically I am trying to implement this package into my project and I ran into the following issue:
TypeError: response.text is not a function at eval (tokenRefreshLink.js?bf0b:14)

My refreshLink is looking like this:

const refreshLink = new TokenRefreshLink({
  accessTokenField: 'accessToken',
  isTokenValidOrUndefined: () => accessTokenExpired(),
  fetchAccessToken: () => fetchAccessToken(),
  handleFetch: (accessToken: string) => setAccessToken(accessToken),
  handleError: (err: Error) => {
    console.log('An error occurred');
    console.log(err);
  },
});

And my helper functions are looking like this:

let accessToken: string = '';

export const setAccessToken = (incomingToken: string): void => {
  if (!incomingToken || incomingToken === null) {
    return console.log('accessToken not found.');
  }

  accessToken = incomingToken;
};

export const getAccessToken = (): string => {
  return accessToken;
};

export const accessTokenExpired = (): boolean => {
  let isValid: boolean = undefined;

  const token: any = getAccessToken().valueOf();

  if (!token || token === null) {
    return isValid === false;
  }

  let decodedToken: any = jwt_decode(token);
  console.log('Decoded Token');

  let expiryDate = new Date(decodedToken.exp * 1000);
  let exp = decodedToken.exp * 1000;
  let dateNow = Date.now();

  // console.log(expiryDate);

  if (dateNow >= exp) {
    isValid === false;
  } else {
    isValid === true;
  }

  return isValid;
};
`

`
export const fetchAccessToken = async (): Promise<any> => {
  const payload = {
    operationName: 'updateTokens',
    variables: {},
    query:
      'mutation updateTokens {\n  updateTokens {\n    accessToken\n    __typename\n  }\n}\n',
  };
  return fetch('http://localhost:4000/graphql'!, {
    method: 'POST',
    credentials: 'include',
    body: JSON.stringify(payload),
    headers: {
      'Content-Type': 'application/json; charset=utf-8',
      Accept: 'application/json',
    },
  }).then(async (res) => {
    const response = await res.json();
    console.log('fetchAccessToken');
    console.log(response.data.updateTokens.accessToken);

    return response.data.updateTokens.accessToken;
  });
};

I don't know if you also need to see my apolloClient, but I will insert it here just in case:

export const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
  ssrMode: true,
  cache: new InMemoryCache(),
  link: from([refreshLink, authLink, errorLink, httpLink]),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
    },
  },
});

I know that my accessTokenExpired and fetchAccessToken functions are working properly, because I am getting a new accessToken when I refresh the page. The accessTokenExpired function also correctly detects whether the token is expired or not. So I'm clueless right now what I should do to prevent this error from happening. Sorry for the inconvenience again.

parseAndCheckResponse assumes too much about underlying response

const parseAndCheckResponse = (request, accessTokenField) => (response: Response) => {

Assumes response has a .text() method, but this is not always the case.

  1. Using request-promise module will return data directly without a response.
  2. Using ApolloClient would return a data object without response object.

I think this should either not assume anything or let user configure that handler.

Incompatible with apollo v3.0.0-rc.0

Argument of type 'TokenRefreshLink' is not assignable to parameter of type 'ApolloLink | RequestHandler'.
Type 'TokenRefreshLink' is not assignable to type 'ApolloLink'.
Types of property 'split' are incompatible.

Please update.

Concurrent requests

How you handle concurrent requests in order to avoid multiple token renewal process to be triggered?

isTokenValidOrUndefined operation

Any chance the operation object can be passed to the isTokenValidOrUndefined function?

I am looking to read from the cache at this point. For example, to read my token from the cache.

Invalid Refresh Token

Using this setup I got the Invalid Refresh Token error despite refresh operation succeed and access token returned.

const refreshLink = new TokenRefreshLink({
    isTokenValidOrUndefined: () => isTokenValid(localStorage.getItem('access-token')),
    fetchAccessToken: () => {
        console.log('Fetching new access token...')
        return fetch(`${API_BASE}/token/refresh`, {
            method: 'GET',
            headers: {
                Authorization: `Bearer ${localStorage.getItem('refresh-token')}`
            }
        })
    },
    handleFetch: accessToken => {
        // this seems never get fires because error
    },
    handleResponse: (operation, accessTokenField) => response => {
        response.json().then(data => {
            localStorage.setItem('access-token', data[accessTokenField])
        })
    },
    handleError: err => {
        console.error(err)
    }
})

New access token returned in this format:

{
    "access_token": "xxx.xxx.xxx"
}

Got this error when refresh operation fires:

Invalid Refresh Token: TypeError: Cannot read property 'data' of undefined
    at TokenRefreshLink._this.extractToken (tokenRefreshLink.js?bf0b:59)
    at eval (tokenRefreshLink.js?bf0b:91)

Debugging shows body is undefined. Please advise.

Cannot read property 'data' of undefined

I keep getting this error:
Cannot read property 'data' of undefined

I tried printing out both the body and _this.accessTokenField in the tokenRefreshLink.js file

console.log('_this.accessTokenField', _this.accessTokenField)
console.log('body', body)

the output is _this.accessTokenField access_token and body undefined respectively.

That body variable is accessed with a data property in the tokenRefreshLink.js but body is undefined. The response I send from the backend is an object with the access_token property with a string value which is the refresh token.

This is how I fetch the access token:

...

fetchAccessToken: () => {
      return fetch('http://localhost:4000/refresh_token', {
        method: 'POST',
        credentials: 'include',
      });
    }
...

Any ideas? What is expected from that body variable that is undefined and keeps failing in my implementation?

React Warnings

I am getting warnings in my terminal when I run my application saying,

(
WARNING in ./node_modules/apollo-link-token-refresh/lib/queuing.js

[1] Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):

Failed to parse source map from /node_modules/apollo-link-token-refresh/src/queuing.ts file: Error: ENOENT: no such file or directory,
open \client\node_modules\apollo-link-token-refresh\src\tokenRefreshLink.ts
)

(
WARNING in ./node_modules/apollo-link-token-refresh/lib/tokenRefreshLink.js
[1] Module Warning (from ./node_modules/source-map-loader/dist/cjs.js

Failed to parse source map from \client\node_modules\apollo-link-token-refresh\src\tokenRefreshLink.ts'

file: Error: ENOENT: no such file or directory, open client\node_modules\apollo-link-token-refresh\src\tokenRefreshLink.ts
)

Everything is working in development, but will this be a problem in production?

Check JWT token exp date

Hi,

I checked some tutorials and went through issues here on repo and what I see is that in isTokenValidOrUndefined() function every one basically do the same check, which is

const { exp } = jwtDecode<JwtPayload>(token);
      if (exp) {
        return Date.now() < exp * 1000;
      } else {
        return false
      }

seems fine, but is it really?
I can just change date on my OS and to add 10 hours to it,
so when token is expired, I can easily manipulate date on my OS and token works again?

it's not exactly a problem with this repo, but I think that isTokenValidOrUndefined() should be async, then I would be able to get server date/time and check whether token expired on not,

this package doesn't work

It doesn't work when using ApolloLink.
when I request something, the tokenRefreshLink start and end in isTokenValidOrUndefined()
fetchAccessToken, handleFetch and other function don't work

const tokenRefreshLink = new TokenRefreshLink({
	accessTokenField: "accessToken",
	isTokenValidOrUndefined: async () => {
		const token = (await SecureStore.getItemAsync("jwt")) ?? getAccessToken()
		try {
			if (token) {
				const { exp } = jwtDecode(token)
				if (Date.now() >= exp * 1000) {
					console.log("expire!")
					return false
				} else {
					return true
				}
			} else {
				return false
			}
		} catch (error) {
			throw Error(error)
		}
	},
	fetchAccessToken: async () => {
		console.log("fetchAccessToken")
		uri = httpsUrlOption("refreshToken")
		const result = await fetch(uri, {
			method: "POST",
			credentials: "include",
		})
		console.log(result.json)
		return result.json
	},
	handleFetch: async (accessToken) => {
		console.log("handleFetch", accessToken)
		setAccessToken(accessToken)
		await SecureStore.setItemAsync("jwt", accessToken)
	},
	handleResponse: (operation, accessTokenField) => (response) => {
		console.log("handleResponse", response)
	},
	handleError: (err) => {
		console.warn("Your refresh token is invalid. Try to relogin")
		console.error(err)
	},
})

const clientState = (cache) =>
	new ApolloClient({
		link: ApolloLink.from([
			tokenRefreshLink,
                        ...)]
              })

Order with restLink

Hey,
Thanks for such a great package!
Could you please tell me what order it should be for the next links?

    link: ApolloLink.from([
        retryLink as unknown as ApolloLink,
        errorLink,
        authLink,
        restLink as unknown as ApolloLink,
        schemaLink,
    ]),

Many thanks!

handleFetch is not working

console.log output:

  • handleResponse:
  • err msg from graphql: access Token error
  • index.js:1 Error: [Token Refresh Link]: Unable to retrieve new access token
    at tokenRefreshLink.ts:179

return res.send({accessToken: });
Response from calling /api refresh_token {"accessToken":""}
The accessTokenField is set as the same.
However, the handleFetch function is being skipped, please help

import ReactDOM from "react-dom";
import jwtDecode, { JwtPayload } from 'jwt-decode';
import { ApolloProvider, ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import { TokenRefreshLink } from 'apollo-link-token-refresh';
import { getAccessToken, setAccessToken } from './accessToken';
import { App } from './App'

const httpLink = new HttpLink({
  uri: `http://localhost:4000/graphql`,
  credentials: 'include'
});

const authLink = setContext((_, { headers }) => {
  const token = getAccessToken()
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : ''
    }
  };
});

const refreshTokenLink = new TokenRefreshLink({
  accessTokenField: 'accessToken',
  isTokenValidOrUndefined: () => {
    const token = getAccessToken()

    if (!token) return false;

    try {
      const { exp } = jwtDecode<JwtPayload>(token);
      if (exp) {
        return Date.now() < exp * 1000;
      } else {
        return false
      }
    } catch {
      return false;
    }
  },
  fetchAccessToken: () => {
    return fetch(`http://localhost:4000/refresh_token`, {
      method: 'POST',
      credentials: 'include'
    });
  },
  handleFetch: accessToken => {
    console.log(`handleFetch: ${accessToken}`);
    setAccessToken(accessToken);
  },
  handleResponse: (operation, accessTokenField) => {
    console.log(`handleResponse: ${getAccessToken()}`);
    console.log(operation);
  },
  handleError: err => {
    console.error(err);
  }
});

const client = new ApolloClient({
  link: ApolloLink.from([
    refreshTokenLink,
    onError(({ graphQLErrors, networkError }) => {

    }),
    authLink,
    httpLink
  ]),
  cache: new InMemoryCache({})
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById("root")
);```


"dependencies": {
    "@apollo/client": "^3.5.5",
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "@types/jest": "^26.0.15",
    "@types/node": "^12.0.0",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "apollo-link-token-refresh": "^0.3.3",
    "graphql": "^16.0.1",
    "jwt-decode": "^3.1.2",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^6.0.2",
    "react-scripts": "4.0.3",
    "typescript": "^4.1.2",
    "web-vitals": "^1.0.1"
  }

Cannot read property 'text' of undefined

Hello,
as I`m trying to refresh access token ani it runs with an error "response.text is not a function".
The flow is following - on login user receives access token (it is kept in apollo state reactive variable - accessTokenVAR()) and refresh token (it is kept in cookies). When access token expires it is supposed to be refreshed with refresh token.

This is my appolo-client code:

let apolloClient;
const cookies = new Cookies();
const isAuthenticated = cookies.get('token');
const httpLink = new HttpLink({
  uri: 'https://localhost3000/graphql', 
  credentials: 'same-origin' 
});

const authLink = setContext((_, { headers }) =>
  ({
    headers: {
      'content-type': 'application/json',
      ...headers,
      Authorization: accessTokenVAR().length !== 0 ? `Bearer ${accessTokenVAR()}` : null
    }
  })
);

const refreshLink = new TokenRefreshLink({
  accessTokenField: 'token',
  isTokenValidOrUndefined: () => {
    if (!isAuthenticated) {
      return true;
    }
    if (accessTokenVAR() && jwt.decode(accessTokenVAR())?.exp * 1000 > Date.now()) {
      return true;
    }
  },
  fetchAccessToken: async () => {
    if (!isAuthenticated) {
      return null;
    }
        const response =  await fetch('https://localhost/auth/refresh', {
          method: 'POST',
          headers: {
            'content-type': 'application/json'
          },
          body: JSON.stringify( {refresh_token: isAuthenticated} )
        })
       return response.json()
  },
  handleFetch: (token) => {
    accessTokenVAR(token);
  },
  handleError: (error) => {
    console.error('Cannot refresh access token:', error);
  }
});

function createApolloClient() {
  return new ApolloClient({
    cache,
    ssrMode: typeof window === 'undefined',
    link: authLink.concat(refreshLink).concat(httpLink)
  });
}

HandleFetch function is never triggered so my variable accessTokenVAR is not refreshed with newly received token.
It is obvious I'm missunderstanding smth.

Make response object a generic for other fetch mechanisms

Currently, fetchAccessToken must return a Promise<Response> object. This works fine if the token is fetched using fetch via REST. However, my API also uses GraphQL for refreshing tokens (as I believe many GraphQL APIs would do) and I have configured a separate Apollo client with a few helper functions that execute the refresh token mutation.

I have managed to make it work by overriding the type checker (the actual logic of the code is not opinionated about this if you implement handleResponse), but I think it would be better if I could specify a custom type so I don't have to do this.

In general, users may obtain their refresh token as a result of calling a variety of functions that might not return a response promise, and I think it could be useful to make the return a generic.

I am happy to submit a pull request for this if you would consider accepting this @newsiberian .

Why 'isTokenValidOrUndefined' must return true if token is undefined?

Sorry if it's obvious, but i would like to know why the option isTokenValidOrUndefined must return true if the token is undefined.

Maybe i'm missing the point but this function should not check if the token exists and it's valid instead of returning true if the token is missing? However the documentation is pretty clear about it and i'm confused:

https://github.com/newsiberian/apollo-link-token-refresh#options
If token not yet expired or user doesn't have a token (guest) true should be returned

https://github.com/newsiberian/apollo-link-token-refresh#example
isTokenValidOrUndefined: () => !isTokenExpired() || typeof getAccessToken() !== 'string'

Thanks in advance

Type 'TokenRefreshLink' is missing the following properties from type 'ApolloLink': onError, setOnError ts(2739)

Hi there,

Just wondering if anyone has encountered this issue.

When trying to use TokenRefreshLink, I get the following error when building up the link property..

Type 'TokenRefreshLink' is missing the following properties from type 'ApolloLink': onError, setOnError ts(2739)
const refreshTokenLink = new TokenRefreshLink({
// etc... <omitted here for brevity>
// set proper values for 
accessTokenField:
isTokenValidOrUndefined:
fetchAccessToken:
handleFetch:
handleError:

I also build up a basic set of other links

Then...

// compose the links in sequence 
const link = from([refreshTokenLink, errorLink, authLink, httpLink]);
                              ^^^
                          error occurs here

Using the following

"apollo-link-token-refresh": "^0.2.7",
"@apollo/client": "^3.0.0-beta.44",
"@apollo/link-context": "^2.0.0-beta.3",
"@apollo/link-error": "^2.0.0-beta.3",

I have found the following similar type of issue over here and also here.

I have tried the following:

  • downgrading to an older version of "@apollo/client": "^3.0.0-beta.37". (didn't solve it)
  • upgrading to a newer version "@apollo/client": "^3.0.0-beta.48", (made it worse, got additional errors)
  • casting (new TokenRefreshLink({ etc as unknown) as ApolloLink

Argument type is not assignable to parameter type TokenRefreshLink.Options<string>

Hi! In apollo-link-token-refresh v.0.3.1 I see the warning message:

Argument type {fetchAccessToken: (function(): Promise<Response>),
 handleError: (function(): null), isTokenValidOrUndefined: (function(): boolean), 
handleResponse: (function(): function(...[any]=))}
 is not assignable to parameter type TokenRefreshLink.Options<string>

Type Error with the new Apollo client

Hi, thanks for your efforts.
When i use the link in my client config, i get this eror:

Type 'TokenRefreshLink<string>' is not assignable to type 'ApolloLink | RequestHandler'.
  Type 'TokenRefreshLink<string>' is not assignable to type 'ApolloLink'.
    Types of property 'split' are incompatible.
      Type '(test: (op: import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/types").Operation) => boolean, left: import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | im...' is not assignable to type '(test: (op: import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/types").Operation) => boolean, left: import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core...'.
        Types of parameters 'left' and 'left' are incompatible.
          Type 'import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/types").RequestHandler' is not assignable to type 'import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/types").RequestHandler'.
            Type 'ApolloLink' is not assignable to type 'ApolloLink | RequestHandler'.
              Type 'import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink' is not assignable to type 'import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink'.
                Types of property 'split' are incompatible.
                  Type '(test: (op: import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/types").Operation) => boolean, left: import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core...' is not assignable to type '(test: (op: import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/types").Operation) => boolean, left: import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | im...'.
                    Types of parameters 'left' and 'left' are incompatible.
                      Type 'import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/types").RequestHandler' is not assignable to type 'import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/types").RequestHandler'.
                        Type 'ApolloLink' is not assignable to type 'ApolloLink | RequestHandler'.
                          Type 'import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink' is not assignable to type 'import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink'.
                            Types of property 'split' are incompatible.
                              Type '(test: (op: import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/types").Operation) => boolean, left: import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | im...' is not assignable to type '(test: (op: import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/types").Operation) => boolean, left: import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core...'.
                                Types of parameters 'right' and 'right' are incompatible.
                                  Type 'import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | import("/home/shando/lernito-exam/frontend/node_modules/@apollo/client/link/core/types").RequestHandler | undefined' is not assignable to type 'import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | import("/home/shando/lernito-exam/frontend/node_modules/apollo-link-token-refresh/node_modules/@apollo/client/link/core/types").RequestHandler | undefined'.
                                    **Type 'ApolloLink' is not assignable to type 'ApolloLink | RequestHandler | undefined'.ts(2322)**

I literally copy pasted the example in in the docs

Not accepting multiple layer of object in response

I've been encountering errors while using this library. If it is a direct single object or one-dimensional object it is working. However, if the response from API consists of multiple layers of an object, the handleFetch will not work.

{ "tokenDetails": { "data": { "token": "token", "refreshtoken": "refreshtoken" } } }

I am using fetch query same as the tutorial.

Webpack warnings when building the package - Failed to parse source map

Hi there!

Running into a few warnings when the webpack builds our application. This does not stop the package from running, but rather a log warning. The warnings relate to this issue: #54

(see below)

WARNING in ../../../node_modules/apollo-link-token-refresh/lib/queuing.js
Module Warning (from ../../../node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/Users/SergiyFomin/work-space/QualityNow/node_modules/apollo-link-token-refresh/src/queuing.ts' file: Error: ENOENT: no such file or directory, open '/Users/SergiyFomin/work-space/QualityNow/node_modules/apollo-link-token-refresh/src/queuing.ts'

WARNING in ../../../node_modules/apollo-link-token-refresh/lib/tokenRefreshLink.js
Module Warning (from ../../../node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/Users/SergiyFomin/work-space/QualityNow/node_modules/apollo-link-token-refresh/src/tokenRefreshLink.ts' file: Error: ENOENT: no such file or directory, open '/Users/SergiyFomin/work-space/QualityNow/node_modules/apollo-link-token-refresh/src/tokenRefreshLink.ts'

I looked into it and the problem appears to be because this repo stores typescript-compiled files in the same directory as the output bundle package - the ./lib. Hence the tsc-compiled files throw these warnings.

I can see we need those compiled files to generate bundle, but we only need typescript definitions with the bundle.

almost as if we need to:

  • run tsc compile to get files for the bundle
  • bundle them
  • run tsc compile again just to store the typescript definitions (emitDeclarationOnly: true)
./lib
  bundle.umd.js/
    bundle.umd.js.map
  queuing.d.ts.map
  queuing.js/                                     <-- throws warnings
    queuing.js.map                            <-- throws warnings
  tokenRefreshLink.d.ts.map
  tokenRefreshLink.js/                 <-- throws warnings
    tokenRefreshLink.js.map        <-- throws warnings  

Question

do you see the need to keep the .js files in the ./lib, or typescript definitions together with the bundle should be enough?

How to Use Token Refresh Link w/ Context API?

First, thank you for the hard work on this package - it's been essential to the apps I've built.

A common practice is to use React context API to login, logout, and retrieve accessTokens for users.

With this package, I have opted to use just store my accessToken inside a global variable like so:

// authToken.js

let _accessToken = "";

export const setAccessToken = (accessToken = "") => {
  _accessToken = accessToken;
};

export const getAccessToken = () => {
  return _accessToken;
};

This typically works as I can rely on having separate screens to redirect the user to after login / logout to trigger component re-renders.

However, I am now using a modal for login / logout so I'd like to use the Context API to trigger re-rendering of the header and other components after the user logs in / out.

Since we are outside of a react component, the useContext can't be used here.

// index.js

const client = new ApolloClient({
  cache: new InMemoryCache({}),
  defaultOptions,
  link: ApolloLink.from([
    new TokenRefreshLink({
      accessTokenField: "token",
      isTokenValidOrUndefined: () => {
        const token = getAccessToken(); // how to use context here instead of global variable?
        if (!token) {
          return true;
        }

        try {
          const { exp } = jwtDecode(token);
          if (Date.now() >= exp * 1000) {
            return false;
          } else {
            return true;
          }
        } catch {
          return false;
        }
      },
     
      // ... rest of links

Is there a way I can provide the accessToken for the refreshLink and use Context API ?

Check if client is online in react native?

if (
         !navigator.onLine ||
         (error instanceof TypeError &&
           error.message === "Network request failed")
       ) {
         console.log("Offline -> do nothing 🍵")
       } else {
         console.log("Online -> log out 👋")

         // your custom action here
         user.logout();
      }       

this doesn't work with react native.

isTokenValidOrUndefined fails with a async function

To access the expiration of my token, I need to use a promise. Therefore, an async function is necessary

For example:

const link = new ApolloLink.from([
  new TokenRefreshLink({
    isTokenValidOrUndefined: async () => {
      const tokenExpiration = await getToken()
      return isValid(tokenExpiration)
    }
  })
])

Although this function returns false, fetchAccessToken does not run to refresh the token

fetchAccessToken tightly couple to Fetch API

I am trying to use apollo-link-token-refresh in a typescript project. In this project, we are using a library function to make the access token refresh request, and that library function does not expose the Fetch API underneath:

interface RefreshedSession {
  accessToken: string;
  expirationTimestamp: number;
}
async function refreshSession: Promise<RefreshedSession> {
  const resp = await fetch(...);
  ...
  return {
    accessToken,
    expirationTimestamp;
  };
}

I believe I should be able to use apollo-link-token-refresh without casting to any or using any other type hacks. Perhaps TokenRefreshLink should have an optional type parameter.

new TokenRefreshLink<RefreshedSession>({
  fetchAccessToken: refreshSession,
  handleResponse: (_, __) => response => response.accessToken,
  handleFetch: saveToken,
  handleError: console.error,
  },
});

Workaround: force my session response to be of a modified Response type:

new TokenRefreshLink({
  fetchAccessToken: async () => {
    const refreshedSession = await refreshSession();
    const fakeReq = new Request('uri://fake');
    return Object.assign(fakeReq, refreshedSession);
  },
  handleResponse: (_, __) => (response: Response & RefreshedSession) => response.accessToken,
  handleFetch: saveToken,
  handleError: console.error,
  },
});

graphql peer dependenceies

Hi,

When I run npm install with my current app I get the following error:

npm ERR! While resolving: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/graphql
npm ERR! graphql@"^16.2.0" from the root project
npm ERR! peer graphql@"^14.0.0 || ^15.0.0 || ^16.0.0" from @apollo/[email protected]
npm ERR! node_modules/@apollo/client
npm ERR! @apollo/client@"^3.4.7" from the root project
npm ERR! peer @apollo/client@"^3.0.0" from [email protected]
npm ERR! node_modules/apollo-link-token-refresh
npm ERR! apollo-link-token-refresh@"^0.3.3" from the root project
npm ERR! 1 more (subscriptions-transport-ws)
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer graphql@"^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" from [email protected]
npm ERR! node_modules/apollo-link-token-refresh
npm ERR! apollo-link-token-refresh@"^0.3.3" from the root project
npm ERR!

General Info:
npm: v7.21.1
node: v16.8.9

I believe this issue is not specific to my own app.

Do you think we can update the peer dependencies for apollo-link-token-refresh to adopt the latest version of graphql?

thanks,
Liusha

Prevent forward if error

Context:
If the refresh token has expired, it will trigger handleError.
Inside, this condition confirms that the refresh token has expired and calls logout, clearing sessions local cache.

// If the refresh token expired, log the user out 
if (err.message === 'Unknown or invalid refresh token.') {
  await logout();
}


// Links
link: from([errorLink, refreshLink, authLink, httpLink]),

This will trigger a new request, which will fail since the token couldn't be refreshed.
If we could manually call forward, or just return a boolean (true forward, false terminate), it would give the developer more power to control the error handle.

If there's another way to accomplish this, please let me know.
Thank you

Cannot get the new AccessToken in AuthLink after updating it in handleFetch

Every example using this package shows that setAccessToken can be called in handleFetch and the new Access Token can be used in the next link.

But my AuthLink is retrieving the old accessToken

In the next link, I am setting the new state in handleFetch

Refresh Link

export const useRefreshLink = () => {
  const { getAccessToken, setAccessToken, refreshToken } = useAuth()

  return new TokenRefreshLink<AuthPayload>({
    accessTokenField: 'payload',
    isTokenValidOrUndefined: () => {
      const accessToken = getAccessToken()
      console.log('Old token', accessToken)
      if (!accessToken) return true
      return isTokenValid(accessToken)
    },
    fetchAccessToken: async () => {
      const refreshResponse = await refreshToken()
      const fakeResponse = new Response()
      return { ...fakeResponse, ...refreshResponse }
    },
    handleResponse: () => (res: Response & AuthPayload) => {
      const payload = {
        accessToken: res.accessToken,
        profile: res.profile,
      }
      return { payload }
    },
    handleFetch: (payload) => {
      const { accessToken, profile } = payload
      console.log(`New token @ ${new Date()}`, accessToken)
      setAccessToken(accessToken)
    },
    handleError: (err) => {
      console.log('err', err)
    },
  })
}

In htis link however, I am getting the old token

Auth Link

export const useAuthLink = () => {
  const { getAccessToken } = useAuth()

  return new ApolloLink((operation, forward) => {
    const accessToken = getAccessToken()

    if (accessToken) {
      console.log(`Auth Token @ ${new Date()}`, accessToken)
      operation.setContext(({ headers = {} }) => ({
        headers: {
          ...headers,
          authorization: `bearer ${accessToken}`,
        },
      }))
    }

    return forward(operation)
  })
}

This useEffect shows that the token is updated AFTER I retrieve the accessToken in my AuthLink

useAuth

export const useAuthLogic = (): AuthContextType => {
  const [accessToken, setAccessToken] = useState<string | null>(null)

  useEffect(() => {
    console.log('AccessToken changed @ ', new Date())
  }, [accessToken])

  const getAccessToken = () => accessToken
  // ...
}

Here is my dev console showing that the token is updated AFTER I try to retrieve the new token in my AuthLink

Old token eyJhbGciOiJI[...]hW1LC4
Token expired: true
New token eyJhbGciOiJI[...]j4yb4
Auth Token: eyJhbGciOiJI[...]hW1LC4
AccessToken changed

Error: not authenticated
    at new ApolloError2 (index.ts:71:5)
    at QueryManager.ts:246:15
    at both (asyncMap.ts:30:30)
    at asyncMap.ts:19:47
    at new Promise (<anonymous>)
    at Object.then (asyncMap.ts:19:16)
    at Object.next (asyncMap.ts:31:39)
    at notifySubscription (module.js:132:18)
    at onNotify (module.js:176:3)
    at SubscriptionObserver2.next (module.js:225:5)

I am using React 18
and this is my links order

const appClient = getApolloClient('app', [refreshLink, authLink, httpLink])

Confusing guidelines

Indicates the current state of access token expiration. If token not yet expired or user doesn't have a token (guest) true should be returned

Please correct me if I'm wrong but for isTokenValidOrUndefined method, it should return true if a user doesn't "need" a token. Contrary to the method description, if they don't have a token (but do need one) then false should be returned.

Am I right in thinking a more explanatory name for the method might be isTokenValid or tokenRefreshNotNeeded (bit weird). Would be better to flip the response and call it: shouldTokenRefresh

or at least the description should be:
"Indicates the current state of access token expiration. If the token is not yet expired or the user does not require a token (guest), then true should be returned"

Sorry - I just was really confused by this for ages 😕

How to update local state in handleFetch

Hello, thank you so much for your apollo-link.

I'd like to know what you would advise to use if I want to store the new token in my local state.
I use the useContext hook in my app, but it's not possible to use hooks outside of function components. What's the best way to update my local state within handeFetch?

Here is where it sits in my code, for now, I simply store the token in local storage, but I'd like to store it in my React context: https://github.com/paritytech/polkassembly/blob/master/front-end/src/index.tsx#L42

Error with apollo version 3.0

Seems this package is throwing an error with Apollo client version 3.0, but its working with 2.6 though.

Error: queuing.js:47 Uncaught (in promise) TypeError: request.operation.toKey is not a function

customize error handler

If server returns non ok response, I can see only error in console. Make customize error handler, please.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.