Search code examples
angulargraphqlauthorizationapollo

Angular Apollo WebSocketLink with dynamic header authorization


i'm consuming a graphql subscription; initially the header has an empty authorization token, after login a token variable is generated in the localstorage. What I want to do is that automatically after logging in, the token variable in the subscription header is updated, I have tried as the documentation says but the token is never updated and it always sends it empty.

I need the header in WebSocketLink to be dynamic in connectionParams

this is my GraphQLModule file, hope someone can help me...

import { NgModule } from '@angular/core';
import { ApolloClientOptions, InMemoryCache, split ,ApolloLink} from '@apollo/client/core';
import { APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import {WebSocketLink} from '@apollo/client/link/ws';
import {getMainDefinition} from '@apollo/client/utilities';
import { environment } from 'environments/environment';
import * as CryptoJS from 'crypto-js';  
import { setContext } from '@apollo/client/link/context';
import {onError} from '@apollo/client/link/error';
import Swal from 'sweetalert2';
import { CoreTranslationService } from '@core/services/translation.service';
import { locale as en } from 'app/main/pages/i18n/en';
import { locale as es } from 'app/main/pages/i18n/es';

const uri = environment.apiUrl; // <-- endpoint1 gql
const urisub = environment.apiSubs;// <-- endpoint2 gql



function operationFilter(operationName:string):boolean{  
  if(operationName!="checkToken") return true;    
  else return false;      //and the others
 }

export function createApollo(httpLink: HttpLink,_coreTranslationService: CoreTranslationService): ApolloClientOptions<any> {

  _coreTranslationService.translate(en, es);

  const basic = setContext((operation, context) => ({
    headers: {
      Accept: 'charset=utf-8'
    }
  }));

  const auth = setContext((operation, context) => {
      const token = localStorage.getItem('token');
      
  
      if (token === null) {
        return {};
      } else {
        let token_decrypt= CryptoJS.AES.decrypt(token,environment.key_decrypt).toString(CryptoJS.enc.Utf8)
        return {
          headers: {
            Authorization: `Bearer ${token_decrypt}`
          }
        };
      }
    });

  const http = httpLink.create({
    uri(operation){ 
      return operationFilter(operation.operationName)? uri : urisub;
    } 
  });
 
  const ws = new WebSocketLink({
    uri:`ws://localhost:3005/subscriptions`,
    options:{      
      lazy: true,
      reconnect: true,
      connectionParams: async () => {
        const token =  localStorage.getItem('token');
        let token_decrypt= null
        if (token) {
           token_decrypt= CryptoJS.AES.decrypt(token,environment.key_decrypt).toString(CryptoJS.enc.Utf8) 
        }               
        return {                 
            Authorization: token ? `Bearer ${token_decrypt}` : "",         
        }
      },
    }
  });


  const error = onError(({networkError, graphQLErrors}) => {
        if (graphQLErrors) {            
          
          graphQLErrors.map(({
                  message,
                  locations,
                  path,
                  extensions
              }) =>{

                console.log('error graph', localStorage.getItem('token'));
                
                if (extensions && localStorage.getItem('token')!=null) {
                  if (extensions.exception.status==401) {

                    Swal.fire({
                      icon: 'error',
                      title: _coreTranslationService.instant('ErrorSub.Title'),
                      text: _coreTranslationService.instant('ErrorSub.Message'),
                      timer: 6000,
                      timerProgressBar: true,
                      showCancelButton: false, 
                      showConfirmButton: false,
                      allowOutsideClick: false,
                      allowEscapeKey: false
                    }); 
                    
          
                    setTimeout(() => {  
                      localStorage.clear();                 
                      window.location.href = "/pages/authentication/login-v2";                       
                    }, 7000);
                    
                    
                  }
                }
                
              }
              
          );
      }
      if (networkError) {
          console.log(`[Network error]: ${networkError}`);
      }
  })
  

  const _split = split(
    ({query}) => {
      const data = getMainDefinition(query);
      return (
        data.kind === 'OperationDefinition' && data.operation === 'subscription'
      );
    },
    ws,
    //http
    auth.concat(http)
  )

  
  
  

  const cleanTypeName = new ApolloLink((operation, forward) => {
    if (operation.variables) {
      const omitTypename = (key, value) => (key === '__typename' ? undefined : value);
      operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename);
    }
    return forward(operation).map((data) => {
      return data;
    });
  });

  


  const link =ApolloLink.from([cleanTypeName, basic, error, _split]);
 
  
  
  return {
    link: link,
    cache: new InMemoryCache({
      addTypename: false,
    }),
    defaultOptions: {
      watchQuery: {
        fetchPolicy: 'network-only',
        //errorPolicy: 'all',
      },
      query: {
          fetchPolicy: 'network-only',
      },
      mutate: {
        
      }
    },
  };
}

@NgModule({
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, CoreTranslationService],
    },
  ],
})
export class GraphQLModule {
}

