Using Warrant with Next.js
In this guide, we'll walk through how to add authorization & access control to a Next.js application using Warrant. We'll use the Warrant React SDK for client-side access checks and the Warrant Node SDK and Warrant Express Middleware for server-side access checks.
The guide assumes that you already have a Warrant account and corresponding API and Client keys. If you don't already have an account, you can sign up for one here.
Client-side Authorization
The Warrant React SDK allows you to securely add authorization & access control checks in pages and components to show/hide content that requires privileged access. To configure the SDK, you need to wrap your application in the WarrantProvider
component and pass it a valid session token.
Create an Allowed Origin
Since the Warrant React SDK interacts directly with the Warrant API, you must first configure an Allowed Origin from the Warrant Dashboard to allow requests from your Next.js application to the Warrant API. Requests from an origin not explicitly specified by an Allowed Origin will fail.
For example, if your Next.js application is served at https://app.example.com
, create an Allowed Origin for https://app.example.com
to allow requests from that origin.
For local development, you can add an Allowed Origin for localhost. For example, if your local Next.js application is being served on localhost port 3000, add an Allowed Origin for http://localhost:3000
.
Allowed Origins support wildcards (*), so creating an Allowed Origin for https://*.example.com
will allow requests coming from any subdomain of https://example.com
such as https://foo.example.com
and https://bar.example.com
.
Add WarrantProvider
The SDK uses React Context to provide access to utility methods for performing access checks anywhere in your app. To configure this functionality, you need to wrap your application with the WarrantProvider
component, passing it your Client Key using the clientKey
prop.
To do this in a Next.js app, you need to create a custom App component in pages/_app.jsx
. Here's what the custom App component should look like with WarrantProvider
configured:
import { WarrantProvider } from "@warrantdev/react-warrant-js";
function MyApp({ Component, pageProps }) {
return (
<WarrantProvider clientKey="<replace_with_your_client_key>">
<Component {...pageProps} />
</WarrantProvider>
);
}
export default MyApp;
Set the Session Token
To finish initializing the SDK you must call the setSessionToken
method with a valid authorization session token. This allows the SDK to make secure access check requests to the Warrant API. Refer to our guide on Creating Sessions to learn how to generate session tokens server-side.
import { useWarrant } from "@warrantdev/react-warrant-js";
const { setSessionToken } = useWarrant();
setSessionToken(theSessionTokenYouGenerated);
We recommend generating a session token for your users during your authentication flow. You can then return the token to the client and pass it to the SDK. Here's an example of what that might look like:
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { useWarrant } from "@warrantdev/react-warrant-js";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const router = useRouter();
const { setSessionToken } = useWarrant();
const handleEmailUpdated = (event) => {
setEmail(event.target.value);
};
const handlePasswordUpdated = (event) => {
setPassword(event.target.value);
};
const login = useCallback(
async (event) => {
event.preventDefault();
/*
* /api/auth/login returns warrantSessionToken
* in the response body when there is a successful login.
*
* NOTE: You must implement an endpoint like this yourself.
*/
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
});
if (response.ok) {
// Pass along the session token to the Warrant React SDK
const sessionToken = (await response.json()).warrantSessionToken;
setSessionToken(sessionToken);
router.replace("/my-dashboard");
} else {
console.log("Invalid email or password");
}
},
[email, password]
);
return (
<form onSubmit={login}>
<input
id="email"
type="email"
placeholder="Email"
value={email}
onChange={handleEmailUpdated}
required
/>
<input
id="password"
type="password"
placeholder="Password"
value={password}
onChange={handlePasswordUpdated}
required
/>
<button type="submit">Log In</button>
</form>
);
};
With the SDK initialized, we can now use the helper methods and components it provides to make access checks in our components. These are useful for determing whether or not to render privileged components, fetch extra data, and more based on the logged in user.
Use hasWarrant
hasWarrant
is one such helper method that's great for implementing conditional logic based on user access.
- Single Warrant
- Multiple Warrants
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { useWarrant } from "@warrantdev/react-warrant-js";
const Store = () => {
const { hasWarrant } = useWarrant();
const router = useRouter();
const { storeId } = router.query;
const myAdminFunction = async () => {
const userHasWarrant = await hasWarrant({
warrants: [{
objectType: "store",
objectId: storeId,
relation: "owner",
}]
});
if (userHasWarrant) {
//
// Code that only executes if user is "owner" of this store.
//
}
};
return <div>{/* ... */}</div>;
};
export default Store;
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { useWarrant } from "@warrantdev/react-warrant-js";
const Store = () => {
const { hasWarrant } = useWarrant();
const router = useRouter();
const { storeId } = router.query;
const myAdminFunction = async () => {
const userHasWarrant = await hasWarrant({
op: "anyOf",
warrants: [{
objectType: "store",
objectId: storeId,
relation: "owner",
}, {
objectType: "role",
objectId: "admin",
relation: "role",
}]
});
if (userHasWarrant) {
//
// Code that only executes if user is "owner" of this store or is a "member" of the role "admin".
//
}
};
return <div>{/* ... */}</div>;
};
export default Store;
Use ProtectedComponent
Wrap components with ProtectedComponent
to only render them if the user has access:
- Single Warrant
- Multiple Warrants
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { ProtectedComponent } from "@warrantdev/react-warrant-js";
const Store = () => {
const router = useRouter();
const { storeId } = router.query;
return (
<div>
{/* Only render the edit button if the user is an 'editor' of this store */}
<ProtectedComponent
warrants={[{
objectType: "store",
objectId: storeId,
relation: "editor",
}]}
>
<Link href={`/stores/${storeId}/edit`}>
<button>Edit Store</button>
</Link>
</ProtectedComponent>
{/* ... */}
</div>
);
};
export default Store;
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { ProtectedComponent } from "@warrantdev/react-warrant-js";
const Store = () => {
const router = useRouter();
const { storeId } = router.query;
return (
<div>
{/* Only render the edit button if the user is an 'editor' of this store or is a 'member' of the role 'manager' */}
<ProtectedComponent
op="anyOf"
warrants={[{
objectType: "store",
objectId: storeId,
relation: "editor",
}, {
objectType: "role",
objectId: "manager",
relation: "member",
}]}
>
<Link href={`/stores/${storeId}/edit`}>
<button>Edit Store</button>
</Link>
</ProtectedComponent>
{/* ... */}
</div>
);
};
export default Store;
Server-side Authorization
Server-side access control is critical in non-static web applications. Only access checks performed server-side can be relied upon when authorizing actions that can modify application state (ex: writing to a database).
The Warrant Node SDK and Warrant Express Middleware will help us add server-side authorization & access control to restrict access to pages and API routes.
Restrict Access to Pages (SSR)
To only allow certain users access to a page in your application, add getServerSideProps to perform an access check when the page is rendered server-side. If the user has access, the page will be rendered successfully. If they don't have access, they'll be redirected to another page.
In the example below, we use the Warrant Node SDK to check if the user has access to the /stores/:storeId
page before rendering it. If the user doesn't have access to the page, they'll be redirected to /stores
.
import { Client as Warrant } from "@warrantdev/warrant-node";
import { getLoggedInUserId } from "../../../utils/auth";
// This page will only be rendered if the user is a "viewer" of the store.
export const getServerSideProps: GetServerSideProps = async (context) => {
const { storeId: storeIdParam } = context.params;
const storeId = storeIdParam as string;
const warrant = new Warrant("<replace_with_your_api_key>");
// NOTE: getLoggedInUser is a method that
// returns the currently logged in user's id.
const isAuthorized = await warrant.isAuthorized({
warrants: [{
objectType: "store",
objectId: storeId,
relation: "viewer",
subject: {
objectType: "user",
objectId: getLoggedInUserId(context.req).toString()
}
}],
})
if (!isAuthorized) {
return {
redirect: {
destination: "/stores",
permanent: false,
},
};
}
const [store, _] = getStore(parseInt(storeId));
return {
props: {
store,
},
};
};
const Store = () => {
return <MyStore>
{/* ... */}
</MyStore>;
};
export default Store;
Protect API Routes
If your Next.js app includes API Routes that you want to authorize access to, use the Warrant Express Middleware to automatically make an access check before the code for each route is executed.
Configure Middleware
Configure the middleware by calling the createMiddleware
method. Pass in a getUserId
method that tells the middleware how to get the current user's id, a getParam
method that tells the middleware how to get url parameters (ex: storeId
), and an onAuthorizeFailure
method that defines what should happen if a user fails an access check.
import { createMiddleware } from "@warrantdev/warrant-express-middleware";
const authorize = createMiddleware({
clientKey: "<replace_with_your_api_key>",
getUserId: (req) => methodToGetUserId(req), // Replace this with a method that returns the current user's id
getParam: (req, paramName) => req.query[paramName],
onAuthorizeFailure: (req, res) => res.status(401).send("Unauthorized"),
});
Add Middleware to API Routes Using next-connect
createMiddleware
returns a method that generates middleware for specific access rules. Once you've configured the middleware, you can use it to protect specific API Routes using next-connect as shown in the example below.
import nc from "next-connect";
import { createMiddleware } from "@warrantdev/warrant-express-middleware";
const authorizeMiddleware = createMiddleware({
clientKey: "<replace_with_your_api_key>",
getUserId: (req) => methodToGetUserId(req), // Replace this with a method that returns the current user's id
getParam: (req, paramName) => req.query[paramName],
onAuthorizeFailure: (req, res) => res.status(401).send("Unauthorized"),
});
// Authorize that user is "editor" of store before
// allowing access to POST /api/stores/:storeId
const isAuthorized = authorizeMiddleware.hasAccess({
warrants: [{
objectType: "store",
objectId: "storeId",
relation: "editor",
}]
});
const authorization = nc().post(isAuthorized);
const handler = nc({
onNoMatch: (req, res) => res.status(404),
})
.use("/api/stores/:storeId", authorization)
.post(async (req, res) => {
const storeId = req.query.storeId;
const store = getStore(parseInt(storeId));
if (!store) {
res.status(404).send("Not Found");
return;
}
let updatedStore = res.json(updatedStore); // Update store
});
export default handler;