1
votes

I've searched for answers but I haven't seen talk about this in the context of using Flutter, Firebase, and Hasura GraphQL together.

Working in Flutter, utilizing a Firebase authentication backend, I am acquiring a user JWT and passing it to a Heroku Hasura GraphQL endpoint (with a query).

Much of the setup and code follows and is inspired by a tutorial at https://hasura.io/blog/build-flutter-app-hasura-firebase-part1/, part 2 and part 3, and flutter graphql documentation at https://github.com/snowballdigital/flutter-graphql.

Firebase is successfully adding new user records to my GraphQL database. Firebase is returning a JWT and it is getting added to the GraphQL AuthLink during the construction of the GraphQL client. This is what the JWT looks like (obscuring personal information):

HEADER:ALGORITHM & TOKEN TYPE

{
    "alg": "RS256",
    "kid": "12809dd239d24bd379c0ad191f8b0edcdb9d3914",
    "typ": "JWT"
}

FULL PAYLOAD:DATA

{
    "iss": "https://securetoken.google.com/<firebase-app-id>",
    "aud": "<firebase-app-id>",
    "auth_time": 1598563214,
    "user_id": "iMovnQvpwuO8HiGOV82cYTmZRM92",
    "sub": "iMovnQvpwuO8HiGOV82cYTmZRM92",
    "iat": 1598635486,
    "exp": 1598639086,
    "email": "<user-email>",
    "email_verified": false,
    "firebase": {
        "identities": {
            "email": [
                "<user-email>"
            ]
        },
        "sign_in_provider": "password"
    }
}

The Decode JWT tool in the HASURA UI shows this error:

"claims key: 'https://hasura.io/jwt/claims' not found"

According to Hasura's documentation something like this should exist in the token:

"https://hasura.io/jwt/claims": {
    "x-hasura-allowed-roles": ["editor","user", "mod"],
    "x-hasura-default-role": "user",
    "x-hasura-user-id": "1234567890",
    "x-hasura-org-id": "123",
    "x-hasura-custom": "custom-value"
  }

In the various tutorials and documentation I'm using, the only place I've seen a need for defining hasura custom claims is in the Firebase cloud function where a user is registered:

exports.registerUser = functions.https.onCall(async (data, context) => {

const email = data.email;
const password = data.password;
const displayName = data.displayName;

if (email === null || password === null || displayName === null) {
    throw new functions.https.HttpsError('unauthenticated', 'missing information');
}

try {
    const userRecord = await admin.auth().createUser({
        email: email,
        password: password,
        displayName: displayName
    });

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

    await admin.auth().setCustomUserClaims(userRecord.uid, customClaims);
    return userRecord.toJSON();

} catch (e) {
    throw new functions.https.HttpsError('unauthenticated', JSON.stringify(error, undefined, 2));
}

});

I'm too new to Firebase and to JWT to understand why the custom claims are not in the token. I assumed Firebase would hand me a JWT with the custom claims embedded and that passing it to the Hasura backend would be enough.

My Heroku Hasura application logs also shows this error:

"Malformed Authorization header","code":"invalid-headers"

Does Firebase needs further configuration in order to hand back the proper claims? Is the missing information in the JWT the same as the server-side error logged as "Malformed Authorization header" or do I need to set additional headers (see Flutter code below).

Here is the GraphQL configuration code in Flutter:

import 'dart:async';

import 'package:dailyvibe/services/jwt_service.dart';
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

class AuthLink extends Link {
  AuthLink()
      : super(
          request: (Operation operation, [NextLink forward]) {
            StreamController<FetchResult> controller;

            Future<void> onListen() async {
              try {
                final String token = JWTSingleton.token;
                operation.setContext(<String, Map<String, String>>{
                  'headers': <String, String>{
                    'Authorization': '''bearer $token'''
                  }
                });
              } catch (error) {
                controller.addError(error);
              }

              await controller.addStream(forward(operation));
              await controller.close();
            }

            controller = StreamController<FetchResult>(onListen: onListen);

            return controller.stream;
          },
        );
}

class ConfigGraphQLClient extends StatefulWidget {
  const ConfigGraphQLClient({
    Key key,
    @required this.child,
  }) : super(key: key);
  final Widget child;

  @override
  _ConfigGraphQLClientState createState() => _ConfigGraphQLClientState();
}

class _ConfigGraphQLClientState extends State<ConfigGraphQLClient> {
  @override
  Widget build(BuildContext context) {
    final cache = InMemoryCache();

    final authLink = AuthLink()
        .concat(HttpLink(uri: 'https://<myapp>.herokuapp.com/v1/graphql'));

    final ValueNotifier<GraphQLClient> client = ValueNotifier(
      GraphQLClient(
        cache: cache,
        link: authLink,
      ),
    );

    return GraphQLProvider(
      client: client,
      child: CacheProvider(
        child: widget.child,
      ),
    );
  }
}
1

1 Answers

0
votes

Answering my own question: custom claims were not in my JWT because I was not calling my registerUser cloud function from Flutter. Now I am, like this:

  final HttpsCallable callable = CloudFunctions.instance
      .getHttpsCallable(functionName: 'registerUser')
        ..timeout = const Duration(seconds: 30);
  await callable.call(<String, dynamic>{
    'email': email,
    'password': password,
  });

(This code snippet is thanks to https://hasura.io/blog/build-flutter-app-hasura-firebase-part3/)

Instead I was using Firebase's createUserWithEmailAndPassword(email: email, password: password) method, which does not create custom claims or trigger the cloud function.