Make your App 10x Secure with Arcjet Protection Layer
Anmol Baranwal
Posted on August 8, 2024
There are countless websites in this era and most of these websites don't focus on security practices.
Later on, it can cause huge blunders but adding security practices takes a lot of time and it's a huge hassle.
Today, we will explore how Arcjet helps you enable those security practices instantly. It's easy and efficient!
I've structured this so that this can be a complete guide for you to get started with Arcjet.
Let's break it down.
What is covered?
In a nutshell, we are covering these topics in detail.
- What is Arcjet and why should you use it?
- Core concepts of Arcjet with useful use cases.
- Demo Testing using a Nextjs app (explanation + output + code).
- Arcjet Dashboard and how to test security rules.
What is Arcjet and why should you use it?
Arcjet provides a strong security layer for developers with just a few lines of code.
It's like a security platform that provides native security to protect web applications against common attacks such as SQL injection, XSS, CSRF and others. Every app needs it!
Why do we even need Arcjet?
As you know, React (just for example) is very popular and is quite safe by design because:
a. String variables in views are escaped automatically.
b. With JSX you pass a function as the event handler, rather than a string that can contain malicious code.
A typical attack won't work but there are multiple ways around it such as XSS via dangerouslySetInnerHTML
.
When you use dangerouslySetInnerHTML you need to make sure the content doesn't contain any javascript. React can't do here anything for you.
You can read more in this stackoverflow discussion.
Arcjet provides an SDK that deals with extra XSS vulnerabilities and adds necessary protections.
We (as developers) should understand that nothing is 100% secure so it's better to use a multi-layered approach to reduce as much risk as possible.
Plus, there is such an efficient solution with proper docs already available!
Let's talk about Architecture.
Arcjet mainly involves two components that outline its architecture:
a. Arcjet SDK installed into your app.
b. Arcjet Cloud API used to make or report decisions.
-→ 🎯 How do they work in sync?
The SDK integrates Arcjet into your application. Security rules are configured in your code using the SDK and they execute either through a middleware layer or directly from your application code.
The SDK includes a WebAssembly module which is used to analyze requests locally in your own environment.
Where possible, a decision is taken locally and then reported to Arcjet so that you can view the details in the Arcjet dashboard
. Decisions may also be cached in memory.
In many cases, Arcjet can make a decision locally and report that decision asynchronously to the Arcjet API. However, there are some cases where Arcjet needs to make a decision in the cloud.
The API has been designed for high performance and low latency and combines all the security functionality into a single request. As per the docs, the total latency will be less than 1 ms.
You can read complete details about their architecture on the official docs including proper request flow diagrams. I also recommend reading about Fingerprinting which is used to track clients across multiple requests for suspicious activity.
They provide four SDK references:
⚡ Arcjet Bun SDK.
You can read the quickstart guide and install it using bun add @arcjet/bun
.
You can also use it with Hono + Bun.
⚡ Arcjet Next.js SDK.
You can read the quickstart guide and install it using npm i @arcjet/next
.
⚡ Arcjet Node.js SDK.
You can read the quickstart guide and install it using npm i @arcjet/node
.
You can get started with Node.js + Hono or Node.js + Express.
⚡ Arcjet SvelteKit SDK.
You can read the quickstart guide and install it using npm i @arcjet/sveltekit
.
More SDK options will be available in the future.
The best part is that Arcjet doesn't interfere with the rest of the application. It's easy to install, does not add significant latency to requests, and doesn't even require changes to the application’s architecture. You should give it a try!
There are even complete guides in the docs to integrate easily with Auth.js, Clerk, Fly.io, NextAuth, OpenAI, and Vercel.
Arcjet is open source with 183 stars but I'm sure it will grow very rapidly.
2. Core concepts of Arcjet with useful use cases.
Arcjet provides a set of key primitives which can be used to build security functionality. Each primitive can be used independently or combined as part of a pre-configured product.
For instance, you can implement signup form protection by combining the rules of rate limiting, bot protection and email validation to give a more efficient workflow.
Let's explore each of them with use cases!
✅ Shield.
Arcjet Shield protects your application against common attacks, including the OWASP Top 10.
-→ 🎯 What is OWASP 10?
In case you don't know, The Open Web Application Security Project (OWASP) is an international non-profit organization dedicated to web application security.
The OWASP Top 10 is a regularly updated report outlining security concerns for web application security, focusing on the 10 most critical risks.
The report is put together by a team of security experts from all over the world and it's globally recognized by developers as the first step towards more secure coding!
Arcjet Shield analyzes every request to your application to detect suspicious activity. Once a certain suspicion threshold
is reached, subsequent requests from that client are blocked for some time.
It uses the idea of blocking traffic using the fingerprint of the client, which includes the IP address.
-→ 🎯 The use case is to protect against common categories such as:
- SQL injection (SQLi)
- Cross-site scripting (XSS)
- Local file inclusion (LFI)
- Remote file inclusion (RFI)
- PHP code injection
- Java code injection
- HTTPoxy
- Shellshock
- Unix/Windows shell injection
- Session fixation
You can also do it for a specific route or in the middleware.
Just be careful that Arcjet is not running multiple times per request. This can be avoided by excluding the API route from the middleware matcher.
The codebase will look like this if we implement it in Next.js middleware.
// -> middleware.ts
// Protect against common attacks e.g. SQL injection, XSS, CSRF
import arcjet, { createMiddleware, shield } from "@arcjet/next";
export const config = {
// matcher tells Next.js which routes to run the middleware on.
// This runs the middleware on all routes except for static assets.
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
// this is the main block
const aj = arcjet({
key: process.env.ARCJET_KEY!
rules: [
// Block common attacks e.g. SQL injection, XSS, CSRF
shield({
// Will block requests. Use "DRY_RUN" to log only
mode: "LIVE",
}),
],
});
// Pass existing middleware with optional existingMiddleware prop
export default createMiddleware(aj);
Read more about Arcjet Shield in the docs including how it works and how the attacks are detected.
✅ Rate Limiting.
Arcjet rate limiting allows you to define rules that limit the number of requests a client can make over some time.
The api route will look like this if we implement it in a Next.js app.
// -> /app/api/arcjet/route.ts
// Prevent API abuse and set quotas based on user sessions
import arcjet, { tokenBucket } from "@arcjet/next";
import { NextResponse } from "next/server";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
// Track requests by a custom user ID and IP address
characteristics: ["userId", "src.ip"],
rules: [
// Create a token bucket rate limit.
// Fixed and sliding window algorithms are also supported.
tokenBucket({
// Will block requests. Use "DRY_RUN" to log only
mode: "LIVE",
// Refill 5 tokens per interval
refillRate: 5,
// Refill every 10 seconds
interval: 10,
// Bucket maximum capacity of 10 tokens
capacity: 10,
}),
],
});
export async function GET(req: Request) {
// Replace with your authenticated user ID
const userId = "user123";
// Deduct 5 tokens from the bucket
const decision = await aj.protect(req, { userId, requested: 5 });
if (decision.isDenied()) {
return NextResponse.json(
{ error: "Too Many Requests", reason: decision.reason },
{ status: 429 },
);
}
return NextResponse.json({ message: "Hello world" });
}
A lot of configuration options are available like you can track requests by IP address. Similar to Shield, you can implement rate limiting in a route or Middleware.
-→ 🎯 Some of the useful use cases can be:
⚡ Avoid brute force by enforcing a rule where a user can only attempt to log in 5 times in 5 minutes. This prevents an attacker from trying multiple username or password combinations.
⚡ Prevent API clients from making too many requests to avoid overloading your API.
⚡ You can use it to implement of concept of quotas in different tiers like only 1k requests are allowed per day in a free version.
Read more about the concepts, and the algorithms involved in the docs.
✅ Bot Protection.
Arcjet bot protection allows you to detect traffic by automated clients or bots and block them based on the rules.
Bots can be good such as search engine crawlers or monitoring agents or bad (such as scrapers or automated scripts). You might also want to allow automated clients access to your API (even though they might seem like bots) but deny access to a signup form.
Bad bots pretend to be real clients and use various mechanisms to evade bot detection. So, it's impossible to create a system that can block all bots and achieve 100% accuracy.
Arcjet allows you to configure bot protection so you can decide which bots are allowed.
You should read about type of bots (docs) you wish to block in the bots.block
configuration options including AUTOMATED
, LIKELY_AUTOMATED
, LIKELY_NOT_A_BOT
, VERIFIED_BOT
.
This will reduce bot traffic and give you more control over which requests reach your application.
// -> middleware.ts
// Detect and block automated clients, scrapers & bots
import arcjet, { createMiddleware, detectBot } from "@arcjet/next";
export const config = {
// matcher tells Next.js which routes to run the middleware on.
// This runs the middleware on all routes except for static assets.
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
detectBot({
// Will block requests. Use "DRY_RUN" to log only
mode: "LIVE",
// Blocks all automated clients
block: ["AUTOMATED"],
}),
],
});
// Pass existing middleware with optional existingMiddleware prop
export default createMiddleware(aj);
-→ 🎯 Some of the useful use cases can be:
⚡ In e-commerce, bots can abuse promotional offers and disturb product availability. Bot protection rules are needed to prevent the abuse.
⚡ As I told you earlier, it's important to ensure that bots don't fill up survey forms.
⚡ It can prevent credential stuffing attacks or using any kind of automated scripts to try thousands of username and password combinations obtained from previous data breaches.
Read more about the concepts, and the algorithms involved in the docs.
You can watch this tutorial video by David to understand more!
✅ Email validation and verification.
Arcjet allows you to validate & verify an email address. This is useful for preventing users from signing up with fake email addresses and can significantly reduce the amount of spam accounts.
It performs it in 2 simple steps:
a. Validation.
This runs locally within the SDK and validates the email address is in the correct format. Validation options are configurable as described within the SDK documentation.
b. Verification.
If the email syntax is valid, the SDK will pass the email address to the Arcjet cloud API to verify the email address. You can filter emails with some really good options like:
-→ MX validation
: Checks if the domain has valid MX records.
-→ Email type
: Checks if the email address is free, disposable or role-based.
-→ Has Gravatar
: Checks if the email address has a Gravatar image associated with it. A little strict maybe but a great option!
You can decide what to do next based on the metadata returned from the SDK.
// Validate email addresses are correct & can receive mail
import arcjet, { validateEmail } from "@arcjet/next";
import { NextResponse } from "next/server";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
validateEmail({
// Will block requests. Use "DRY_RUN" to log only
mode: "LIVE",
// Blocks disposable, no MX records, and invalid emails
block: ["DISPOSABLE", "NO_MX_RECORDS", "INVALID"],
}),
],
});
export async function POST(req: Request) {
const decision = await aj.protect(req, {
// Pass the email address, such as from a form submission
email: "fake@arcjet.ai",
});
if (decision.isDenied()) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
return NextResponse.json({
message: "Hello world",
});
}
We can further pass the message based on the email type and deal with it accordingly. I've done it while testing in the next section!
I love the overall process and you can read about the concepts involved in the docs.
3. Demo Testing using a Nextjs app.
I've attached the demo repo and the deployed link at the end.
I won't be discussing the basic structure like header, or frontend page that is not relevant. You can just check the repo for that.
Let's do it.
Main page. (
src/app/page.tsx
)
import Link from 'next/link'
import { buttonVariants } from '@/components/ui/button'
import { Icons } from '@/components/icons'
export default function Home() {
return (
<div className="flex h-screen flex-col items-center bg-black pt-28 text-center text-white">
<div className="absolute left-6 top-6">
<Link href={'https://twitter.com/Anmol_Codes'}>
<div className="flex items-center justify-center text-white">
<Icons.X className="mr-2 h-4 w-4 text-white" /> Made by Anmol
</div>
</Link>
</div>
<h2 className="bg-gradient-to-r from-[#A855F7] to-[#D9ACF5] bg-clip-text pb-2 text-3xl font-bold tracking-tighter text-transparent sm:text-4xl xl:text-5xl/none">
Demo of core features of Arcjet
</h2>
<p className="tracking-tigher px-2 pt-6 text-lg leading-8 text-gray-300 lg:px-72">
The only security layer that your app will ever need. <br />
Find the code on{' '}
<span className="border-b">
<Link
href={'https://github.com/Anmol-Baranwal/arcjet-demo'}
target="_blank"
>
GitHub
</Link>
</span>
.
</p>
<div className="flex gap-4 pt-12">
<Link
href="/signup-form"
className={buttonVariants({ variant: 'primary' })}
>
Signup form protection
</Link>
<Link
href="/bots-protection"
className={buttonVariants({ variant: 'primary' })}
>
Bot protection
</Link>
<Link
href="/rate-limiting"
className={buttonVariants({ variant: 'primary' })}
>
Rate limiting
</Link>
<Link href="/shield" className={buttonVariants({ variant: 'primary' })}>
Shield Demonstration
</Link>
</div>
</div>
)
}
The output will be as shown.
Each button will be linked to an inner page and each will trigger a different API request through a route handler.
🎯 Bot Protection.
First, we need to build an API route for bot protection: src/app/api/bot-protection/route.ts
.
import arcjet, { detectBot } from '@arcjet/next'
import { NextResponse } from 'next/server'
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
characteristics: ['userId'], // track requests by a custom user ID
rules: [
detectBot({
mode: 'LIVE', // will block requests. Use "DRY_RUN" to log only
block: ['AUTOMATED', 'LIKELY_AUTOMATED'], // blocks all automated clients (LIKELY_AUTOMATED is also another option)
}),
],
})
export async function GET(req: Request) {
const userId = 'user123'
const decision = await aj.protect(req, { userId })
if (decision.isDenied() && decision.reason.isBot()) {
return NextResponse.json(
{ error: 'Bot Detected in request', reason: decision.reason },
{ status: 429 }
)
}
return NextResponse.json({ message: 'OK' })
}
You can also enable Bot Protection across your entire Next.js app by implementing it in Middleware.
✅ Explanation:
It blocks two types of bots that are Automated
where the SDK is sure that the request was made by an automated bot and LIKELY_AUTOMATED
where the SDK has some evidence that the request was made by an automated bot. There are other options which you can read in the docs.
We're using a temporary ID which is always configurable based on your choice.
When the decision is denied and the reason is a bot, then we just send an error message.
...
if (decision.isDenied() && decision.reason.isBot()) {
return NextResponse.json(
{ error: 'Bot Detected in request', reason: decision.reason },
{ status: 429 }
)
}
return NextResponse.json({ message: 'OK' })
For some cases, a curl
request should be allowed so it's possible to configure it in the rule. You can remove any of these rules by listing them in the patterns
remove
configuration property.
rules: [
detectBot({
mode: 'LIVE', // will block requests. Use "DRY_RUN" to log only
block: ['AUTOMATED', 'LIKELY_AUTOMATED'], // blocks all automated clients (LIKELY_AUTOMATED is also another option)
patterns: {
remove: [
// Removes the datadog agent from the list of bots so it will be
// considered as ArcjetBotType.LIKELY_NOT_A_BOT
'datadog agent',
// Also allow curl clients to pass through. Matches a user agent
// string with the word "curl" in it
'^curl',
],
},
}),
],
You can simply type this command in curl and you will see that the bot is detected. (I've removed the pattern in the current codebase)
curl -v https://arcjet-demo.vercel.app/api/bot-protection
🎯 Rate Limiting.
This allows you to define rules that limit the number of requests a client can make over a period of time.
There are three algorithms mentioned in the docs which are Fixed Window, Sliding Window and Token Budget.
First, we need a route handler to trigger that request: src/app/api/rate-limiting/route.ts
import arcjet, { tokenBucket } from '@arcjet/next'
import { NextResponse } from 'next/server'
// Configure Arcjet
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
characteristics: ['userId'], // you can also track requests by IP address using "ip.src"
rules: [
tokenBucket({
mode: 'LIVE', // will block requests. Use "DRY_RUN" to log only
match: '/api/rate-limiting', // match all requests to /api/rate-limiting
refillRate: 2, // refill 2 tokens per interval
interval: 60, // refill every 60 seconds
capacity: 4, // bucket maximum capacity of 4 tokens
}),
],
})
export async function GET(req: Request) {
const userId = 'user123' // Replace with your authenticated user ID
const decision = await aj.protect(req, { userId, requested: 1 }) // Deduct 1 token from the bucket
console.log('Arcjet SDK decision', decision.conclusion)
let message = ''
let remaining = 0
let reset = 0
if (decision.reason.isRateLimit()) {
const resetTime = decision.reason.resetTime
remaining = decision.reason.remaining
if (resetTime) {
const seconds = Math.floor((resetTime.getTime() - Date.now()) / 1000)
reset = seconds
message = `Reset in ${seconds} seconds.`
}
}
if (decision.isDenied() && decision.reason.isRateLimit()) {
return NextResponse.json(
{ error: `HTTP 429: Too many requests. ${message}`, remaining, reset },
{ status: 429 }
)
}
return NextResponse.json(
{ message: 'HTTP 200: OK', remaining, reset },
{ status: 200 }
)
}
✅ Explanation:
Imagine it's 8:00:00
and you make 4 requests, further requests will be blocked until 8:01:00
when the bucket refills by 2 tokens.
You could then make 2 requests over the following 60 seconds. In case of no requests, the bucket will refill to 4 tokens at 8:02:00
. No further tokens will be added after it reaches 4 tokens.
That's the concept of the token bucket algorithm.
Let's build the frontend to trigger the API route to check if it works properly:
'use client'
import Header from '@/components/header'
import { buttonVariants } from '@/components/ui/button'
import Link from 'next/link'
import { useState } from 'react'
export default function RateLimiting() {
const [message, setMessage] = useState('')
const handleClick = async () => {
const res = await fetch('/api/rate-limiting')
const data = await res.json()
if (res.status === 429) {
setMessage(data.error)
} else {
setMessage(
`HTTP 200: OK. ${data.remaining} requests remaining. Reset in ${data.reset} seconds.`
)
}
}
return (
<div className="min-h-screen bg-black">
<Header
title="Rate Limiting Example"
docsLink="/https://docs.arcjet.com/rate-limiting/quick-start/nextjs"
/>
<div className="container pb-20 pt-12">
<button
onClick={handleClick}
className={buttonVariants({ variant: 'primary' })}
>
Click ME to Call API
</button>
{message && (
<div className="mt-4 w-fit rounded border border-dashed border-white bg-transparent p-4 text-white">
<p>{message}</p>
</div>
)}
</div>
<p className="container space-y-1 text-gray-400">
<div>
The limit is set to 4 requests every 60 seconds. <br />
After this, 2 requests are refilled every minute until it reaches 4
requests. Use it wisely :)
</div>
<div className="pt-1">
Rate limits can be{' '}
<Link
href={'https://docs.arcjet.com/reference/nextjs#ad-hoc-rules'}
className="border-b border-gray-400"
>
dynamically adjusted{' '}
</Link>
e.g. to set a limit based on the authenticated user.
</div>
</p>
</div>
)
}
Let's see a couple of snapshots to see how it behaves.
You can further test it yourself on the deployed link.
I was trying to build a real timer in the description but it has been a little confusing so I kept it simple to avoid complex chunk code.
🎯 Arcjet Shield.
Let's build the API route for demonstrating the arcjet shield: src/app/api/shield/route.ts
.
import arcjet, { shield } from '@arcjet/next'
import { NextResponse } from 'next/server'
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
characteristics: ['userId'], // track requests by a custom user ID
rules: [
shield({
mode: 'LIVE', // will block the request
}),
],
})
export async function GET(req: Request) {
const userId = 'user123'
const decision = await aj.protect(req, { userId })
if (decision.isDenied() && decision.reason.isShield()) {
return NextResponse.json(
{
error: 'You seem suspicious :(',
reason: decision.reason,
},
{ status: 403 }
)
}
return NextResponse.json({ message: 'OK' })
}
The code is simple. You can simulate an attack using curl
and this error will be shown!
You can use this command.
curl -v -H "x-arcjet-suspicious: true" http://arcjet-demo.vercel.app/api/shield
🎯 Signup Form Protection.
Arcjet signup form protection is carried out by combining rate limiting
, bot protection
, and email validation
to protect your signup forms from abuse.
Let's create an API route on the path: src/app/api/signup-form/route.ts
. Comments are there to make code more understandable!
import arcjet, { protectSignup } from '@arcjet/next'
import { NextResponse } from 'next/server'
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
rules: [
protectSignup({
email: {
mode: 'LIVE', // will block requests. Use "DRY_RUN" to log only
// Block emails that are disposable, invalid, or have no MX records
block: ['DISPOSABLE', 'INVALID', 'NO_MX_RECORDS'], // See https://docs.arcjet.com/email-validation/concepts#email-types
},
bots: {
mode: 'LIVE',
// Block clients that we are sure are automated
block: ['AUTOMATED'],
},
// It would be unusual for a form to be submitted more than 5 times in 5
// minutes from the same IP address
rateLimit: {
// uses a sliding window rate limit
mode: 'LIVE',
interval: '5m', // counts requests over a 10 minute sliding window
max: 5, // allows 15 submissions within the window
},
}),
],
})
export async function POST(req: Request) {
const data = await req.json()
const email = data.email
const decision = await aj.protect(req, {
email,
})
console.log('Arcjet decision: ', decision)
if (decision.isDenied()) {
let message = 'Request cannot be allowed.'
if (decision.reason.isEmail()) {
if (decision.reason.emailTypes.includes('INVALID')) {
message = 'Email address is invalid.'
} else if (decision.reason.emailTypes.includes('DISPOSABLE')) {
message = 'Disposable email addresses are not allowed.'
} else if (decision.reason.emailTypes.includes('NO_MX_RECORDS')) {
message =
'Your email domain does not have an MX record. Please check for any typos!!'
}
} else if (decision.reason.isRateLimit()) {
const reset = decision.reason.resetTime
if (reset === undefined) {
message = 'Too many requests. Please try again later.'
} else {
// no of seconds between reset Date and now
const seconds = Math.floor((reset.getTime() - Date.now()) / 1000)
const minutes = Math.ceil(seconds / 60)
if (minutes > 1) {
message = `Too many requests. Please try again in ${minutes} minutes.`
} else {
message = `Too many requests. Please try again in ${seconds} seconds.`
}
}
} else {
message = 'Forbidden'
}
// if (decision.ip.hasCountry()) {
// message += ` PS: Hello from ${decision.ip.country}.`
// }
return NextResponse.json(
{ message, reason: decision.reason },
{ status: decision.reason.isRateLimit() ? 429 : 400 }
)
}
return NextResponse.json({ message: 'Email is Valid and has MX Records' })
}
We have passed a different message based on the type of email. Let's see the page structure and the output.
Create a frontend page to trigger the above API route at: src/app/signup-form/page.tsx
.
'use client'
import React, { useState, type FormEvent } from 'react'
import Header from '@/components/header'
import { Input } from '@/components/ui/input'
import { Button, buttonVariants } from '@/components/ui/button'
export default function Page() {
const [isLoading, setIsLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
const [data, setData] = useState('')
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
setIsLoading(true)
setError(null)
setData('')
try {
const formData = new FormData(event.currentTarget)
const response = await fetch('/api/signup-form', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(formData)),
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(`${response.status}: ${error.message}`)
}
// Handle response if necessary
const data = await response.json()
setData(data.message)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
// Capture the error message to display to the user
setError(error.message)
console.error(error)
} finally {
setIsLoading(false)
}
}
return (
<div>
<div className="min-h-screen bg-black">
<Header
title="Form Protection Example"
docsLink="/https://docs.arcjet.com/signup-protection/quick-start/nextjs"
/>
<form onSubmit={onSubmit} className="px-6">
<Input
type="text"
defaultValue={'invalid@email'}
name="email"
id="email"
className="my-4 w-60 border-dashed border-white bg-transparent text-white"
/>
<Button
type="submit"
disabled={isLoading}
className={buttonVariants({ variant: 'primary' })}
>
{isLoading ? 'Loading...' : 'Submit Form'}
</Button>
</form>
<div className="px-6 pb-16">
{data && <div className="py-6 text-green-400">{data}</div>}
{error && <div className="py-6 text-red-400">{error}</div>}
<h2 className="mt-6 text-xl font-bold text-white">Test emails</h2>
<p className="py-4 text-gray-300">
Email validation 1/3rd of the process. Try these emails to see how
it works:
</p>
<ul className="ms-8 list-outside list-disc">
<li className="text-gray-400">
<code className="rounded-md border border-gray-800 bg-transparent p-1 text-gray-400">
invalid.@arcjet
</code>{' '}
– is an invalid email address.
</li>
<li className="pt-2 text-gray-400">
<code className="rounded-md border border-gray-800 bg-transparent p-1 text-gray-400">
test@0zc7eznv3rsiswlohu.tk
</code>{' '}
– is from a disposable email provider.
</li>
<li className="pt-2 text-gray-400">
<code className="rounded-md border border-gray-800 bg-transparent p-1 text-gray-400">
nonexistent@arcjet.ai
</code>{' '}
– is a valid email address & domain, but has no MX records.
</li>
</ul>
</div>
</div>
</div>
)
}
This is how it looks.
There are various emails (picked from the arcjet example) just to show how it works. Let's see the output!
🎯 How to combine rules?
You can check the code at: src/app/api/arcjet/route.ts
.
We can combine the rules like this.
import arcjet, { tokenBucket, detectBot, shield } from '@arcjet/next'
import { NextResponse } from 'next/server'
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
characteristics: ['userId'], // track requests by a custom user ID
rules: [
tokenBucket({
mode: 'LIVE', // will block requests. Use "DRY_RUN" to log only
refillRate: 2, // refill 2 tokens per interval
interval: 40, // refill every 40 seconds
capacity: 2, // bucket maximum capacity of 2 tokens
}),
detectBot({
mode: 'LIVE', // will block requests. Use "DRY_RUN" to log only
block: ['AUTOMATED'], // blocks all automated clients
}),
shield({
mode: 'LIVE', // this will block, use DRY_RUN if you want to allow the request
}),
],
})
Then we can just handle the message based on the rule that was denied.
export async function GET(req: Request) {
const userId = 'user123' // Replace with your authenticated user ID
const decision = await aj.protect(req, { userId, requested: 1 }) // Deduct 1 token from the bucket
// console.log('Arcjet SDK decision', decision)
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
return NextResponse.json(
{ error: 'Too Many Requests', reason: decision.reason },
{ status: 429 }
)
} else if (decision.reason.isBot()) {
return NextResponse.json(
{ error: 'Bot Detected in request', reason: decision.reason },
{ status: 429 }
)
} else if (decision.reason.isShield()) {
return NextResponse.json(
{
error: 'You seem suspicious :(',
// Useful for debugging, but don't return it to the client in
// production
//reason: decision.reason,
},
{ status: 403 }
)
} else if (decision.reason.isShield()) {
return NextResponse.json(
{
error: 'You seem suspicious :(',
reason: decision.reason,
},
{ status: 403 }
)
}
}
return NextResponse.json({ message: 'OK' })
}
It's as simple and efficient as that!
- GitHub: github.com/Anmol-Baranwal/arcjet-demo
- My Deployed: https://arcjet-demo.vercel.app
- Arcjet Example: example.arcjet.com
4. Arcjet Dashboard and how to test security rules.
🎯 Arcjet Dashboard.
Just to let you know, all the analytics and the requests are shown in the dashboard. That makes it much better and easier to track the requests from a single location.
🎯 Testing security rules.
Testing is the most important part, we need to make sure those security rules work without breaking the production.
One simple way to do it is by using Newman CLI which is a command line runner for Postman. It allows you to effortlessly run and test a Postman collection directly from the command line.
Watch the below tutorial to understand how to test security rules using Newman!
I think it's safe to say that Arcjet is the best protection layer that you can give to your app.
For me (as a developer), it will make my app at least 10x secure.
I hope you loved the breakdown of Arcjet and let me know in the comments if you are planning to use it.
Disclaimer: Arcjet contacted me to ask if I would like to try their beta and then write about the experience. They paid me for my time, but didn't influence this writeup.
Have a great day! Till next time.
You can join my community for developers and tech writers at dub.sh/opensouls.
Posted on August 8, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
July 20, 2020