I'm trying to set up a refresh token strategy to refresh JWT in angular 9 with GraphQL and apollo client when my first request returns a 401.
I have set up a new angular module for graphql where I'm creating my apolloclient. Everything works great even with authenticated requests but I need to have my normal refresh token strategy work as well (re-make and return the original request after refresh token cycle completes). I have found only a few resources to help with this and I've gotten really close - the only thing I'm missing is returning the observable from my refresh token observable.
Here is the code that would think should work:
import { NgModule } from '@angular/core';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { AuthenticationService } from './authentication/services/authentication.service';
import { ApolloLink } from 'apollo-link';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular';
import { onError } from 'apollo-link-error';
export function createApollo(httpLink: HttpLink, authenticationService: AuthenticationService) {
const authLink = new ApolloLink((operation, forward) => {
operation.setContext({
headers: {
Authorization: 'Bearer ' + localStorage.getItem('auth_token')
}
});
return forward(operation);
});
const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) =>
{
if (message.toLowerCase() === 'unauthorized') {
authenticationService.refreshToken().subscribe(() => {
return forward(operation);
});
}
}
);
}
});
return {
link: errorLink.concat(authLink.concat(httpLink.create({ uri: 'http://localhost:3000/graphql' }))),
cache: new InMemoryCache(),
};
}
@NgModule({
exports: [ApolloModule, HttpLinkModule],
providers: [
{
provide: APOLLO_OPTIONS,
useFactory: createApollo,
deps: [HttpLink, AuthenticationService]
}
]
})
export class GraphqlModule { }
I know that my request is working the second time because if I log out the result from the forward(operation) observable inside my authenticationService subscription, I can see the results after the initial 401 failure.
if (message.toLowerCase() === 'unauthorized') {
authenticationService.refreshToken().subscribe(() => {
return forward(operation).subscribe(result => {
console.log(result);
});
});
}
the above shows me the data from the original request, but it's not being passed up to my component that originally called the graphql.
I'm far from an expert with observables but I'm thinking I need to do some kind of map (flatmap, mergemap etc) to make this return work correctly, but I just don't know.
Any help would be greatly appreciated
TIA
EDIT #1: this is getting me closer as it's now actually subscribing to my method in AuthenticationService (I see results in the tap())
const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
if (graphQLErrors[0].message.toLowerCase() === 'unauthorized') {
return authenticationService.refreshToken()
.pipe(
switchMap(() => forward(operation))
);
}
}
});
I'm now seeing this error being thrown:
core.js:6210 ERROR TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
EDIT #2: Including screenshot of onError() function signature:
EDIT #3 Here is the final working solution in case someone else comes across this and needs it for angular. I don't love having to update my service method to return a promise, and then convert that promise into an Observable - but as @Andrei Gătej discovered for me, this Observable is from a different namespace.
import { NgModule } from '@angular/core';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { AuthenticationService } from './authentication/services/authentication.service';
import { ApolloLink } from 'apollo-link';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular';
import { onError } from 'apollo-link-error';
import { Observable } from 'apollo-link';
export function createApollo(httpLink: HttpLink, authenticationService: AuthenticationService) {
const authLink = new ApolloLink((operation, forward) => {
operation.setContext({
headers: {
Authorization: 'Bearer ' + localStorage.getItem('auth_token')
}
});
return forward(operation);
});
const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
if (graphQLErrors.some(x => x.message.toLowerCase() === 'unauthorized')) {
return promiseToObservable(authenticationService.refreshToken().toPromise()).flatMap(() => forward(operation));
}
}
});
return {
link: errorLink.concat(authLink.concat(httpLink.create({ uri: '/graphql' }))),
cache: new InMemoryCache(),
};
}
const promiseToObservable = (promise: Promise<any>) =>
new Observable((subscriber: any) => {
promise.then(
value => {
if (subscriber.closed) {
return;
}
subscriber.next(value);
subscriber.complete();
},
err => subscriber.error(err)
);
});
@NgModule({
exports: [ApolloModule, HttpLinkModule],
providers: [
{
provide: APOLLO_OPTIONS,
useFactory: createApollo,
deps: [HttpLink, AuthenticationService]
}
]
})
export class GraphqlModule { }