Make your App 10x Secure with Arcjet Protection Layer

anmolbaranwal

Anmol Baranwal

Posted on August 8, 2024

Make your App 10x Secure with Arcjet Protection Layer

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.

excited to try it


What is covered?

In a nutshell, we are covering these topics in detail.

  1. What is Arcjet and why should you use it?
  2. Core concepts of Arcjet with useful use cases.
  3. Demo Testing using a Nextjs app (explanation + output + code).
  4. Arcjet Dashboard and how to test security rules.

Star Arcjet ⭐


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!

arcjet

 

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!

arcjet sdk options

 

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.

arcjet post request analysis

asynchronous report is made to the Arcjet API for post-request analysis

 

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:

sdk ref

 

⚡ 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.

Star Arcjet ⭐


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);
Enter fullscreen mode Exit fullscreen mode

shield

the shield rule with options

 

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" });
}
Enter fullscreen mode Exit fullscreen mode

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.

rate limiting

the rate limiting rule with options

 

-→ 🎯 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);
Enter fullscreen mode Exit fullscreen mode

bot protection

the bot protection rule with options

 

-→ 🎯 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",
  });
}
Enter fullscreen mode Exit fullscreen mode

email validation

the email validation rule with options

 

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

The output will be as shown.

main page

Each button will be linked to an inner page and each will trigger a different API request through a route handler.

 

🎯 Bot Protection.

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' })
}
Enter fullscreen mode Exit fullscreen mode

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' })
Enter fullscreen mode Exit fullscreen mode

browser api request to check bot detection

browser request to the api is fine

 

curl request denied

making a req using curl blocks the request

 

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',
        ],
      },
    }),
  ],
Enter fullscreen mode Exit fullscreen mode

curl is allowed for bot protection

bot is not detected when curl is allowed

 

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
Enter fullscreen mode Exit fullscreen mode

 

🎯 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 }
  )
}
Enter fullscreen mode Exit fullscreen mode

✅ 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

the rate limiting inner page frontend

the rate limiting inner page frontend

 

Let's see a couple of snapshots to see how it behaves.

4 requests in the start

4 requests in the start

 

Once you exhaust that limit

Once you exhaust that limit

 

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.

page

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' })
}
Enter fullscreen mode Exit fullscreen mode

The code is simple. You can simulate an attack using curl and this error will be shown!

shield

You can try it using curl (you will see this after 5 times)

 

You can use this command.

curl -v -H "x-arcjet-suspicious: true" http://arcjet-demo.vercel.app/api/shield
Enter fullscreen mode Exit fullscreen mode

 

🎯 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' })
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

This is how it looks.

output of signup protection page

There are various emails (picked from the arcjet example) just to show how it works. Let's see the output!

email address is invalid

email address is invalid

 

Disposable email address

Disposable email address

 

email domain does not have an MX record

email domain does not have an MX record

 

Email is Valid and has MX Records

Email is Valid and has MX Records

 

rate limitation crossed (5 requests in 5 minutes)

rate limitation crossed (5 requests in 5 minutes)

 

🎯 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
    }),
  ],
})
Enter fullscreen mode Exit fullscreen mode

 

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' })
}
Enter fullscreen mode Exit fullscreen mode

It's as simple and efficient as that!

 


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.

arcjet dashboard request logs

arcjet dashboard request logs

 

arcjet dashboard total requests analytics

arcjet dashboard total requests analytics

 

arcjet dashboard filter by conclusion

arcjet dashboard filter by conclusion

 

🎯 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.

If you loved this,
please follow me for more :)
profile of Twitter with username Anmol_Codes profile of GitHub with username Anmol-Baranwal profile of LinkedIn with username Anmol-Baranwal

Ending GIF waving goodbye

💖 💪 🙅 🚩
anmolbaranwal
Anmol Baranwal

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