npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@groo.dev/auth-react

v0.2.10

Published

React hooks and components for Groo Auth SDK

Readme

@groo.dev/auth-react

React hooks and components for integrating Groo authentication into your React/Next.js applications.

AuthProvider supports two modes:

  • redirect (default) — session cookie + your own backend proxy via @groo.dev/auth-server. Unchanged from prior versions; see "Redirect mode" below.
  • spa — no backend required. OAuth authorization-code + PKCE flow straight from the browser to accounts.groo.dev, with in-memory access tokens and an HttpOnly refresh cookie for silent session restore. See "SPA mode quick start" below.

Both modes ship the same styled components (SignInButton, SignUpButton, UserButton) and hooks (useAuth, useUser, useScopes) — signIn()/signOut() on the auth context are the API surface these components call, so anything they do you can do yourself.

spa mode additionally ships <UserProfile> — a full self-serve account-settings panel (profile, security, connected apps, API tokens, danger zone) — plus the five hooks that back it (useProfile, usePasskeys, useDevices, useConnectedApps, useApiTokens). These are spa-mode only: they call the Bearer-scoped /v1/account/* API directly, which redirect-mode apps have no token to call. In redirect mode, point users at accounts-web's own settings pages instead (UserButton does this automatically — see below).

Installation

npm install @groo.dev/auth-react

Prerequisites

Before using this SDK, you need a registered application at accounts.groo.dev with a client_id:

  • For redirect mode: any application type, plus a backend API using @groo.dev/auth-server to proxy authentication.
  • For spa mode: the application must be registered as a spa-type application (a public client, no client secret) with your app's URL listed as a redirect URI origin. No backend is required for auth itself.

SPA mode quick start

import { AuthProvider, SignInButton, UserButton } from '@groo.dev/auth-react'
import '@groo.dev/auth-react/styles.css'

<AuthProvider
  mode="spa"
  baseUrl="https://accounts.groo.dev"
  clientId="app_yourclientid"
  redirectUri="https://yourapp.example/"
>
  <header>
    <SignInButton />
    <UserButton />
  </header>
</AuthProvider>

Notes:

  • Import @groo.dev/auth-react/styles.css once (e.g. in your root layout) — it provides the default look and the theming variables below. Components render unstyled without it.
  • spa mode requires the accounts:profile scope: the SDK loads the signed-in user from /v1/account/profile, and AuthProvider throws at render time if the requested scopes array omits it. The default scope list (openid profile email offline_access accounts:profile accounts:passkeys accounts:devices accounts:apps accounts:tokens) already includes it, plus the other four accounts:* scopes each <UserProfile> tab needs — only a concern if you pass a custom scopes prop (see "Account management hooks" below for which scope backs which tab).
  • Sign-in and sign-out redirect the top-level page to accounts.groo.dev; there is no embedded credential UI in your app (by design — see the design spec's non-goals).
  • SignInButton, SignUpButton, and UserButton work in both modes without changes to your code — only the AuthProvider props differ.
  • In spa mode, signOut() is app-local: it revokes this app's tokens only, and the accounts.groo.dev SSO session persists, so the next signIn() round-trips silently without prompting for credentials again.

Redirect mode (unchanged)

If you don't pass mode="spa" (or pass mode="redirect" explicitly), AuthProvider behaves exactly as before: session cookie + /v1/__auth/me proxy via @groo.dev/auth-server. Existing redirect-mode consumers (pass, drive, ai, ops) need no changes to keep working, and SignInButton/SignUpButton/UserButton work there too.

Theming

Override any of these CSS custom properties on :root (or an ancestor of the components) after importing @groo.dev/auth-react/styles.css:

| Variable | Purpose | Default | |---|---|---| | --groo-auth-accent | Primary brand color (buttons, avatar background, focus ring) | #4f46e5 | | --groo-auth-accent-fg | Text/icon color on top of the accent color | #ffffff | | --groo-auth-bg | Background color for menus/panels | #ffffff | | --groo-auth-fg | Primary text color | #111827 | | --groo-auth-muted | Secondary text color (e.g. email under name) | #6b7280 | | --groo-auth-border | Border color for menus, secondary buttons | #e5e7eb | | --groo-auth-danger | Color for destructive actions (e.g. sign out) | #dc2626 | | --groo-auth-radius | Corner radius for buttons, menus, avatars | 8px | | --groo-auth-shadow | Box shadow for popover menus | 0 8px 24px rgb(0 0 0 / 0.12) | | --groo-auth-font | Font family | system-ui, -apple-system, 'Segoe UI', sans-serif | | --groo-auth-font-size | Base font size | 14px | | --groo-auth-focus-ring | box-shadow used for keyboard focus | 0 0 0 2px var(--groo-auth-bg), 0 0 0 4px var(--groo-auth-accent) |

Dark mode applies automatically via prefers-color-scheme: dark; override explicitly with data-groo-theme="dark" or data-groo-theme="light" on :root (or any ancestor). Every component also accepts a className prop, and internal nodes carry stable groo-auth-* classes for deeper overrides.

Quick Start (redirect mode)

1. Set up your backend API

Your API needs to use @groo.dev/auth-server:

// api/src/index.ts
import { Hono } from 'hono'
import { grooAuth } from '@groo.dev/auth-server'
import { GrooHonoMiddleware } from '@groo.dev/auth-server/hono'

type Env = {
  CLIENT_ID: string
  CLIENT_SECRET: string
}

const hono = new GrooHonoMiddleware<Env>((env) => grooAuth({
  clientId: env.CLIENT_ID,
  clientSecret: env.CLIENT_SECRET,
}))

const app = new Hono<{ Bindings: Env }>()

app.use('*', hono.init)
app.route('/v1', hono.routes)

export default app

2. Proxy API calls from your frontend

Configure your frontend to proxy /v1/* requests to your API:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/v1/:path*',
        destination: 'https://api.myapp.groo.dev/v1/:path*',
      },
    ]
  },
}

export default nextConfig

3. Wrap your app with AuthProvider

// app/providers.tsx
'use client'

import { AuthProvider } from '@groo.dev/auth-react'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <AuthProvider
      baseUrl="https://accounts.groo.dev"
      clientId="your-client-id"
      redirectUri="https://myapp.groo.dev"
    >
      {children}
    </AuthProvider>
  )
}

4. Use the auth hooks

'use client'

import { useAuth, LoginButton, LogoutButton } from '@groo.dev/auth-react'

export default function Page() {
  const { user, isLoading, error } = useAuth()

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return user ? (
    <div>
      <p>Welcome, {user.name || user.email}!</p>
      <LogoutButton>Sign Out</LogoutButton>
    </div>
  ) : (
    <div>
      <p>Not logged in</p>
      <LoginButton>Sign In</LoginButton>
    </div>
  )
}

API Reference

AuthProvider

Provides authentication context to your app.

<AuthProvider
  baseUrl="https://accounts.groo.dev"  // Required: Groo accounts service URL
  clientId="your-client-id"            // Required: Your application's client ID
  redirectUri="https://myapp.groo.dev" // Required: Where to redirect after login
  mode="redirect"                      // Optional: 'redirect' (default) or 'spa'
  scopes={['openid', 'profile', 'email', 'offline_access', 'accounts:profile', 'accounts:passkeys', 'accounts:devices', 'accounts:apps', 'accounts:tokens']} // Optional, spa mode only
>
  {children}
</AuthProvider>

mode and scopes only matter for spa mode — see "SPA mode quick start" above. Redirect-mode apps can omit both.

useAuth()

Hook to access authentication state.

const { user, isLoading, error, loginUrl, logoutUrl, scopes, mode, accountsUrl, signIn, signOut, refetch, authedFetch, stepUp, getToken } = useAuth()

Returns:

  • user: User | null - Current user or null if not authenticated
  • isLoading: boolean - True while fetching user data
  • error: Error | null - Error if authentication check failed
  • loginUrl: string - URL to redirect for login (redirect mode)
  • logoutUrl: string - URL to redirect for logout (redirect mode)
  • scopes: string[] - OAuth scopes granted: consent record in redirect mode, token scope in spa mode
  • mode: 'redirect' | 'spa' - The active provider mode
  • accountsUrl: string - Normalized accounts base URL (no trailing slash), for deep links
  • signIn: (options?: { screen?: 'signup' }) => Promise<void> - Starts sign-in (or sign-up, via { screen: 'signup' }); this is what SignInButton/SignUpButton call
  • signOut: () => Promise<void> - Signs out; this is what UserButton's menu calls
  • refetch: () => Promise<void> - Function to refetch user data
  • authedFetch: (path: string, init?: RequestInit) => Promise<Response> - Authenticated fetch (spa mode only; throws in redirect mode) — see "authedFetch" below
  • stepUp: () => Promise<void> - Step-up re-auth round trip (spa mode only; throws in redirect mode) — see "Step-up re-authentication" below
  • getToken: () => Promise<string | null> - Current access token, refreshing if expired, null when signed out (spa mode only; throws in redirect mode) — see "authedFetch" below

SignInButton / SignUpButton

Branded buttons that start the sign-in / sign-up flow, in either provider mode (redirect navigation in redirect mode, PKCE authorize redirect in spa mode). <SignUpButton> sends the user to accounts-web's signup screen; because new accounts are approval-gated, a successful signup always lands on a "pending approval" screen rather than resuming your app immediately — the OAuth context (the redirect back into your app) is preserved via a continue link on that screen's "sign in" link, so once an admin approves the account the user signs in and the original flow resumes.

import { SignInButton, SignUpButton } from '@groo.dev/auth-react'

<SignInButton className="btn">Sign In</SignInButton>
<SignUpButton className="btn">Sign Up</SignUpButton>

UserButton

Avatar chip that opens a dropdown menu: name/email header, "Manage account", any app-supplied custom items, and sign out. Keyboard-navigable, portal-rendered, tracks the trigger on scroll/resize.

import { UserButton } from '@groo.dev/auth-react'

<UserButton
  items={[{ label: 'Settings', onClick: () => router.push('/settings') }]} // optional custom items
  manageAccount="modal"       // optional: force 'modal' or 'link' — see below
  manageAccountHref="/custom" // optional: override the 'link' destination
/>

Renders nothing when signed out — pair with SignInButton for a typical header (see the SPA mode quick start above).

"Manage account" auto-wiring: the manageAccount prop controls what clicking "Manage account" does, and defaults based on provider mode:

  • spa mode (default 'modal') — opens <UserProfile mode="modal"> in place, no navigation.
  • redirect mode (default 'link') — a plain link to ${accountsUrl}/settings (or manageAccountHref if you pass one).

You can override the default, but passing manageAccount="modal" in redirect mode is a silent no-op: <UserProfile> requires spa mode internally and renders nothing (with a one-time console.warn) when it doesn't have one, so the menu item renders but clicking it does nothing visible. Don't force 'modal' unless the provider is in spa mode.

UserProfile

Tabbed account-settings panel — Profile, Security (passkeys + devices/sessions), Connected apps, API tokens, and a Danger zone (sign out everywhere, delete account). spa mode only — like the hooks it's built on, it renders null (with a one-time console.warn) if the provider isn't in spa mode.

import { UserProfile } from '@groo.dev/auth-react'

// Inline (default) — renders the panel in place, e.g. on your app's /account route.
<UserProfile />

// Modal — render conditionally and control it yourself.
const [open, setOpen] = useState(false)
<UserProfile mode="modal" open={open} onClose={() => setOpen(false)} />

Props: mode?: 'inline' | 'modal' (default 'inline'), open?: boolean (modal only), onClose?: () => void (modal only — called on Escape or backdrop click), className?: string.

You usually don't render <UserProfile mode="modal"> yourself — <UserButton> does it automatically in spa mode (see "Manage account" auto-wiring above). Render it directly when you want an inline /account page, or a modal you trigger from your own UI.

Focus management: modal mode moves focus into the dialog on open and restores it to whatever was focused before opening once the dialog closes (Escape, backdrop click, or unmount while open). This is not a full focus trap (Tab can still leave the dialog) — that's out of scope for now.

Account management hooks (spa mode only)

Each hook backs one <UserProfile> tab and returns { <data>, isLoading, error, refetch, <mutations> }. Mutations throw on failure — including StepUpRequiredError for the ones gated on recent authentication (see "Step-up re-authentication" below). Import them directly if you're building custom account-settings UI instead of using <UserProfile>.

import { useProfile, usePasskeys, useDevices, useConnectedApps, useApiTokens } from '@groo.dev/auth-react'
  • useProfile(){ user, isLoading, error, updateProfile({ name?, phone? }), changePassword({ currentPassword?, newPassword }), requestEmailChange(newEmail), verifyCurrentEmail(code), verifyNewEmail(code), deleteAccount(confirmEmail) }. Email change is a 3-step flow (request → verify current → verify new); changePassword and deleteAccount are step-up gated. deleteAccount signs the user out on success.
  • usePasskeys(){ passkeys, isLoading, error, refetch, rename(id, name), remove(id) }. Registering a new passkey stays on accounts-web (WebAuthn RP ID constraint) — this hook only manages existing ones.
  • useDevices(){ devices, isLoading, error, refetch, revoke(id), signOutEverywhere() }. signOutEverywhere is step-up gated.
  • useConnectedApps(){ apps, isLoading, error, refetch, revoke(applicationId) } — lists OAuth grants (apps the user has authorized) and lets them be revoked.
  • useApiTokens(){ tokens, isLoading, error, refetch, create(input): Promise<string>, revoke(id) }. create({ name, scopes, description?, expiresInDays? }) is step-up gated and resolves to the plaintext token secret — shown once; the API never returns it again.

Each hook requires one accounts:* scope:

| Hook | Required scope | |---|---| | useProfile | accounts:profile | | usePasskeys | accounts:passkeys | | useDevices | accounts:devices | | useConnectedApps | accounts:apps | | useApiTokens | accounts:tokens |

The SDK's default spa-mode scopes already include all five, so this table only matters if you pass a custom scopes prop to AuthProvider — omitting one of these scopes will make the corresponding hook (and <UserProfile> tab) fail with an insufficient-scope error.

Step-up re-authentication (StepUpRequiredError)

Password change, email change, account deletion, and API-token creation require a recent sign-in (checked via the access token's auth_time). If the token is stale, the API rejects the mutation with 403 stale_auth, and the hooks above throw StepUpRequiredError instead of a generic Error.

import { StepUpRequiredError } from '@groo.dev/auth-react'
import { useAuth } from '@groo.dev/auth-react'

const { stepUp } = useAuth()

try {
  await changePassword({ currentPassword, newPassword })
} catch (err) {
  if (err instanceof StepUpRequiredError) {
    // Show a "Confirm it's you" prompt, then call stepUp() from its button.
  } else {
    // Handle as a normal error.
  }
}

stepUp() (from useAuth()) round-trips the top-level page through accounts-web's /authorize with max_age, which redirects to /login only if the session is actually stale — otherwise it returns immediately with a fresh token. Call it from the click handler of your "Confirm it's you" button; the browser navigates away and back, and your mutation form's local state (e.g. the password fields) is preserved because it's a full authorize round trip through the existing SSO session, not a fresh login. <UserProfile>'s built-in tabs already implement this pattern (see StepUpNotice internally) — reuse it as a reference if you're building custom UI with the hooks above.

authedFetch — calling your own Bearer-protected API

If your app has its own backend and wants to call it with the same access token the SDK uses for /v1/account/* (rather than managing a second auth flow), useAuth() exposes the same authedFetch the hooks use internally:

const { authedFetch } = useAuth()

const response = await authedFetch('https://api.myapp.example/v1/widgets', { method: 'POST', body: JSON.stringify(data) })

authedFetch(url, init?) attaches the current access token as an Authorization: Bearer header, and — like the account hooks — refreshes and retries once on a 401 before giving up. Pass an absolute URL for your own API (a path starting with / is resolved against the accounts base URL, which is only useful for calling accounts endpoints directly). spa mode only: in redirect mode authedFetch throws immediately, since there's no client-held access token to attach — use your existing server-proxied fetch instead.

getToken — calling your own API from non-fetch clients

If your app authenticates its own API calls with axios, GraphQL, or another non-fetch client, useAuth() also exposes getToken() — the raw access token authedFetch attaches internally, so you can attach it yourself:

const { getToken } = useAuth()

api.interceptors.request.use(async (config) => {
  const token = await getToken()
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

getToken() returns the current access token, refreshing it first if it's expired, or null if the user is signed out. spa mode only: in redirect mode getToken throws immediately — redirect-mode apps authenticate with the session cookie (credentials: 'include') instead.

Audience caveat: the access token's audience is accounts — don't send it to third-party hosts, and note that other first-party Groo APIs will reject it as well until the audience-validation rollout across those services completes. authedFetch and getToken are intended for calling accounts's own /v1/account/* endpoints (or your own backend, which can treat the token as an opaque bearer credential to forward/verify itself) — not as a general-purpose token for calling arbitrary Groo services yet.

Example app

examples/spa-demo/ in this repo is a minimal Vite + React SPA wiring up AuthProvider in spa mode end-to-end: SignInButton/SignUpButton/UserButton in the header, and <UserProfile> rendered inline on an /account view. See its own README.md for the app-registration and environment-variable setup needed to run it.

LoginButton

A link that redirects to the login page with OAuth parameters.

<LoginButton className="btn">
  Sign In
</LoginButton>

The login URL includes:

  • client_id - Your application's client ID
  • redirect_uri - Current page URL (for redirect after login)

LogoutButton

A link that redirects to the logout endpoint. Clears the session and redirects back to your app.

<LogoutButton className="btn">
  Sign Out
</LogoutButton>

The logout URL redirects to {baseUrl}/v1/auth/logout?redirect_uri={origin} which:

  1. Clears the session cookie
  2. Redirects the user back to your app's origin

RequireAuth

Wrapper component that only renders children when user is authenticated.

<RequireAuth
  fallback={<div>Loading...</div>}  // Optional: Show while loading
  redirectTo="/login"                // Optional: Redirect if not authenticated
>
  <ProtectedContent />
</RequireAuth>

User Object

interface User {
  id: string
  email: string | null
  phone: string | null
  name: string | null
  role: string
}

// When using auth-server middleware, you also get consent info:
interface ConsentedUser extends User {
  consent: {
    id: string
    consentedAt: string
    lastAccessedAt: string
    revokedAt: string | null
    appData: Record<string, unknown>  // App-specific data storage
  }
}

How It Works

Login Flow

  1. AuthProvider checks authentication by calling /v1/__auth/me on your API
  2. Your API validates the session via accounts.groo.dev/v1/client/auth/me
  3. LoginButton redirects to accounts.groo.dev/login?client_id=...&redirect_uri=...
  4. User authenticates and grants consent to your application
  5. User is redirected back with a session cookie
  6. SDK detects the session and fetches user data

Logout Flow

  1. LogoutButton redirects to accounts.groo.dev/v1/auth/logout?redirect_uri=...
  2. Session is deleted and cookie is cleared
  3. User is redirected back to your app

Related Packages

License

MIT