Solution

  • I share my solution I don't know if it is the best option but it is the one that worked for me, on the client side:

    graphql.module.ts

    import { NgModule } from '@angular/core';
    import { ApolloClientOptions, InMemoryCache, split ,ApolloLink, Operation, makeVar} from '@apollo/client/core';
    import { Apollo, APOLLO_OPTIONS } from 'apollo-angular';
    import { HttpLink } from 'apollo-angular/http';
    import {WebSocketLink} from '@apollo/client/link/ws';
    import {getMainDefinition} from '@apollo/client/utilities';
    import { environment } from 'environments/environment';
    import * as CryptoJS from 'crypto-js';  
    import { setContext } from '@apollo/client/link/context';
    import {onError} from '@apollo/client/link/error';
    import Swal from 'sweetalert2';
    import { CoreTranslationService } from '@core/services/translation.service';
    import { locale as en } from 'app/main/pages/i18n/en';
    import { locale as es } from 'app/main/pages/i18n/es';
    import { SubscriptionClient } from 'subscriptions-transport-ws';
    import { HttpClientModule } from '@angular/common/http';
    import {HttpLinkModule} from 'apollo-angular-link-http';
    
    
    const uri = environment.apiUrl; 
    const urisub = environment.apiSubs;
    
    
    
    function operationFilter(operationName:string):boolean{  
      if(operationName!="checkToken") return true;  
      else return false;      
    
    
    @NgModule({
    
      exports: [
        HttpClientModule,
        HttpLinkModule
      ]
    })
    export class GraphQLModule {
        public Clientws: any;
        public subscriptionClient: SubscriptionClient = null;
    
       constructor(apollo: Apollo, httpLink: HttpLink,_coreTranslationService: CoreTranslationService){
    
        _coreTranslationService.translate(en, es);
    
        const getIdToken = () => localStorage.getItem('token') || null;
    
        const auth = setContext((operation, context) => {      
            return {
              headers: {
                Authorization: `Bearer ${getIdToken()}`
              }
            };      
        });
    
        const http = httpLink.create({
            uri(operation){ 
              return operationFilter(operation.operationName)? uri : urisub;
            } 
        });
    
    
          
          const wsClient = new SubscriptionClient(`ws://localhost:3005/subscriptions`, {
            reconnect: true,
            connectionParams: async () => {                
              return {                 
                  Authorization:`Bearer ${getIdToken()}`,         
              }
            },
          })
    
    
    
          this.Clientws = wsClient
    
          const ws = new WebSocketLink(wsClient)
    
          
    
          this.subscriptionClient = (<any>ws).subscriptionClient;
    
            const error = onError(({networkError, graphQLErrors}) => {
    
                          
            if (graphQLErrors  && getIdToken()!=null && getIdToken()!='') {            
              
              graphQLErrors.map(({
                      message,
                      locations,
                      path,
                      extensions
                  }) =>{
    
                    if (extensions) {
                      if (extensions.exception.status==401 && getIdToken()!=null && getIdToken()!='') {                      
                        
    
                        Swal.fire({
                          icon: 'error',
                          title: _coreTranslationService.instant('ErrorSub.Title'),
                          text: _coreTranslationService.instant('ErrorSub.Message'),
                          timer: 6000,
                          timerProgressBar: true,
                          showCancelButton: false, 
                          showConfirmButton: false,
                          allowOutsideClick: false,
                          allowEscapeKey: false
                        }); 
                        
              
                        setTimeout(() => {  
                          localStorage.clear();                 
                          window.location.href = "/pages/authentication/login-v2";                       
                        }, 7000);
                        
                        
                      }
                    }
                    
                  }
                  
              );
          }
          if (networkError) {
              console.log(`[Network error]:`, networkError);
          }
      })
    
          const _split = split(
            ({query}) => {
              const data = getMainDefinition(query);
              return (
                data.kind === 'OperationDefinition' && data.operation === 'subscription'
              );
            },
            ws,
            auth.concat(http)
          )
    
    
            const cleanTypeName = new ApolloLink((operation, forward) => {
              if (operation.variables) {
                const omitTypename = (key, value) => (key === '__typename' ? undefined : value);
                operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename);
              }
              return forward(operation).map((data) => {
                return data;
              });
            });
    
              const basic = setContext((operation, context) => ({
                headers: {
                  Accept: 'charset=utf-8'
                }
              }));
    
      
    
    
      const link =ApolloLink.from([cleanTypeName, basic, error, _split]);
    
        apollo.create({
          link:link,
          cache: new InMemoryCache({
            addTypename: false,
          }),
          defaultOptions: {
            watchQuery: {
              fetchPolicy: 'network-only',
              //errorPolicy: 'all',
            },
            query: {
                fetchPolicy: 'network-only',
            },
            mutate: {
              
            }
          },
        });
       }
    }
    

    login.component.ts

    constructor(
        .....,
        private gqlModule: GraphQLModule,
        .....
      ){
         .......
      }
    
    onSubmit(event) { //submit login form 
    ......
    
    //if service login response true
    this.gqlModule.Clientws.close(false,false); // this closes the websocket and automatically it reconnects with the new information in the header
    //Note .. If you do not have the reconnection in true, you need to connect again manually    
    ......
    }
    

    on the server side :

    configure subscription options

    onConnect: (connectionParams, webSocket, context) => {   
             
    
              if (connectionParams['Authorization'] != null && connectionParams['Authorization'] != '') {
    
                if (connectionParams['Authorization'].split(" ")[1]!= null && connectionParams['Authorization'].split(" ")[1]!= '' && connectionParams['Authorization'].split(" ")[1]!= 'null') { 
                 return { authorization: connectionParams['Authorization'] };
                }
                
              } 
              
            },