A tutorial for using Firebase to add authentication and authorization to a realtime Hasura app
- Set up Hasura and create the data model using the Hasura Console
- Set up Authentication
- Build the React web app
- React
- GraphQL
- SQL

{
"type":"RS256",
"jwk_url": "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com",
"audience": "<firebase-project-id>",
"issuer": "https://securetoken.google.com/<firebase-project-id>"
}

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CREATE TABLE "public"."loved_language" ( | |
"name" text NOT NULL, | |
"user_id" text NOT NULL, | |
CONSTRAINT loved_language_pkey PRIMARY KEY (name, user_id), | |
CONSTRAINT loved_language_programming_language_fky FOREIGN KEY (name) REFERENCES programming_language(name), | |
) |





This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const functions = require("firebase-functions"); | |
const admin = require("firebase-admin"); | |
admin.initializeApp(functions.config().firebase); | |
// On sign up. | |
exports.processSignUp = functions.auth.user().onCreate(user => { | |
const customClaims = { | |
"https://hasura.io/jwt/claims": { | |
"x-hasura-default-role": "user", | |
"x-hasura-allowed-roles": ["user"], | |
"x-hasura-user-id": user.uid | |
} | |
}; | |
return admin | |
.auth() | |
.setCustomUserClaims(user.uid, customClaims) | |
.then(() => { | |
// Update real-time database to notify client to force refresh. | |
const metadataRef = admin.database().ref("metadata/" + user.uid); | |
// Set the refresh time to the current UTC timestamp. | |
// This will be captured on the client to force a token refresh. | |
return metadataRef.set({ refreshTime: new Date().getTime() }); | |
}) | |
.catch(error => { | |
console.log(error); | |
}); | |
}); |
{
"rules": {
"metadata": {
"$uid": {
".read": "auth != null && auth.uid == $uid"
}
}
}
}

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import firebase from "firebase/app"; | |
import "firebase/auth"; | |
import "firebase/database"; | |
import React, { useState, useEffect } from "react"; | |
import App from "./App"; | |
const provider = new firebase.auth.GoogleAuthProvider(); | |
// Find these options in your Firebase console | |
firebase.initializeApp({ | |
apiKey: "xxx", | |
authDomain: "xxx", | |
databaseURL: "xxx", | |
projectId: "xxx", | |
storageBucket: "xxx", | |
messagingSenderId: "xxx" | |
}); | |
export default function Auth() { | |
const [authState, setAuthState] = useState({ status: "loading" }); | |
useEffect(() => { | |
return firebase.auth().onAuthStateChanged(async user => { | |
if (user) { | |
const token = await user.getIdToken(); | |
const idTokenResult = await user.getIdTokenResult(); | |
const hasuraClaim = | |
idTokenResult.claims["https://hasura.io/jwt/claims"]; | |
if (hasuraClaim) { | |
setAuthState({ status: "in", user, token }); | |
} else { | |
// Check if refresh is required. | |
const metadataRef = firebase | |
.database() | |
.ref("metadata/" + user.uid + "/refreshTime"); | |
metadataRef.on("value", async (data) => { | |
if(!data.exists) return | |
// Force refresh to pick up the latest custom claims changes. | |
const token = await user.getIdToken(true); | |
setAuthState({ status: "in", user, token }); | |
}); | |
} | |
} else { | |
setAuthState({ status: "out" }); | |
} | |
}); | |
}, []); | |
const signInWithGoogle = async () => { | |
try { | |
await firebase.auth().signInWithPopup(provider); | |
} catch (error) { | |
console.log(error); | |
} | |
}; | |
const signOut = async () => { | |
try { | |
setAuthState({ status: "loading" }); | |
await firebase.auth().signOut(); | |
setAuthState({ status: "out" }); | |
} catch (error) { | |
console.log(error); | |
} | |
}; | |
let content; | |
if (authState.status === "loading") { | |
content = null; | |
} else { | |
content = ( | |
<> | |
<div> | |
{authState.status === "in" ? ( | |
<div> | |
<h2>Welcome, {authState.user.displayName}</h2> | |
<button onClick={signOut}>Sign out</button> | |
</div> | |
) : ( | |
<button onClick={signInWithGoogle}>Sign in with Google</button> | |
)} | |
</div> | |
<App authState={authState} /> | |
</> | |
); | |
} | |
return <div className="auth">{content}</div>; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { InMemoryCache } from "apollo-cache-inmemory"; | |
import ApolloClient from "apollo-client"; | |
import { split } from "apollo-link"; | |
import { HttpLink } from "apollo-link-http"; | |
import { WebSocketLink } from "apollo-link-ws"; | |
import { getMainDefinition } from "apollo-utilities"; | |
import gql from "graphql-tag"; | |
import React from "react"; | |
import { ApolloProvider, Mutation, Subscription } from "react-apollo"; | |
const PL_SUB = gql` | |
subscription PL { | |
programming_language(order_by: { vote_count: desc }) { | |
name | |
vote_count | |
} | |
} | |
`; | |
const PL_WITH_LOVE_SUB = gql` | |
subscription PL_WITH_LOVE($userId: String!) { | |
programming_language(order_by: { vote_count: desc }) { | |
name | |
vote_count | |
lovedLanguagesByname_aggregate(where: { user_id: { _eq: $userId } }) { | |
aggregate { | |
count | |
} | |
} | |
} | |
} | |
`; | |
const VOTE_MUTATION = gql` | |
mutation Vote($name: String!) { | |
update_programming_language( | |
_inc: { vote_count: 1 } | |
where: { name: { _eq: $name } } | |
) { | |
returning { | |
vote_count | |
} | |
} | |
} | |
`; | |
const LOVE_MUTATION = gql` | |
mutation Love($name: String!) { | |
insert_loved_language(objects: { name: $name }) { | |
affected_rows | |
} | |
} | |
`; | |
const UNLOVE_MUTATION = gql` | |
mutation Unlove($name: String!) { | |
delete_loved_language(where: { name: { _eq: $name } }) { | |
affected_rows | |
} | |
} | |
`; | |
export default function App({ authState }) { | |
const isIn = authState.status === "in"; | |
const headers = isIn ? { Authorization: `Bearer ${authState.token}` } : {}; | |
const httpLink = new HttpLink({ | |
uri: "<hasura-app-url>", | |
headers | |
}); | |
const wsLink = new WebSocketLink({ | |
uri: "<hasura-app-url>", | |
options: { | |
reconnect: true, | |
connectionParams: { | |
headers | |
} | |
} | |
}); | |
const link = split( | |
({ query }) => { | |
const { kind, operation } = getMainDefinition(query); | |
return kind === "OperationDefinition" && operation === "subscription"; | |
}, | |
wsLink, | |
httpLink | |
); | |
const client = new ApolloClient({ | |
link, | |
cache: new InMemoryCache() | |
}); | |
return ( | |
<ApolloProvider client={client}> | |
<Subscription | |
subscription={isIn ? PL_WITH_LOVE_SUB : PL_SUB} | |
variables={ | |
isIn | |
? { | |
userId: authState.user.uid | |
} | |
: null | |
} | |
> | |
{({ data, loading, error }) => { | |
if (loading) return "loading..."; | |
if (error) return error.message; | |
return ( | |
<ul className="pl-list"> | |
{data.programming_language.map(pl => { | |
const { name, vote_count } = pl; | |
let content = null; | |
if (isIn) { | |
const isLoved = | |
pl.lovedLanguagesByname_aggregate.aggregate.count === 1; | |
if (isLoved) { | |
content = ( | |
<Mutation mutation={UNLOVE_MUTATION} variables={{ name }}> | |
{unlove => <button onClick={unlove}>Unlove</button>} | |
</Mutation> | |
); | |
} else { | |
content = ( | |
<Mutation mutation={LOVE_MUTATION} variables={{ name }}> | |
{love => <button onClick={love}>Love</button>} | |
</Mutation> | |
); | |
} | |
} | |
return ( | |
<li key={name}> | |
<span>{`${name} - ${vote_count}`}</span> | |
<span> | |
<Mutation mutation={VOTE_MUTATION} variables={{ name }}> | |
{vote => <button onClick={vote}>Vote</button>} | |
</Mutation> | |
{content} | |
</span> | |
</li> | |
); | |
})} | |
</ul> | |
); | |
}} | |
</Subscription> | |
</ApolloProvider> | |
); | |
} |
Related reading