GitHub
Open Github

UseAuth

JWT auth with brute force protection and bot detection.

Installation

pnpm add @hookraft/useauth

useAuth

The core hook. Handle login, logout, JWT decoding, token refresh, and brute force protection in one place.

import { useAuth } from "@hookraft/useauth"
const auth = useAuth({
  onLogin: async (credentials) => await signIn(credentials),
  onLogout: async () => await signOut(),
  onRefresh: async () => await refreshToken(),
  onTokenExpired: () => toast.error("Session expired"),
  onError: (error) => toast.error("Login failed"),
  maxAttempts: 5,
  lockoutDuration: 30,
  minAttemptInterval: 500,
  storage: "localStorage",
  decodeToken: true,
})

Full Example

function LoginPage() {
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")

  const auth = useAuth({
    onLogin: async (credentials) => {
      const res = await fetch("/api/auth/login", {
        method: "POST",
        body: JSON.stringify(credentials),
      })
      const data = await res.json()
      return { token: data.token, user: data.user }
    },
    onLogout: async () => {
      await fetch("/api/auth/logout", { method: "POST" })
    },
    onRefresh: async () => {
      const res = await fetch("/api/auth/refresh", { method: "POST" })
      const data = await res.json()
      return data.token
    },
    onTokenExpired: () => toast.error("Your session expired. Please log in again."),
    onError: () => toast.error("Invalid credentials"),
    maxAttempts: 5,
    lockoutDuration: 30,
    minAttemptInterval: 500,
    storage: "localStorage",
    storageKey: "my_app_token",
    decodeToken: true,
  })

  if (auth.is("authenticated")) {
    return (
      <div>
        <p>Welcome, {auth.user?.name}</p>
        <p>Session expires: {auth.tokenExpiresAt?.toLocaleString()}</p>
        <button onClick={auth.logout}>Logout</button>
      </div>
    )
  }

  return (
    <div>
      <input
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        placeholder="Password"
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />

      <button
        onClick={() => auth.login({ email, password })}
        disabled={auth.is("loading") || auth.is("locked")}
      >
        {auth.is("loading") ? "Logging in..." : "Login"}
      </button>

      {auth.is("error") && (
        <p>{auth.attempts} failed attempt(s). {5 - auth.attempts} remaining before lockout.</p>
      )}

      {auth.is("locked") && auth.lockoutReason === "max_attempts" && (
        <p>Too many failed attempts. Try again in {auth.remainingTime}s</p>
      )}

      {auth.is("locked") && auth.lockoutReason === "bot_detection" && (
        <p>Suspicious activity detected. Please slow down.</p>
      )}
    </div>
  )
}

Options

PropTypeDefault
onLogin
(credentials: C) => Promise<{ token: string; user?: U }>
-
onLogout
() => Promise<void> | void
-
onRefresh
() => Promise<string>
-
onTokenExpired
() => void
-
onError
(error: unknown) => void
-
decodeToken
boolean
true
storage
StorageType
localStorage
storageKey
string
hookraft_auth_token
maxAttempts
number
5
lockoutDuration
number
30
minAttemptInterval
number
500

Returns

PropTypeDefault
status
AuthStatus
-
is(status)
(s: AuthStatus) => boolean
-
user
U | undefined
-
token
string | null
-
login(credentials)
(credentials: C) => Promise<void>
-
logout()
() => Promise<void>
-
tokenPayload
TokenPayload | null
-
tokenExpiresAt
Date | null
-
attempts
number
-
lockout
LockoutState | null
-
remainingTime
number
-
lockoutReason
LockoutReason | null
-

Auth Component

The declarative JSX companion. Render different UI for each auth state without writing conditionals manually.

import { Auth } from "@hookraft/useauth"
<Auth
  when={auth.status}
  onAuthenticated={() => router.push("/dashboard")}
  onIdle={() => router.push("/login")}
  fallback={<p>Please log in to continue</p>}
>
  <Dashboard />
</Auth>

With Slot Components

Use named slots to render specific UI for each state:

function App() {
  const auth = useAuth({ ...options })

  return (
    <Auth when={auth.status} fallback={<LoginForm onLogin={auth.login} />}>
      <Auth.Loading when={auth.status}>
        <Spinner />
      </Auth.Loading>

      <Auth.Authenticated when={auth.status}>
        <Dashboard user={auth.user} onLogout={auth.logout} />
      </Auth.Authenticated>

      <Auth.Locked when={auth.status}>
        <p>
          {auth.lockoutReason === "bot_detection"
            ? "Suspicious activity detected."
            : "Too many attempts."}
          {" "}Try again in {auth.remainingTime}s
        </p>
      </Auth.Locked>

      <Auth.Error when={auth.status}>
        <p>{auth.attempts} failed attempt(s). Try again.</p>
      </Auth.Error>
    </Auth>
  )
}

Props

PropTypeDefault
when
AuthStatus
-
onAuthenticated
() => void
-
onLoading
() => void
-
onLocked
() => void
-
onError
() => void
-
onIdle
() => void
-
fallback
ReactNode
null
children
ReactNode
-

Slot Components

PropTypeDefault
Auth.Authenticated
{ when: AuthStatus, children: ReactNode }
-
Auth.Loading
{ when: AuthStatus, children: ReactNode }
-
Auth.Locked
{ when: AuthStatus, children: ReactNode }
-
Auth.Error
{ when: AuthStatus, children: ReactNode }
-