In our previous post, we modeled a schema and deployed it to Dgraph Cloud for an Instagram-like social media application. We learned how to give a concrete shape to the data requirements of an app using a GraphQL schema and establish relationships that allow various application interactions like liking, commenting, and posting content.
Today, we’re going to learn how to enable authentication for our app, using Auth0 as the authentication service provider. Dgraph is platform agnostic when it comes to what provider you prefer to use. Instead of Auth0, you can use Firebase, Google authentication, and so on. Dgraph only handles the authorization part where you specify what functions users are allowed to perform based on their authentication status. We’re going to explore authorization and learn how to connect Dgraph with Auth0 in the immediate next article.
We’re also going to start adding some UI to the app, using React as the front-end library. For now, it’ll only have a navbar with two buttons for users to log in and out.
You can find all the code for today’s article here.
Below are our learning objectives for today:
We’ll use Create React App to set up the React project:
npx create-react-app instaclone
This creates the project in a folder called instaclone
. Navigate to that directory and start the development server:
cd instaclone
npm start
You’ll now see a basic React app by opening http://localhost:3000/ from your browser. We’ll go step by step and make it our own.
If you have npm
version 5.1 or lower, you’ll have to install create-react-app
globally with the following command:
npm install -g create-react-app
Then you can just execute:
create-react-app instaclone
We need to install a couple of dependencies for Auth0 integration. So let’s install them first. Execute the following command from the root of your directory:
npm install @auth0/auth0-react
@auth0/auth0-react
is the [Auth0 React SDK] for using Auth0 API in our React app. This exposes useful React hooks and utilities to handle authentication in a programmatic way.
First, we need to create an “entity application” on Auth0 that represents our app. This will generate the necessary credentials that we can use to make the integration.
Create a .env
file at the root of your project. This file will hold some important variables. That’ll make it easier for us to add more variables as we need and pull them in by their names.
You’ll see a new side page containing the application’s details.
.env
file:REACT_APP_AUTH0_DOMAIN="<<your-auth0-domain-name>>"
REACT_APP_AUTH0_CLIENT_ID="<<your-auth0-application-client-id>>"
Make sure that the variable names start with REACT_APP_
.
After users log in or authenticate, Auth0 will take them to the callback URL that you specify. Likewise, the logout URL is for redirecting after users have logged out. Create React App serves React apps at http://localhost:3000 by default, so that’s what we’re using here.
Auth0 returns a JWT after a user has successfully logged in. By adding custom claims, we can inject information about a user in that JWT. We can then use that information to restrict users or grant them access to specific tasks.
Let’s add a custom claim that adds the logged-in user’s email address in the JWT.
function (user, context, callback) {
const namespace = 'https://dgraph.io/jwt/claims';
context.idToken[namespace] = {
'USER': user.email
};
return callback(null, user, context);
}
USER
to the namespace
field of idToken
. The value of that claim is the authenticated user
’s email address. idToken
is the kind of JWT that Auth0 returns upon successful authentication. This is just a simple JavaScript object.Don’t worry if this confuses you. We’ll learn how to inspect a token’s contents once we receive it by using JWT Debugger. We’ll do that soon.
We connect our React app with Auth0 by using the Auth0Provider
component of Auth0 SDK. It also makes the authentication state available to the app so that you can use it for various operations.
You do this by wrapping your root component with the Auth0Provider
component. Under the hood, a component called Auth0Context
manages the authentication state using React context. Auth0Provider
just exposes that context to the child components down the tree.
index.js
file, first import the component:import { Auth0Provider } from "@auth0/auth0-react";
.env
file:const domain = process.env.REACT_APP_AUTH0_DOMAIN;
const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID;
App
component with Auth0Provider
, passing in domain
, clientID
, and the redirection URL as props. The redirection URL in this case is the “origin” URL, i.e. http://localhost:3000:ReactDOM.render(
<Auth0Provider
domain={domain}
clientId={clientId}
redirectUri={window.location.origin}
>
<React.StrictMode>
<App />
</React.StrictMode>
</Auth0Provider>,
document.getElementById("root")
);
We’re ready to flash out some UI so that we can test out the authentication. We’re going to use Grommet as the UI component library.
Execute the following command from your terminal to install the packages we need:
npm install grommet grommet-icons styled-components
We’re going to style the app our way. There are also some files that we don’t need for now. So let’s unclutter our workspace by removing some files from the src
directory:
src/App.css
src/App.test.js
src/index.css
src/reportWebVitals.js
src/setupTests.js
React is a component-based library, so we can build the UI out of many components. So let’s create a separate folder for them called Components
inside the src
directory.
To trigger authentication, we need UI elements like a button. So let’s create buttons for that!
First, create the following three files inside your Components
directory:
Components/GenericButton.js
Components/LogInButton.js
Components/LogOutButton.js
Components/AuthButtons.js
We’ll create a GenericButton
component that will just render Grommet’s Button
component with props that we can pass in. This will make it reusable and also modifiable via props.
GenericButton.js
:import { Button } from "grommet";
export const GenericButton = (props) => <Button {...props}></Button>;
LogInButton
and LogOutButton
component. Inside your LogInButton.js
file, write the following code:import { useAuth0 } from "@auth0/auth0-react";
import { GenericButton } from "./GenericButton";
const LogInButton = () => {
const { loginWithRedirect } = useAuth0();
return (
<GenericButton
color="status-ok"
secondary
plain={true}
label="Log in"
style={{ margin: 10 }}
onClick={loginWithRedirect}
/>
);
};
export default LogInButton;
What’s happening here?
useAuth0
hook from Auth0 React SDK.loginWithRedirect
method that we’re extracting by executing the hook. This method is responsible for redirecting users to the callback URL after they’ve successfully authenticated.GenericButton
component with props. In one of these props, we’re attaching the onClick
event to our loginWithRedirect
method. So when users click this button, it’ll log them in and redirect them.On to making the logout button!
Inside LogOutButton.js
, we have the following similar code:
import { GenericButton } from "./GenericButton";
import { useAuth0 } from "@auth0/auth0-react";
const LogOutButton = () => {
const { logout } = useAuth0();
return (
<GenericButton
color="status-warning"
plain={true}
label="Log out"
style={{ margin: 10 }}
onClick={() =>
logout({
returnTo: window.location.origin,
})
}
/>
);
};
export default LogOutButton;
What’s happening here?
logout
method by calling the useAuth0
hook.GenericButton
with props.
logout
method as the onClick
event handler. In the method, we’re passing an object specifying the redirection URL after users have logged out.Now we can use these two buttons to build a component called AuthButton
. This component will render LogInButton
if the user is logged out, or LogOutButton
if the user is logged in.
Create the file AuthButton.js
in your Components
directory with the following:
import LogInButton from "./LogInButton";
import LogOutButton from "./LogOutButton";
import { useAuth0 } from "@auth0/auth0-react";
const AuthButton = () => {
const { isAuthenticated } = useAuth0();
return isAuthenticated ? <LogOutButton /> : <LogInButton />;
};
export default AuthButton;
What’s happing here?
isAuthenticated
from the useAuth0
hook.LogOutButton
. Otherwise, we render LogInButton
.Let’s build a navbar component. For now, the navbar will hold the application name and logo, and of course, buttons for users to log in and out.
Components
directory, create a file called Nav.js
.import React from "react";
import { Box, Menu, Text } from "grommet";
import { Instagram } from "grommet-icons";
import AuthButton from "./AuthButtons";
const NavBar = () => (
<Box
as="header"
flex={false}
direction="row"
background="white"
elevation="medium"
align="center"
justify="center"
responsive={true}
>
<Instagram />
<Text size="large" color="brand" style={{ marginLeft: 10 }}>
InstaClone
</Text>
<Box
margin={{ left: "medium" }}
round="xsmall"
background={{ color: "white", opacity: "weak" }}
direction="row"
align="center"
pad={{ horizontal: "small" }}
>
<AuthButton />
</Box>
</Box>
);
export default NavBar;
What’s happening here?
Box
to make the navbar. Using some of its props, we’re doing some styling and making it responsive.Instagram
component works as the app’s logo.Text
component, we’re rendering our app’s name on the navbar.AuthButton
component inside another Box
.We have all the pieces of the simple UI that we need for now. Time to put them together from our root App
component.
public/index.html
file:<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" />
theme
object in your src/App.js
file:const theme = {
global: {
font: {
family: "Roboto",
size: "18px",
height: "20px",
},
},
};
Grommet
where we can specify the theming configurations. We do that by passing the above theme
object as a prop to to Grommet
.Grommet
will wrap our App
component, making the theme persist across component tree.src/App.js
:const App = () => (
<Grommet theme={theme} full>
<Box fill background="light-3">
<NavBar />
</Box>
</Grommet>
);
Again we use Box
to set the layout. This Box
is the topmost container of our app.
Now you can run npm start
from your terminal and see your app with all the customizations we’ve made so far:
We’re now ready to test our app and check if everything works as we intended.
This certificate contains the public key of the signed token. The JWTs that Auth0 returns after successful authentication are all signed by its equivalent private key. We need to give the public key to Dgraph so that Dgraph can trust any JWT that’s signed with the equivalent private key.
Let’s extract the public key. For that, execute the following from your terminal, giving the command the full path to where you downloaded the certificate file.:
openssl x509 -pubkey -noout -in path-to-your-certicate-file.pem
You’ll see something like this:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAySC4ObdOmwz4uNncFGTJ
LAlvDVyU1Rrku87Z7a6pQ+FOSUhx1gRa0XOr7KlPFQx0H+MYKnZb/n30ROZXGB1H
9iNyvY7NixCqO6i918MBC0xJyVFMfOQhy9m0GjTjofy2lJcu4axyG2eZ4SnfV3vi
KlEK8ma5hB93kPwextBYQ6nrgpo1C+65YeXyg+hRE3GbTKIr7DNnOQFA04MihfpG
jQyjA/u8nHhgm6dV+YnupmmJPzA+6sGZXZ4jiKkGEE7q4ZlyXr6hsz2sF85uycVE
LD23bNlyOKOP+jhbSbdNU7L8j7QMbQf4CrwzKtxm1ul5HySJDajjaiXrVL0xYdsB
2wIDAQAQ
-----END PUBLIC KEY-----
We’ll give this key to Dgraph via the GraphQL schema. For that you need to replace each newline with the newline character (\n
), as you’ll see in a bit when we re-deploy the schema.
At this point, you should have the JWT from Auth0 with your email as the custom claim. For that:
JSON
response with several fields. Our field of interest is id_token
; that’s where the custom claims live.id_token
value.We’ll learn how to connect Dgraph with Auth0 in the next part of our InstaClone series. You’ll see that Dgraph will be able to trust these id_token
s and verify GraphQL requests from the user. Using the logged-in user’s email address inside this token, we’ll also learn how to write authorization rules to restrict specific tasks to unauthenticated users.
For now, just follow along with following steps:
type User
@auth(
update: { rule: """
query($USER: String!) {
queryUser (filter: { email: { eq: $USER }}) {
__typename
}
}"""
}
delete: { rule: """
query($USER: String!) {
queryUser (filter: { email: { eq: $USER }}) {
__typename
}
}"""
}
) {
username: String! @id
name: String!
about: String
email: String! @id @search(by: [hash])
avatarImageURL: String!
posts: [Post!] @hasInverse(field: postedBy)
following: Int!
follower: Int!
}
type Post
@auth(
update: { rule: """
query($USER: String!) {
queryPost {
postedBy (filter: { email: { eq: $USER }}) {
__typename
}
}
}"""
}
add: { rule: """
query($USER: String!) {
queryPost {
postedBy (filter: { email: { eq: $USER }}) {
__typename
}
}
}"""
}
delete: { rule: """
query($USER: String!) {
queryPost {
postedBy (filter: { email: { eq: $USER }}) {
__typename
}
}
}"""
}
) {
id: ID!
postedBy: User!
imageURL: String!
description: String
likes: Int!
comments: [Comment!] @hasInverse(field: commentOn)
}
type Comment
@auth(
add: { rule: """
query($USER: String!) {
queryComment {
commentBy(filter: { email: { eq: $USER }}) {
__typename
}
}
}"""
}
delete: { rule: """
query($USER: String!) {
queryComment {
commentBy(filter: { email: { eq: $USER }}) {
__typename
}
}
}"""
}
update: { rule: """
query($USER: String!) {
queryComment {
commentBy(filter: { email: { eq: $USER }}) {
__typename
}
}
}"""
}
) {
id: ID!
text: String!
commentBy: User!
commentOn: Post!
}
# Dgraph.Authorization {"VerificationKey":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAySC4ObdOmwz4uNncFGTJ\nLAlvDVyU1Rrku87Z7a6pQ+FOSUhx1gRa0XOr7KlPFQx0H+MYKnZb/n30ROZXGB1H\n9iNyvY7NixCqO6i918MBC0xJyVFMfOQhy9m0GjTjofy2lJcu4axyG2eZ4SnfV3vi\nKlEK8ma5hB93kPwextBYQ6nrgpo1C+65YeXyg+hRE3GbTKIr7DNnOQFA04MihfpG\njQyjA/u8nHhgm6dV+YnupmmJPzA+6sGZXZ4jiKkGEE7q4ZlyXr6hsz2sF85uycVE\nLD23bNlyOKOP+jhbSbdNU7L8j7QMbQf4CrwzKtxm1ul5HySJDajjaiXrVL0xYdsB\n2wIDAQAQ\n-----END PUBLIC KEY-----","Header":"X-Auth-Token","Namespace":"https://dgraph.io/jwt/claims","Algo":"RS256","Audience":["<<your-auth0-application-client-id>>"]}
id_token
as an X-Auth-Token
header. As a result, GraphQL requests will contain this header so that Dgraph can verify that a logged-in user is making the requests.In the GraphQL schema above, we’ve introduced a couple of “authorization rules”. For example, users can only make posts for themselves, not for anyone else. We’ll go into details on how they work in our next article. But we can test if the rules are working or not.
addUser
mutation. You have to use the same email address that you used to open your Auth0 account for this mutation:mutation AddAUser($userInput: [AddUserInput!]!) {
addUser(input:$userInput) {
user {
name
username
}
}
}
{
"userInput": [
{
"username": "sakib",
"name": "Abu Sakib",
"email": "[email protected]",
"about": "Programming, writing and literature.",
"avatarImageURL": "https://robohash.org/cosmos.png?size=50x50&set=set102",
"following": 159,
"follower": 15
}
]
}
This should yield success:
mutation AddAPost($postInput: [AddPostInput!]!) {
addPost(input:$postInput) {
post {
id
description
likes
postedBy {
username
}
}
}
}
{
"postInput": [
{
"postedBy": {
"username": "sakib"
},
"description": "Never thought of I'd be able to do systems programming but here we are...",
"likes": 2,
"imageURL": "http://dummyimage.com/107x100.png/ff4344/ffffff"
}
]
}
This should also be successful:
{
"postInput": [
{
"postedBy": {
"username": "karen"
},
"description": "The first edition of Robert Aickman's 'Cold Hand in Mine'!",
"likes": 8,
"imageURL": "http://dummyimage.com/107x100.png/df4344/ffffff"
}
]
}
This doesn’t get through and we get a null
response:
That means our authorization rules are working. Dgraph was able to figure out that the logged-in user is not the owner of this account since the email in that mutation doesn’t match with the email that Auth0 provided in its JWT. So the user shouldn’t be allowed to make that addPost
mutation.
Throughout this article, we learned how to integrate Auth0 into a React app step by step and enable authentication. We also pieced together a simple UI to try out the new feature! Using the UI, we were able to log in and receive a JWT token from Auth0 containing information about the logged-in user. We then used that token to make authenticated requests to the API so that Dgraph can verify the user and the requests.
In the next part of the series, we’ll discuss authorization and how Dgraph handles it.