Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

no AuthHeaders for Websocket-Connection, Apolloclient Offix Angular/Ionic Hasura #775

Closed
baststar opened this issue Aug 21, 2020 · 2 comments

Comments

@baststar
Copy link

baststar commented Aug 21, 2020

Hi,
i don't know if this is the correct repository for my question?!

Success:
Queries/Mutations over httpUrl: 'https://hasura.xyz.de/v1/graphql' works -> AuthHeaders available (see getAuthContextProvider)
👍

Problem:
Subscriptions over wsUrl: 'wss://hasura.xyz.de/v1/graphql' doesnt work:
Error: "cannot start as connection_init failed with : Missing Authorization header in JWT authentication mode"

So my scenario:
As backend i'm using Hasura with jwt-auth within a docker-container.

docker-compose.yml

version: '3.6'

services:
  caddy:
    image: caddy/caddy
    ports:
      - 443:443
    volumes:
      -
        type: bind
        source: ./Caddyfile
        target: /etc/caddy/Caddyfile
      - ./caddydata:/data
      - ./caddy_certs:/root/.caddy
    depends_on:
      - hasura

  hasura:
    image: hasura/graphql-engine:v1.3.0
    ports:
      - "8080:8080"
    depends_on:
      - postgresd
    restart: always
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://hasurauser:xyz@postgresd:5432/hasuradb?sslmode=disable
      ## enable the console served by server
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
      ## enable debugging mode. It is recommended to disable this in production
      HASURA_GRAPHQL_DEV_MODE: "true"
      HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
      ## uncomment next line to set an admin secret
      HASURA_GRAPHQL_ADMIN_SECRET: xyz
      HASURA_GRAPHQL_JWT_SECRET: '{"type":"RS256","jwk_url": "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com", "audience": "xyz", "issuer": "https://securetoken.google.com/xyz"}'

  postgresd:
    image: postgres:12
    ports:
      - "5432:5432"
    restart: unless-stopped
    environment:
      - POSTGRES_USER=root
      - POSTGRES_PASSWORD=xyz
      - POSTGRES_DB=root
      - PGDATA=/var/lib/postgresql/hasuradb
    volumes:
      - ./docker_postgres_init.sql:/docker-entrypoint-initdb.d/docker_postgres_init.sql
      - ./hasuradb:/var/lib/postgresql/hasuradb

Caddyfile:

hasura.xyz.de {
  reverse_proxy hasura:8080
}

docker_postgres_init.sql

CREATE USER hasurauser WITH PASSWORD 'xyz';

-- HASURA
CREATE DATABASE hasuradb
    WITH 
    OWNER = hasurauser
    ENCODING = 'UTF8'
    LC_COLLATE = 'en_US.utf8'
    LC_CTYPE = 'en_US.utf8'
    TABLESPACE = pg_default
    CONNECTION LIMIT = -1;

CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE SCHEMA IF NOT EXISTS hdb_catalog;
CREATE SCHEMA IF NOT EXISTS hdb_views;
ALTER SCHEMA hdb_catalog OWNER TO hasurauser;
ALTER SCHEMA hdb_views OWNER TO hasurauser;
GRANT SELECT ON ALL TABLES IN SCHEMA information_schema TO hasurauser;
GRANT SELECT ON ALL TABLES IN SCHEMA pg_catalog TO hasurauser;
GRANT USAGE ON SCHEMA public TO hasurauser;
GRANT ALL ON ALL TABLES IN SCHEMA public TO hasurauser;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO hasurauser;
GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO hasurauser;

Firebase Functions to set Hasura-User-Claims and to add the Firebase-User into the Hasura-DB:

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import UserRecord = admin.auth.UserRecord;
import {Pool} from 'pg';

const pool = new Pool({
    user: 'hasurauser',
    password: 'xyz',
    database: 'hasuradb',
    host: 'xyz',
    port: 5432,
    ssl: false
});

admin.initializeApp(functions.config().firebase);

export const Registration = functions.auth.user().onCreate((user: UserRecord) => {

    const customClaims = {
        'https://hasura.io/jwt/claims': {
            'x-hasura-default-role': 'user',
            'x-hasura-allowed-roles': ['user'],
            'x-hasura-user-id': user.uid
        }
    };

    const userid = user.uid || "no_uid";
    const username = user.displayName || "";
    const useremail = user.email || "";

    const query = {
        name: 'insert_user',
        text: 'insert into users(id, name, email) values ($1, $2, $3) RETURNING *',
        values: [userid, username, useremail],
    }

    pool.query(query).then(() => {
        console.log('successfully added user to postgres');
    }).catch((error) => {
        console.log('error adding user to postgres');
        pool.end().catch((err) => {
            console.log(err);
        });
    });
    return admin.auth().setCustomUserClaims(user.uid, customClaims);
});

