newsiberian / apollo-link-token-refresh

Apollo Link that performs access tokens (JWT) renew
MIT License
337 stars 45 forks source link

this package doesn't work #26

Closed ganzp87 closed 4 years ago

ganzp87 commented 4 years ago

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,
                        ...)]
              })
mikeislearning commented 4 years ago

Hey there @ganzp87 - I believe that isTokenValidOrUndefined is not an async function, so I think it'll always return a falsy value

ganzp87 commented 4 years ago

@mikeislearning - Is there no way to use SecureStore or something async function ? I just want to keep the token in the secure-store.

mikeislearning commented 4 years ago

@ganzp87

Here's how I set mine up.

  1. I use SecureStorage alongside React Context. As you know, the SecureStorage stores the jwt token in memory, even if the user closes the app.
  2. Upon the app loading, I check the value of what's in SecureStorage. Once, it's verified as valid, I add it to Context. We'll call this Auth Context
  3. I also have another time tracking context value, because my tokens only last 1 hour. We'll call this Time Context.
  4. Inside the isTokenValidOrUndefined function is where I synchronously check the value of Time Context. If it is greater than 1 hour ago, I update Time Context, Auth Context, and the Secure Storage value all the handleFetch function

You could also use redux instead of context if you're more familiar with that.

Lemme know if that helps

ganzp87 commented 4 years ago

@mikeislearning

I also use SecureStore in my Auth Context. However anyway the context using storage is a async function. You mean that I have to add another setting something like sync time context for this package.
But I think your suggestion is inefficient, because the reason I use this package is just for convenience. My jwt token last just 5 min. Therefore the token in every request should be checked.

Now I am not using this. I made a similiar async function with TokenRefreshLink for using SecureStore. I check a token is valid and if token expired, reissue and get by myself.

I might not understand your suggestion well. but that seems to be Inefficient.

The one thing that I want to know is why token validating process should be sync function and if it is async function, I wonder if it can cause serious problems

mikeislearning commented 4 years ago

Hey @ganzp87 the longer I use the package, the less success I'm having with it. I was able to get it working like a year ago with Apollo Client v2, and I think that's what this was built for. I'll keep plugging away to try and get it to work. Lemme know if you have any luck

ganzp87 commented 4 years ago

@mikeislearning This is what I made. I want it to help you

new ApolloClient({
    link: ApolloLink.from([
        requestLink,
                ...
        )]
})

let isRefreshing = false
let pendingRequests = []

const resolvePendingRequests = () => {
    pendingRequests.map((callback) => callback())
    pendingRequests = []
}

const requestLink = new ApolloLink((operation, forward) => {
    const { operationName } = operation
    const operNoNeededToken = ['operation name that you don't want to refetch a token']
    if (!operNoNeededToken.includes(operationName)) {
        let forward$
        if (!isRefreshing) {
            isRefreshing = true
            forward$ = fromPromise(
                refetchAccessToken(operationName)
                    .then((token) => {
                        resolvePendingRequests()
                        return token
                    })
                    .catch((error) => {
                        pendingRequests = []
                        return
                    })
                    .finally(() => {
                        isRefreshing = false
                    })
            ).filter((value) => Boolean(value))
        } else {
            forward$ = fromPromise(
                new Promise((resolve) => {
                    pendingRequests.push(() => resolve())
                })
            )
        }
        return forward$.flatMap(() => forward(operation))
    } else {
        return new Observable((observer) => {
            let handle
            Promise.resolve(operation)
                .then(() => {
                    handle = forward(operation).subscribe({
                        next: observer.next.bind(observer),
                        error: observer.error.bind(observer),
                        complete: observer.complete.bind(observer),
                    })
                })
                .catch(observer.error.bind(observer))
            return () => {
                if (handle) handle.unsubscribe()
            }
        })
    }
})

export const refetchAccessToken = async () => {
    const _this = this
    const token = (await SecureStore.getItemAsync("jwt")) ?? getAccessToken()
    if (token) {
        const { exp } = jwtDecode(token)
        if (Date.now() <= exp * 1000) {
            accessToken = token
            return token
        }
    }
    const result = await fetch(httpsUrlOption("refresh_token"), {
        method: "POST",
        credentials: "include",
    })

    try {
        const { accessToken: aToken, ok } = await result.json()
        console.log(aToken, ok)
        if (!ok) {
            console.log("logout because the token is invalid")
        } else {
            await SecureStore.setItemAsync("isLoggedIn", "true")
            await SecureStore.setItemAsync("jwt", aToken)
            const { exp } = jwtDecode(aToken)
            if (Date.now() <= exp * 1000) {
                accessToken = aToken
                return aToken
            }
        }
        setAccessToken(aToken)
        return aToken
    } catch (error) {
        throw Error(error)
    }
}

export const setAccessToken = (s) => {
    accessToken = s
}

export const getAccessToken = () => {
    return accessToken
}