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 must wrap your application in the WarrantProvider
component, create a session token for the user, and pass session token to the React SDK using the setSessionToken
method.
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
Warrant Sessions
To finish initializing the SDK for a given user, you must create a session token for the user and call the setSessionToken
method with the created session token. This allows the SDK to make access check requests to the Warrant API on behalf of the user. Refer to our guide on Creating Sessions to learn how to generate session tokens for users.
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, you can now use the helper methods and components it provides to make access checks throughout your application. These are useful for determing whether or not to render privileged components, fetch extra data, and more based on the logged in user's privileges.
Identity Provider Sessions
If you are using an identity provider (IdP) for your application's authentication, you can use tokens generated by the provider in place of a session token. To do so, make sure you have your JWKS endpoint configured correctly. Refer to Identity Provider Sessions to read more about configuring the use of third party tokens. Once configured, you can call setSessionToken
with your IdP token in the authentication flow:
setSessionToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ")
Add Access Checks to Your App
Sometimes pages or components should only be shown to users who have elevated levels of access. In other cases, a page or component should be accessible to all users, but not all of its functionality (e.g. hiding an 'Edit' button from read-only users or only fetching privileged data for admin users). For access-control-related conditional logic and conditional rendering of content, the Warrant React SDK provides:
check
,checkMany
,hasPermission
, andhasFeature
utility methods to add access checks within component logic.ProtectedComponent
,PermissionProtectedComponent
, andFeatureProtectedComponent
wrapper components to conditionally render elements on a page or in a component.WithWarrantCheck
,WithPermissionCheck
, andWithFeatureCheck
Higher Order Components (HOCs) to wrap components with for conditional rendering, useful for creating protected routes with your favorite router.
check
and checkMany
Make specific access checks within a component using check
and checkMany
:
- Single Warrant
- Multiple Warrants
import React, { useEffect } from "react";
import { useWarrant } from "@warrantdev/react-warrant-js";
const MyComponent = () => {
const { check } = useWarrant();
useEffect(() => {
const fetchProtectedInfo = async () => {
// Only fetch protected info from server if
// user is a "viewer" of the info object "protected_info".
const userHasWarrant = await check({
object: {
objectType: "info",
objectId: "protected_info",
},
relation: "viewer",
});
if (userHasWarrant) {
// request protected info from server
}
};
fetchProtectedInfo();
});
return (
<div>{protectedInfo && <ProtectedInfo>{protectedInfo}</ProtectedInfo>}</div>
);
};
export default MyComponent;
import React, { useEffect } from "react";
import { useWarrant } from "@warrantdev/react-warrant-js";
const MyComponent = () => {
const { checkMany } = useWarrant();
useEffect(() => {
const fetchProtectedInfo = async () => {
// Only fetch protected info from server if
// user is a "viewer" of the info object "protected_info" or is a "member" of the role "admin".
const userHasAccess = await checkMany({
op: "anyOf",
warrants: [
{
objectType: "info",
objectId: "protected_info",
relation: "viewer",
},
{
objectType: "role",
objectId: "admin",
relation: "member",
},
],
});
if (userHasAccess) {
// request protected info from server
}
};
fetchProtectedInfo();
});
return (
<div>{protectedInfo && <ProtectedInfo>{protectedInfo}</ProtectedInfo>}</div>
);
};
export default MyComponent;
ProtectedComponent
Wrap components and markup with ProtectedComponent
to only render them if the user has the required warrant:
- Single Warrant
- Multiple Warrants
import React from "react";
import { ProtectedComponent } from "@warrantdev/react-warrant-js";
const MyComponent = () => {
return (
<div>
<MyPublicComponent />
{/* hides MyProtectedComponent unless the user is a "viewer" of myObject with id object.id */}
<ProtectedComponent
warrants={[
{
object: {
objectType: "myObject",
objectId: object.id,
},
relation: "viewer",
},
]}
>
<MyProtectedComponent />
</ProtectedComponent>
</div>
);
};
export default MyComponent;
import React from "react";
import { ProtectedComponent } from "@warrantdev/react-warrant-js";
const MyComponent = () => {
return (
<div>
<MyPublicComponent />
{/* hides MyProtectedComponent unless the user is a "viewer" of myObject with id object.id or is a "member" of the role "admin" */}
<ProtectedComponent
op="anyOf"
warrants={[
{
object: {
objectType: "myObject",
objectId: object.id,
},
relation: "viewer",
},
{
object: {
objectType: "role",
objectId: "admin",
},
relation: "member",
},
]}
>
<MyProtectedComponent />
</ProtectedComponent>
</div>
);
};
export default MyComponent;
PermissionProtectedComponent
Wrap components and markup with PermissionProtectedComponent
to only render them if the user has the required permission:
import React from "react";
import { ProtectedComponent } from "@warrantdev/react-warrant-js";
const MyComponent = () => {
return (
<div>
<MyPublicComponent />
{/* hides MyProtectedComponent unless the user has permission 'view-protected-component' */}
<PermissionProtectedComponent permissionId="view-protected-component">
<MyProtectedComponent />
</ProtectedComponent>
</div>
);
};
export default MyComponent;
FeatureProtectedComponent
Wrap components and markup with FeatureProtectedComponent
to only render them if the user has the required feature:
import React from "react";
import { ProtectedComponent } from "@warrantdev/react-warrant-js";
const MyComponent = () => {
return (
<div>
<MyPublicComponent />
{/* hides MyProtectedComponent unless the user has feature 'protected-component-feature' */}
<FeatureProtectedComponent featureId="protected-component-feature">
<MyProtectedComponent />
</ProtectedComponent>
</div>
);
};
export default MyComponent;
withWarrantCheck
Wrap components with the withWarrantCheck
HOC to ensure that a check for the required warrant is always performed before the component is rendered in any context of your application. This can be useful for building protected routes that are only accessible to users with the required warrant(s).
import React from "react";
import { withWarrantCheck } from "@warrantdev/react-warrant-js";
const MySecretComponent = () => {
return <div>Super secret text</div>;
};
// Only render MySecretComponent if the user
// can "view" the component "MySecretComponent".
export default withWarrantCheck(MySecretComponent, {
warrants: [
{
object: {
objectType: "component",
objectId: "MySecretComponent",
},
relation: "view",
},
],
redirectTo: "/",
});
withPermissionCheck
Wrap components with the withPermissionCheck
HOC to ensure that a check for the required permission is always performed before the component is rendered in any context of your application. This can be useful for building protected routes that are only accessible to users with the required permission.
import React from "react";
import { withPermissionCheck } from "@warrantdev/react-warrant-js";
const MySecretComponent = () => {
return <div>Super secret text</div>;
};
// Only render MySecretComponent if the user
// has permission "view-secret-component".
export default withPermissionCheck(MySecretComponent, {
permissionId: "view-secret-component",
redirectTo: "/",
});
withFeatureCheck
Wrap components with the withFeatureCheck
HOC to ensure that a check for the required feature is always performed before the component is rendered in any context of your application. This can be useful for building protected routes that are only accessible to users with the required feature.
import React from "react";
import { withFeatureCheck } from "@warrantdev/react-warrant-js";
const MySecretComponent = () => {
return <div>Super secret text</div>;
};
// Only render MySecretComponent if the user
// has feature "secret-component".
export default withFeatureCheck(MySecretComponent, {
featureId: "secret-component",
redirectTo: "/",
});
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 userId.
const isAuthorized = await warrant.check({
object: {
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;
Usage in API Routes
If your Next.js app includes API Routes, you can use the Warrant Node SDK in your route handlers just as you would in any Node.js-based server-side runtime. The Warrant Node SDK is particularly useful for writing middleware functions to protect access to specific API routes behind a warrant, a permission, or even a feature.
Creating a Middleware using warrant-node
Use the Warrant Node SDK to create a higher-order middleware function that returns a native Next.js middleware which will automatically check for a warrant, permission, or feature before the handler is executed. You can wrap API route handlers with this function to add access control to your API routes.
import { WarrantClient } from "@warrantdev/warrant-node";
/*
* withPermission returns a Nextjs middleware function
* which will check that the logged in user has the
* required permission before executing the route handler.
*/
export function withPermission(permissionId, handler) {
const warrant = new WarrantClient({
apiKey: "<replace_with_your_api_key>",
});
return async function (req, res) {
/*
* Check if the user has the required permission.
* If the user has the required permission,
* execute the passed in route handler.
* If the user does NOT have the required
* permission, return a 403 - Forbidden.
*
* NOTE: getLoggedInUser() should return
* the currently logged in user's userId.
*/
if (
!(await warrant.hasPermission({
permissionId: permissionId,
subject: {
objectType: "user",
objectId: getLoggedInUserId(),
},
}))
) {
return new NextResponse(
JSON.stringify({ success: false, message: "access denied" }),
{ status: 403, headers: { "content-type": "application/json" } }
);
}
};
res.next();
return;
}
Wrap API Routes with Middleware
Wrap your API route handlers with your custom Warrant middleware to protect them behind a permission:
const myApiHandler = (req, res) => {
// your handler code
};
/*
* Wrap the handler with the withPermission middleware
* before exporting it. This means the handler will only
* execute if the user has the "edit-stores" permission.
* Otherwise, the middleware will automatically return a
* 403 (Forbidden) response to the client.
*/
export default withPermission("edit-stores", myApiHandler);