DEV Community

loading...

Authenticating full-stack Nextjs App

imranib profile image Imran Irshad ・11 min read

In The Previous blog we created a FullStack Nextjs with Typescript, Graphql. In this post we will learn how we add authentication to the app. You can read the post here.
You can get the code from this repo .

If You want to see the final code you can get it from here.

From Your terminal run this command

git clone https://github.com/imran-ib/full-stack-nextjs.git

cd into full-stack-nextjs and run npm install

After the installation is finished npm run dev and go to localhost:3000.

You should see something like this

Alt Text

and if you go to localhost:3000/api you can see our graphql api
Alt Text

Let's get started Now

in pages/api/index.ts file we have two models userand post and two resolvers mutations and query and schema

let's add these to their separate files. create three folders Models , Resolvers , Schema in pages/api directory

and add user and post to pages/api/Models

mutations and query to pages/api/Resolvers

schema to pages/api/Schema

and import these files to pages/api/index.ts

Alt Text
import models and resolvers to schema

Alt Text
import this file (schema.ts) to pages/api/index .

Note: Please don't forget to move all related imports to each file

Now You App Should working file as before.

our pages/api/index.ts file looks like this

import { ApolloServer } from "apollo-server-micro";
import { schema } from "./Schema";

export const config = {
  api: {
    bodyParser: false,
  },
};

export default new ApolloServer({ schema }).createHandler({
  path: "/api",
});

Enter fullscreen mode Exit fullscreen mode

we need to change the way we are creating server

const server = new ApolloServer({ schema });

export default server.createHandler({
  path: "/api",
});
Enter fullscreen mode Exit fullscreen mode

*Note : if you have any problem or getting any error while changing files to different directories you can get the code from running following command *

git clone -b file-structre https://github.com/imran-ib/auntenticating-next-app.git

after cloning the repo just install dependencies.

Context

context is an object or a function that creates an object. We pass this object to our server and server will passes this context to every resolver. In this way we can share information through out our app.

create a file named context.ts in pages/api

import { PrismaClient } from "@prisma/client";
import { Request, Response } from "express";
const prisma = new PrismaClient();

export interface Context {
  prisma: PrismaClient;
  req: Request;
  res: Response;
}

export const createContext = (ctx: any): Context => {
  return {
    ...ctx,
    prisma,
  };
};

Enter fullscreen mode Exit fullscreen mode

Now Import createContext to our pages/api/index.ts and set it to Apollo server context

const server = new ApolloServer({
  schema,
  context: createContext,
});
Enter fullscreen mode Exit fullscreen mode

So far so good

graphql-shield

graphql-shield is a sort of middleware that creates a permission layer for your application. we will add it to the schema as a middleware and it will allow us to check users permissions before resolving any function.

npm i graphql-shield

create new file pages/api/permissions/index.ts and paste following code

import { shield } from "graphql-shield";

const rules = {
  isAuthenticatedUser: {},
};

export const permissions = shield({
  Query: {},
  Mutation: {},
});

Enter fullscreen mode Exit fullscreen mode

isAuthenticatedUser : we will set it to a Boolean so that if user is authenticated it will be true otherwise it will be false

in permissions we will add queries and mutations we want to protect

Now We will add this to schema but for that we need another package graphql-middleware

GraphQL Middleware lets you run arbitrary code before or after a resolver is invoked.

npm i graphql-middleware

now import permissions and applyMiddleware from the package we installed graphql middleware into pages/api/index.ts

import { ApolloServer } from "apollo-server-micro";
import { applyMiddleware } from "graphql-middleware";
import { schema } from "../../server/Schema";
import { createContext } from "./context";
import { permissions } from "./permissions";

export const config = {
  api: {
    bodyParser: false,
  },
};

const server = new ApolloServer({
  schema: applyMiddleware(schema, permissions), // <-- Changes
  context: createContext,
});

export default server.createHandler({
  path: "/api",
});

Enter fullscreen mode Exit fullscreen mode

This is it. Graphql Shield is set up.

JWT

Now we need to identify users with the help of jsonwebtoken

npm i jsonwebtoken && npm i -D @types/jsonwebtoken

jesonwebtoken normally refers as simply jwt . jwt is base64url-encoded which can be easily decoded later. it is worth mentioning that we don't put valuable information in jwt because anyone can decode it and get information. We only put information like user id which is normally random digit or a long string. We need that to identify user but if someone gets that information it is ok for us.

When user will try to login and provides correct credentials we will generate a jwt for that user with user's id encoded in it and send it to client side. We then take that token on client side and store it in local storage. with every request we will send that token to our server.

let's go to our mutation.ts file and in this file we four mutations

signupUser , deletePost , createDraft, and publish

