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:
Setting up the React project
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.
For older NPM versions
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
Installing dependencies
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.
Integrating Auth0
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.
Creating an Auth0 application
- Go to the Auth0 website and sign up. After that, it’ll take you to your dashboard.
- Select Applications from the left sidebar.
- On the new page, select Create Application.
- A modal will pop up.
- Give the application a name.
- Select Single Page Web Applications as the application type.
- Then click on Create.
You’ll see a new side page containing the application’s details.
- Copy the Client ID and Domain and put them inside the
.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_
.
- Now scroll down a bit.
- You’ll see three text areas labeled Allowed Callback URLs, Allowed Web Origins, and Allowed Logout URLs.
- Put http://localhost:3000 in each.
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.
Adding custom claims
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.
- Go to the Rules page by clicking on Auth Pipeline>Rules from the left sidebar of your dashboard.
- Click on Create.
- Select Empty rule.
- On the new page, add the following in the Script area:
function (user, context, callback) {
const namespace = 'https://dgraph.io/jwt/claims';
context.idToken[namespace] = {
'USER': user.email
};
return callback(null, user, context);
}
- While we’re at it, let’s give the rule a descriptive name too instead of “Empty rule”.
- This adds the custom claim called
USER
to thenamespace
field ofidToken
. The value of that claim is the authenticateduser
’s email address.idToken
is the kind of JWT that Auth0 returns upon successful authentication. This is just a simple JavaScript object. - Now click on Save changes and we’re done!
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.
Connecting InstaClone with Auth0
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.
- In your
index.js
file, first import the component:
import { Auth0Provider } from "@auth0/auth0-react";
- Then pull in the domain name and client ID from your
.env
file:
const domain = process.env.REACT_APP_AUTH0_DOMAIN;
const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID;
- Now wrap your
App
component withAuth0Provider
, passing indomain
,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")
);
Adding some UI
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.
Installing dependencies and cleaning up
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.
Making the login and logout buttons
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.
- So place the following bit of code inside
GenericButton.js
:
import { Button } from "grommet";
export const GenericButton = (props) => <Button {...props}></Button>;
- Now we can use this to make the
LogInButton
andLogOutButton
component. Inside yourLogInButton.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?
- We’re importing the
useAuth0
hook from Auth0 React SDK. - The hook exposes a
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. - Next, we’re returning the
GenericButton
component with props. In one of these props, we’re attaching theonClick
event to ourloginWithRedirect
method. So when users click this button, it’ll log them in and redirect them. - Lastly, we export the component for later use.
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?
- Quite intuitively, we’re now extracting the
logout
method by calling theuseAuth0
hook. - Next, we’re returning
GenericButton
with props.- We’re attaching the
logout
method as theonClick
event handler. In the method, we’re passing an object specifying the redirection URL after users have logged out.
- We’re attaching the
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?
- We extract a boolean property called
isAuthenticated
from theuseAuth0
hook. - If that property is true, then the user is authenticated and so we render
LogOutButton
. Otherwise, we renderLogInButton
.
Building a navbar component
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.
- Inside the
Components
directory, create a file calledNav.js
. - Import the necessary tools that we need:
import React from "react";
import { Box, Menu, Text } from "grommet";
import { Instagram } from "grommet-icons";
import AuthButton from "./AuthButtons";
- Now let’s write a component called NavBar using Grommet’s toolkit:
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?
- We’re using Grommet’s
Box
to make the navbar. Using some of its props, we’re doing some styling and making it responsive. - The
Instagram
component works as the app’s logo. - Using Grommet’s
Text
component, we’re rendering our app’s name on the navbar. - Then we’re rendering our
AuthButton
component inside anotherBox
.
Putting it all together
We have all the pieces of the simple UI that we need for now. Time to put them together from our root App
component.
- You can create a custom theme object and tell Grommet to use that throughout the app. This object can contain stuff like which font to use, font size, etc.
- For example, you can use the Roboto font. First, you need to bring it in by placing the following in your
public/index.html
file:
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" />
- Then create the
theme
object in yoursrc/App.js
file:
const theme = {
global: {
font: {
family: "Roboto",
size: "18px",
height: "20px",
},
},
};
- There’s a top-level container component called
Grommet
where we can specify the theming configurations. We do that by passing the abovetheme
object as a prop to toGrommet
. -
Grommet
will wrap ourApp
component, making the theme persist across component tree. - We do all of this by putting the following in
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:
Test it out
We’re now ready to test our app and check if everything works as we intended.
Getting the JWT token
- Click on the Log in button to log in.
- If you’ve successfully logged in, you should see the Log out button in a different color.
-
At this point, you should have the JWT from Auth0 with your email as the custom claim. For that:
- Open your browser’s development console and go to the Network tab.
- Look for a network event with a file type named token.
- Clicking on it will open its details. From there go to the Response tab.
- It’ll show you the
JSON
response with several fields. Our field of interest isid_token
; that’s where the custom claims live. - Select the
id_token
value. - Now go to the JWT Debugger website and paste the token there. You’ll see the decoded value there, and the claim that we added using Auth0 Rules:
Enforcing authorization rules using JWT
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:
- Paste the following schema and re-deploy your Dgraph Cloud backend:
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!
}
- Go to the GraphQL window from the left sidebar of your dashboard. Click on Request Headers above Explorer.
- Paste your
id_token
as anX-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.
- First let’s simulate opening an account by executing a
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": "sakib@dgraph.io",
"about": "Programming, writing and literature.",
"avatarImageURL": "https://robohash.org/cosmos.png?size=50x50&set=set102",
"following": 159,
"follower": 15
}
]
}
This should yield success:
- Now let’s try to post something from our own account by executing the following mutation:
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:
- Can we post for someone else? That shouldn’t be allowed. Let’s try the same mutation but with the following post details:
{
"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.
Conclusion
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.
ReferencesPhoto by George Prentzas on Unsplash.This is a companion discussion topic for the original entry at https://dgraph.io/blog/post/insta-authentication/