As Frontend i'm using Angular with following packages:
apollo-client@2.6.10
offix-client-boost@0.15.5

For Authentication i'm using FirebaseAuth incl. Hasura-Claims

Inspired by https://github.com/aerogear/datasync-starter/tree/0.11.0
these are the relevant code-snippets:

graphql.module.ts

import {APP_INITIALIZER, NgModule} from '@angular/core';
import {VoyagerService} from './services/sync/voyager.service';
import {AuthService} from './services/auth.service';

export const apolloClientFactory = (aeroGear: VoyagerService, authService: AuthService) => {
    return () => aeroGear.createApolloClient(authService);
};

@NgModule({
    providers: [
        {
            provide: APP_INITIALIZER,
            useFactory: apolloClientFactory,
            deps: [VoyagerService, AuthService],
            multi: true
        }
    ]
})

export class GraphQLModule {}

voyager.service.ts:

import {
    ApolloOfflineClient,
    ConflictListener,
    createClient,
    ApolloOfflineStore,
    OffixBoostOptions
} from 'offix-client-boost';
...

@Injectable({
    providedIn: 'root'
})
/**
 * Service provides Apollo Voyager client
 */
export class VoyagerService {

    private _apolloClient: ApolloOfflineClient;
    private _offlineStore: ApolloOfflineStore;

    constructor(public dialog: MatDialog) {
    }

    get apolloClient(): ApolloOfflineClient {
        return this._apolloClient;
    }

    get offlineStore(): ApolloOfflineStore {
        return this._offlineStore;
    }

    public async createApolloClient(authService: AuthService): Promise<void> {

        const options: OffixBoostOptions = {
            httpUrl: 'https://hasura.xyz.de/v1/graphql',
            wsUrl: 'wss://hasura.xyz.de/v1/graphql',
            conflictListener: new ConflictLogger(this.dialog),
            fileUpload: true,
            mutationCacheUpdates: actionCacheUpdates,
        };

        options.authContextProvider = await authService.getAuthContextProvider();
        const offlineClient = await createClient(options);
        this._offlineStore = offlineClient.offlineStore;
        this._apolloClient = offlineClient;
    }
}

auth.service.ts

import {Injectable} from '@angular/core';
import {AngularFireAuth} from '@angular/fire/auth';
import {take} from 'rxjs/operators';
import {AuthContextProvider} from 'offix-client-boost';
...

@Injectable({
    providedIn: 'root'
})
export class AuthService {

    ...

    constructor(private angularFireAuth: AngularFireAuth) {
           ...
    }

    ...
    public async getAuthContextProvider(): Promise<AuthContextProvider | undefined> {

        let token = '';

        try {
            token = await this.angularFireAuth.idToken.pipe(take(1)).toPromise();
        } catch (error) {
            try {
                const localstorageUser: LocalstorageUser = JSON.parse(window.localStorage.getItem('user'));
                token = localstorageUser.token;
            } catch (error) {
                console.log(error);
            }
        }

        return async () => {
            if (!token) {
                throw new Error('No token available');
            }
            return {
                headers: {
                    Authorization: `Bearer ${token}`
                }
            };
        };
    }
}

Payload of the token from firebaseAuth used by hasura:

{
  "https://hasura.io/jwt/claims": {
    "x-hasura-default-role": "user",
    "x-hasura-allowed-roles": [
      "user"
    ],
    "x-hasura-user-id": "xyz"
  },
  "iss": "https://securetoken.google.com/xyz",
  "aud": "xyz",
  "auth_time": 1598033114,
  "user_id": "xyz",
  "sub": "xyz",
  "iat": 1111111111,
  "exp": 1598036714,
  "email": "sasdf@asdf.de",
  "email_verified": false,
  "firebase": {
    "identities": {
      "email": [
        "sasdf@asdf.de"
      ]
    },
    "sign_in_provider": "password"
  }
}
@wtrocki
Copy link
Contributor

wtrocki commented Aug 26, 2020

As for subscriptions we should provide different handling for apollo-link-ws.
Check offix boost (we have ability to pass those there:

https://github.com/aerogear/offix/blob/master/packages/offix/client-boost/src/links/WebsocketLink.ts#L10-L15

@kingsleyzissou
Copy link
Contributor

Hi @baststar, we have made the decision to move away from the Apollo client and we have decided to deprecate our Offix packages and release our Datastore. I will be closing this issue, but if you have any questions, please feel free to let us know.

You can see the docs for the updated datastore here:
https://offix.dev/docs/getting-started

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants