Authentication

Sign-in and sign-out

Hosted sign-in, the embedded form, server-side sign-in and sign-out.

Hosted sign-in#

The recommended path: every method the environment enables, MFA, bot protection and branding, with nothing to build. See Hosted AuthKit for the round trip, and the Next.js quickstart for the code.

Embedded sign-in form#

SignIn from @3een/auth/components renders an email and password form in your own page. It posts to your /api/auth/sign-in route, and asks for a six-digit authenticator code if the account has MFA. The pending MFA state is kept in a short-lived httpOnly cookie.

app/sign-in/page.tsxtype-checked

'use client';
// app/sign-in/page.tsx — the embedded form (password + TOTP)
import { SignIn } from '@3een/auth/components';

export default function SignInPage() {
  // Posts to your own /api/auth/sign-in; the API key never reaches the
  // browser. Passkeys, magic codes, social and SSO stay on the hosted page,
  // linked under the form.
  return <SignIn afterSignInUrl="/dashboard" labels={{ title: 'Sign in to Acme' }} />;
}

Server-side sign-in#

AuthClient.signIn returns the tokens and the user, for flows you drive yourself. Keep the tokens in httpOnly cookies. A wrong password and an unknown address get the same answer: 401 with code invalid_credentials.

Sign-out#

signOut() from @3een/auth/components posts to your /api/auth/sign-out, which revokes the session with the API, clears the cookies and then navigates to the return path. On Express, mount handleSignOut(config).

components/billing-menu.tsxtype-checked

'use client';
// Hide or show UI by role or permission. This does not protect data: the
// route behind it must check too (getAuth().has() or requirePermission).
import { OrganizationSwitcher, Protect, signOut, useSession } from '@3een/auth/components';

export function BillingMenu() {
  const { user, loading, has } = useSession();
  if (loading) return <p>Loading…</p>;
  if (!user) return null;

  return (
    <nav aria-label="Account">
      <OrganizationSwitcher onSwitched={(organizationId) => console.info('Now in', organizationId)} />
      <Protect permission="billing:read" fallback={<p>Ask an admin for billing access.</p>}>
        <a href="/billing">Billing</a>
      </Protect>
      {has({ role: 'admin' }) && <a href="/admin">Admin</a>}
      <button type="button" onClick={() => signOut('/api/auth', '/')}>
        Sign out
      </button>
    </nav>
  );
}