Modeling an Instagram Clone: Authentication - Dgraph Blog

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 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.
  • 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 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")
);

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 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?

  • 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 the onClick event to our loginWithRedirect 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 the useAuth0 hook.
  • Next, we’re returning GenericButton with props.
    • We’re attaching the 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?

  • We extract a boolean property called isAuthenticated from the useAuth0 hook.
  • If that property is true, then the user is authenticated and so we render LogOutButton. Otherwise, we render LogInButton.

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 called Nav.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 another Box.

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 your src/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 above theme object as a prop to to Grommet.
  • Grommet will wrap our App 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 is id_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_tokens 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 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.

  • 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/

Perhaps dgraph is more strict now than when this tutorial was written,

I needed to explicate the “add: …” auth rule like so:


type User
@auth(
  add: { rule: """
  	query($USER: String!) {
    	queryUser (filter: { email: { eq: $USER }}) {
    		__typename
  		}
  	}"""
  }
  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!
}
1 Like

It’s airways been this way, without the add rule like the example, anyone could create a new node. I bet this was one of the situations with tutorials when you forget to come back and clean up and flush out missing parts.

Also check out Putting it All Together - Dgraph Authentication, Authorization, and Granular Access Control (PART 1) - Dgraph Blog