Let's create new mutation userLogin

 t.field("userLogin", {
      type: "String",
      args: {
        email: stringArg(),
        password: stringArg(),
      },
      description: "User Sign in",
      resolve: async (parent, args, ctx) => {
        try {
          const User = await prisma.user.findOne({
            where: {
              email: args.email,
            },
          });

          if (!User) return new Error(`Authorization failed`);
          const token = jwt.sign({ UserId: User.id }, "MyNewApp");
          return token;
        } catch (error) {
          console.log("definition -> error", error);
        }
      },
    });
Enter fullscreen mode Exit fullscreen mode

NOTE: ideally you would want to hash the password when creating user and then compare that hash when you letting them in, but in this case I am skipping bcrypt

So What is happening in this resolver

  • we are creating new resolver userLogin
  • Return type is string (jwt)
  • takes two arguments email and password
  • we find user from database and if user not found then we will throw an error
  • if there is user then create a jwt token and return it

let's see this in action

First we need a user so let's create one

Alt Text

now Sign user in

Alt Text

Great We have token Now we will send this token in authorization header so that our app can decode it and find the user by id

at the bottom of above image you see Query Variables we can pass here http headers. Copy the token and pass it as following

{
  "Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsImlhdCI6MTYwNTk3NDgyNX0.kYDgyvB3C0LYiam7ixUDzkQIbCAl19Gyp83MiR9q_S8"
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

create a utils/decocdejwt.ts

import { verify } from "jsonwebtoken";

// export const APP_SECRET = process.env.APP_SECRET
export const APP_SECRET = "MyNewApp";

interface Token {
  UserId: string;   // <-- NOTE: capital "U"
}

export function getUserId(context) {
  const Authorization = context.req.headers["authorization"];
  if (Authorization) {
    const token = Authorization.replace("Bearer ", "");
    const verifiedToken = verify(token, APP_SECRET) as Token;
    const res = verifiedToken && verifiedToken.UserId;
    return res;
  }
}
Enter fullscreen mode Exit fullscreen mode

getUserId function will take context and from that it will get Authorization header and decode the jwt and give us user's id

Now Let's go to api/permissions/index.ts and add new rule

import { rule, shield } from "graphql-shield";
import { PrismaClient } from "@prisma/client";
import { getUserId } from "../../../utils/decodejwt";
const prisma = new PrismaClient();

const rules = {
  isAuthenticatedUser: rule()(async (__parent, _args, context) => {
    const userId = parseInt(getUserId(context));// it will return string but we need int here
    const User = await prisma.user.findOne({ where: { id: userId } });
    if (User) {
      return true;
    } else {
      return false;
    }
  }),
};

export const permissions = shield({
  Query: {},
  Mutation: {},
});

Enter fullscreen mode Exit fullscreen mode

isAuthenticatedUser is our new rule that will return true if there is any user exists otherwise it will return false

Let's look at createPost mutation

 t.field("createDraft", {
      type: "Post",
      args: {
        title: stringArg({ nullable: false }),
        content: stringArg(),
        authorEmail: stringArg(),
      },
      resolve: (_, { title, content, authorEmail }, ctx) => {
        return prisma.post.create({
          data: {
            title,
            content,
            published: false,
            author: {
              connect: { email: authorEmail },
            },
          },
        });
      },
    });
Enter fullscreen mode Exit fullscreen mode
  • return type is Post
  • takes three arguments title, content , authorEmail
  • creates a new post and connect to user by provided email

Add this mutation to the permissions

import { rule, shield } from "graphql-shield";
import { PrismaClient } from "@prisma/client";
import { getUserId } from "../../../utils/decodejwt";
const prisma = new PrismaClient();

const rules = {
  isAuthenticatedUser: rule()(async (__parent, _args, context) => {
    const userId = parseInt(getUserId(context));
    const User = await prisma.user.findOne({ where: { id: userId } });
    if (User) {
      return true;
    } else {
      return false;
    }
  }),
};

export const permissions = shield({
  Query: {},
  Mutation: {
    createDraft: rules.isAuthenticatedUser,  // <-- added
  },
});

Enter fullscreen mode Exit fullscreen mode

Now Our createDraft should be protected. let's see this in action

go to localhost:3000/api open a new tab and remove HTTP HEADERS and try to create a new post.

You should get an error Not Authorised!

Alt Text

Let's Try this with HTTP HEARDERS (undo ctrl + z)

Alt Text

All Mutation and Queries can be protect and additional rules can be applied.

graphql-shield also provide several Other methods like or, and, not etc. which can be very helpful and flexible in creating different rules

import { rule, shield } from "graphql-shield";
import { PrismaClient } from "@prisma/client";
import { getUserId } from "../../../utils/decodejwt";
const prisma = new PrismaClient();

const rules = {
  isAuthenticatedUser: rule()(async (__parent, _args, context) => {
    const userId = parseInt(getUserId(context));
    const User = await prisma.user.findOne({ where: { id: userId } });
    if (User) {
      return true;
    } else {
      return false;
    }
  }),
};

export const permissions = shield({
  Query: {
    post: rules.isAuthenticatedUser,
    feed: rules.isAuthenticatedUser,
    drafts: rules.isAuthenticatedUser,
    filterPosts: rules.isAuthenticatedUser,
  },
  Mutation: {
    createDraft: rules.isAuthenticatedUser,
    deletePost: rules.isAuthenticatedUser,
    publish: rules.isAuthenticatedUser,
  },
});

Enter fullscreen mode Exit fullscreen mode

Note: if you are facing any trouble with above code you can checkout shield branch

git clone -b shield https://github.com/imran-ib/auntenticating-next-app.git

Now when user logs in we will store the token in local storage and and send that token with every request. But first we need a query that will return a user if there is token.

 t.field('CurrentUser', {
      type: 'User',
      nullable: true,
      resolve: async (_root, _agrs, ctx) => {
          //get the user id from token
        const userId = parseInt(getUserId(ctx));

        if (!userId) return;

        return ctx.prisma.user.findOne({
          where: { id: userId }
        });
      },
    });
Enter fullscreen mode Exit fullscreen mode

Now we can determine weather user is logged in or not.

Now Let's move on to client side

create a new page in pages and name it private.

import { withApollo } from "../apollo/client";

const PrivatePage = () => {
  return <h1>This is Private Page </h1>;
};
export default withApollo(PrivatePage);

Enter fullscreen mode Exit fullscreen mode

User should not be able to visit this page if he/she is not logged in

*NOTE: the post is already getting long so instead of creating login form we will send just email and password of user already registered directly in query *

let's modify pages/index.ts .

Changes we will make in this component

  • import gql and useMutation from @apollo/react-hooks

  • when user is successfully logged in we will push them to private route so for that we need router from next/router

  • create a mutation that will be responsible for logging user in

  • use the Mutation

  • useMutation has a method called onCompleted that will be called on success of mutation. so we will grab the token when mutation is successfully executed and store the token to local storage and push the user to private route.

  • Handle the loading and error state

  • create a button to log user in

So index.ts should look like this now

import { withApollo } from "../apollo/client";
import { gql, useMutation } from "@apollo/react-hooks";
import { useRouter } from "next/router";

const USER_LOGIN = gql`
  mutation UserLogin {
    userLogin(email: "john@exapmle.com", password: "123456")
  }
`;

const IndexPage = () => {
  const Router = useRouter();

  const [login, { data, loading, error }] = useMutation(USER_LOGIN, {
    onCompleted: (data) => {
      const token = data?.userLogin;
      localStorage.setItem("token", token);
      Router.push("/private");
    },
  });
  if (loading) return <p>Loading...</p>;
  if (loading) return <p>{error.message}</p>;
  return (
    <>
      <h1>Hello Next.js 👋</h1>
      <button onClick={() => login()}>Login</button>
    </>
  );
};
export default withApollo(IndexPage);

Enter fullscreen mode Exit fullscreen mode

Now go to localhost:3000 and open developer tools and go to application tab. on the left side open local storage panel

Alt Text

Let's try this

Alt Text

GREATE

Now We will get that token from local storage and send it with every request

got apollo/client.js and update createApolloClient

we need to change the way we are creating client. we will create two links authLink and httpLink and concat the togehter

function createApolloClient() {
  // Declare variable to store authToken
  let token;

  const httpLink = createHttpLink({
    uri: "http://localhost:3000/api",
    credentials: "include",
  });

  const authLink = setContext((_, { headers }) => {
    // get the authentication token from local storage if it exists
    if (typeof window !== "undefined") {
      token = localStorage.getItem("token");
    }
    // return the headers to the context so httpLink can read them
    return {
      headers: {
        ...headers,
        Authorization: token ? `Bearer ${token}` : "",
      },
    };
  });

  const client = new ApolloClient({
    ssrMode: typeof window === "undefined",
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(),
  });

  return client;
}
Enter fullscreen mode Exit fullscreen mode

Entire File

import React from "react";
import Head from "next/head";
import { ApolloProvider, createHttpLink } from "@apollo/react-hooks";
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { setContext } from "@apollo/client/link/context";

let apolloClient = null;

/**
 * Creates and provides the apolloContext
 * to a next.js PageTree. Use it by wrapping
 * your PageComponent via HOC pattern.
 * @param {Function|Class} PageComponent
 * @param {Object} [config]
 * @param {Boolean} [config.ssr=true]
 */
export function withApollo(PageComponent, { ssr = true } = {}) {
  const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
    const client = apolloClient || initApolloClient(apolloState);
    return (
      <ApolloProvider client={client}>
        <PageComponent {...pageProps} />
      </ApolloProvider>
    );
  };

  // Set the correct displayName in development
  if (process.env.NODE_ENV !== "production") {
    const displayName =
      PageComponent.displayName || PageComponent.name || "Component";

    if (displayName === "App") {
      console.warn("This withApollo HOC only works with PageComponents.");
    }

    WithApollo.displayName = `withApollo(${displayName})`;
  }

  if (ssr || PageComponent.getInitialProps) {
    WithApollo.getInitialProps = async (ctx) => {
      const { AppTree } = ctx;

      // Initialize ApolloClient, add it to the ctx object so
      // we can use it in `PageComponent.getInitialProp`.
      const apolloClient = (ctx.apolloClient = initApolloClient());

      // Run wrapped getInitialProps methods
      let pageProps = {};
      if (PageComponent.getInitialProps) {
        pageProps = await PageComponent.getInitialProps(ctx);
      }

      // Only on the server:
      if (typeof window === "undefined") {
        // When redirecting, the response is finished.
        // No point in continuing to render
        if (ctx.res && ctx.res.finished) {
          return pageProps;
        }

        // Only if ssr is enabled
        if (ssr) {
          try {
            // Run all GraphQL queries
            const { getDataFromTree } = await import("@apollo/react-ssr");
            await getDataFromTree(
              <AppTree
                pageProps={{
                  ...pageProps,
                  apolloClient,
                }}
              />
            );
          } catch (error) {
            // Prevent Apollo Client GraphQL errors from crashing SSR.
            // Handle them in components via the data.error prop:
            // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
            console.error("Error while running `getDataFromTree`", error);
          }

          // getDataFromTree does not call componentWillUnmount
          // head side effect therefore need to be cleared manually
          Head.rewind();
        }
      }

      // Extract query data from the Apollo store
      const apolloState = apolloClient.cache.extract();
      return {
        ...pageProps,
        apolloState,
      };
    };
  }

  return WithApollo;
}

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {Object} initialState
 */
