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,
),
);
}
}