Reputation: 769
Seen similar questions here, but can't figure out what I'm doing wrong. I've got this mutation that refreshes the refreshToken
and the access token
:
type Auth {
user: User!
token: String!
refreshToken: String!
}
type Mutation {
refreshTokens: Auth!
}
The resolver:
Mutation: {
refreshTokens: async (root, args, { req }, info) => {
const result = await issueNewToken(req);
return result;
},
}
Auth helper:
issueNewToken: async (req) => {
try {
const token = req.headers.rt;
if (token) {
const decoded = await jwt.verify(token, jwtRefreshTokenSecret);
let user = await User.findById(decoded.id);
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.");
}
},
And then the function that needs to refresh the tokens if expired:
const refreshTokens = async () => {
const {
data: {
refreshTokens: {
user: { _id: id, address: address },
token: token,
refreshToken: refreshToken,
},
},
} = await renewTokenApiClient.mutate({
mutation: REFRESH_TOKENS,
});
localStorage.setItem("refreshtoken", refreshToken);
localStorage.setItem("token", token);
};
And the mutation:
export const REFRESH_TOKENS = gql`
mutation refreshTokens {
refreshTokens {
user {
_id
address
}
token
refreshToken
}
}
`;
Now, this works in the playground. When I pass the refresh token in the http header, I get the updated token and refreshToken. However, the refreshTokens
function above returns
"Cannot return null for non-nullable field Mutation.refreshTokens."
What am I missing here?
UPD:
The issue is indeed with the operation not getting proper headers. After some documentation and other questions on stackoverflow, I formed the following function, however, the operation is still not getting the headers, even though I seem to set them in the header after refreshing the tokens (which works properly).
import {
ApolloClient,
createHttpLink,
fromPromise,
from,
} from "@apollo/client";
import { onError } from "@apollo/client/link/error";
import { REFRESH_TOKENS } from "../mutations/User";
import { setContext } from "@apollo/client/link/context";
import cache from "../cache";
let isRefreshing = false;
let pendingRequests = [];
const setIsRefreshing = (value) => {
isRefreshing = value;
};
const addPendingRequest = (pendingRequest) => {
pendingRequests.push(pendingRequest);
};
const headerLink = setContext((_, { headers }) => {
const token = localStorage.getItem("refreshtoken");
return {
headers: {
...headers,
rt: token ? token : "",
},
};
});
const renewTokenApiClient = new ApolloClient({
link: from([headerLink, createHttpLink({ uri: "/graphql" })]),
cache,
credentials: "include",
});
const resolvePendingRequests = () => {
pendingRequests.map((callback) => {
console.log(callback);
callback();
});
pendingRequests = [];
};
const refreshTokens = async () => {
const {
data: {
refreshTokens: {
user: { _id: id, address: address },
token: token,
refreshToken: refreshToken,
},
},
} = await renewTokenApiClient.mutate({
mutation: REFRESH_TOKENS,
});
localStorage.setItem("refreshtoken", refreshToken);
localStorage.setItem("token", token);
};
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (const err of graphQLErrors) {
switch (err?.message) {
case "jwt expired":
const oldHeaders = operation.getContext().headers;
if (!isRefreshing) {
setIsRefreshing(true);
const headers = {
...operation.getContext().headers,
};
return fromPromise(
refreshTokens()
.then((refreshTokensResult) => {
let headers = {
//readd old headers
oldHeaders,
//switch out old access token for new one
authorization: `Bearer ${refreshTokensResult.token}`,
rt: refreshTokensResult.refreshToken,
};
operation.setContext({
headers,
});
console.log(operation, "ops");
})
.catch(() => {
resolvePendingRequests();
setIsRefreshing(false);
localStorage.clear();
return forward(operation);
})
).flatMap(() => {
resolvePendingRequests();
setIsRefreshing(false);
console.log(operation);
return forward(operation);
});
} else {
return fromPromise(
new Promise((resolve) => {
addPendingRequest(() => resolve());
})
).flatMap(() => {
return forward(operation);
});
}
}
}
} else if (networkError) console.log(`[Network error]: ${networkError}`);
}
);
export default errorLink;
UPD2: Found the solution and posted the answer.
Upvotes: 0
Views: 876
Reputation: 769
Ended up with this, which works. refreshTokens()
sets the new tokens in the localStorage.
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (const err of graphQLErrors) {
switch (err?.message) {
case "jwt expired":
// Let's refresh token through async request
return new Observable((observer) => {
refreshTokens()
.then(() => {
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
},
}));
})
.then(() => {
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
};
// Retry last failed request
forward(operation).subscribe(subscriber);
})
.catch((error) => {
// No refresh or client token available, we force user to login
localStorage.clear();
isLoggedInVar(false);
observer.error(error);
});
});
}
}
} else if (networkError) console.log(`[Network error]: ${networkError}`);
}
);
Upvotes: 0
Reputation: 320
Looking at the code for the issueNewToken
, there is a path that will return undefined
if the token is not present in the headers. It is not clear how you create the renewTokenApiClient
but a header needs to be specified somewhere to be sent with the request, it is not like a cookie that is sent automatically.
I would suggest checking the network tab to see if the header is present in the request. If it is not, then depending on the client use there are ways to inject it. For apollo client, you will need to create a link to inject it.
import { setContext } from '@apollo/client/link/context';
const authenticationLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
});
Upvotes: 1