Authenticating full-stack Nextjs App
Imran Irshad
Posted on November 24, 2020
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
and if you go to localhost:3000/api you can see our graphql api
Let's get started Now
in pages/api/index.ts
file we have two models user
and 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
import models and resolvers to schema
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",
});
we need to change the way we are creating server
const server = new ApolloServer({ schema });
export default server.createHandler({
path: "/api",
});
*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,
};
};
Now Import createContext
to our pages/api/index.ts
and set it to Apollo server context
const server = new ApolloServer({
schema,
context: createContext,
});
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: {},
});
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",
});
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);
}
},
});
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
andpassword
- 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
now Sign user in
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"
}
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;
}
}
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: {},
});
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 },
},
},
});
},
});
- 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
},
});
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!
Let's Try this with HTTP HEARDERS
(undo ctrl + z
)
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,
},
});
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 }
});
},
});
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);
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
anduseMutation
from@apollo/react-hooks
when user is successfully logged in we will push them to
private
route so for that we need router fromnext/router
create a mutation that will be responsible for logging user in
use the Mutation
useMutation
has a method calledonCompleted
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 toprivate
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);
Now go to localhost:3000
and open developer tools and go to application tab. on the left side open local storage panel
Let's try this
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;
}
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;
}
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
}
}
`;
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();
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);
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.
Posted on November 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024
November 29, 2024