function initApolloClient(initialState) {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (typeof window === "undefined") {
    return createApolloClient(initialState);
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = createApolloClient(initialState);
  }

  return apolloClient;
}

/**
 * Creates and configures the ApolloClient
 * @param  {Object} [initialState={}]
 */

function createApolloClient() {
  // Declare variable to store authToken
  let token;

  const httpLink = createHttpLink({
    uri: "http://localhost:3000/api",
    credentials: "include",
  });

  const authLink = setContext((_, { headers }) => {
    // get the authentication token from local storage if it exists
    if (typeof window !== "undefined") {
      token = localStorage.getItem("token");
    }
    // return the headers to the context so httpLink can read them
    return {
      headers: {
        ...headers,
        Authorization: token ? `Bearer ${token}` : "",
      },
    };
  });

  const client = new ApolloClient({
    ssrMode: typeof window === "undefined",
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(),
  });

  return client;
}

Enter fullscreen mode Exit fullscreen mode

Now we are sending auth token with every request.

Now Go To pages/private.tsx and create new query that will fetch current user

const CurrentUser = gql`
  query CurrentUser {
    CurrentUser {
      id
      name
      email
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

get the data and handle loading and error state

  //NOTE we should use router in client side component instead of pages
  const Router = useRouter();
  const { data, loading, error } = useQuery(CurrentUser);
  if (loading) return <p>Loading...</p>;
  if (loading) return <p>{error.message}</p>;
  const user = data.CurrentUser;
  if (!user) Router.back();

Enter fullscreen mode Exit fullscreen mode

if there is current user we will let them through otherwise we will push them back to home page

import { gql, useQuery } from "@apollo/react-hooks";
import { withApollo } from "../apollo/client";
import { useRouter } from "next/router";

const CurrentUser = gql`
  query CurrentUser {
    CurrentUser {
      id
      name
      email
    }
  }
`;

const PrivatePage = () => {
  //NOTE we should use router in client side component instead of pages
  const Router = useRouter();
  const { data, loading, error } = useQuery(CurrentUser);
  if (loading) return <p>Loading...</p>;
  if (loading) return <p>{error.message}</p>;
  const user = data.CurrentUser;
  if (!user) Router.back();

  return (
    <>
      <h1>This is Private Page </h1>
      {user?.id}
      {user?.email}
      {user?.name}
    </>
  );
};
export default withApollo(PrivatePage);

Enter fullscreen mode Exit fullscreen mode

you can have a hook useUser where you can check weather there is a user logged in or not. import that hook in every component where needed.

Discussion

pic
Editor guide