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

@drawboard/authagonal-login

v0.1.68

Published

Default login UI for Authagonal — runtime-configurable via branding.json

Readme

@drawboard/authagonal-login

Default login UI for Authagonal — an OAuth 2.0 / OpenID Connect authentication server backed by Azure Table Storage.

Use as a standalone app (built into the Authagonal Docker image) or as an npm package to build a custom login experience while reusing the API client, branding, i18n, and base components.

Installation

npm install @drawboard/authagonal-login

react, react-dom, and react-router-dom are externalized at build time — your app must provide them.

Quick start

Import the base components and styles, then mount the router:

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthLayout, LoginPage, ForgotPasswordPage, ResetPasswordPage } from '@drawboard/authagonal-login';
import '@drawboard/authagonal-login/styles.css';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route element={<AuthLayout />}>
          <Route path="/login" element={<LoginPage />} />
          <Route path="/forgot-password" element={<ForgotPasswordPage />} />
          <Route path="/reset-password" element={<ResetPasswordPage />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

Customizing pages

Override individual pages while keeping the rest. Your custom page has access to the same API client, branding hooks, and i18n as the built-in pages:

import { AuthLayout, ForgotPasswordPage, ResetPasswordPage } from '@drawboard/authagonal-login';
import { login, useBranding, useTranslation, ApiRequestError } from '@drawboard/authagonal-login';
import '@drawboard/authagonal-login/styles.css';

function MyLoginPage() {
  const { t } = useTranslation();
  const branding = useBranding();
  const [agreedToTerms, setAgreedToTerms] = useState(false);

  async function handleSubmit(email: string, password: string) {
    if (!agreedToTerms) throw new Error('You must agree to the Terms of Service');
    await login(email, password);
    window.location.href = '/';
  }

  return (
    <form onSubmit={/* ... */}>
      {/* Your custom UI using t(), branding, login(), etc. */}
    </form>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route element={<AuthLayout />}>
          <Route path="/login" element={<MyLoginPage />} />
          <Route path="/forgot-password" element={<ForgotPasswordPage />} />
          <Route path="/reset-password" element={<ResetPasswordPage />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

See demos/custom-server/login-app for a complete working example with a Terms of Service checkbox and branded footer.

API client

All functions call the Authagonal auth API with cookie credentials. Set VITE_API_URL to point to a different origin during development.

import { login, logout, forgotPassword, resetPassword, getSession, ssoCheck, getProviders, getPasswordPolicy, ApiRequestError } from '@drawboard/authagonal-login';

// Password login — sets a session cookie
await login('[email protected]', 'password');

// End the session
await logout();

// Check if the user has an active session
const session = await getSession();
// → { authenticated: true, userId, email, name }

// Check if an email domain requires SSO
const sso = await ssoCheck('[email protected]');
// → { ssoRequired: true, redirectUrl: '/oidc/azure/login' }

// List configured external providers (Google, Azure AD, etc.)
const { providers } = await getProviders();
// → [{ connectionId: 'google', name: 'Google', loginUrl: '/oidc/google/login' }]

// Password reset flow
await forgotPassword('[email protected]');
await resetPassword(token, newPassword);

// Fetch password policy rules for frontend validation
const { rules } = await getPasswordPolicy();
// → [{ rule: 'MinLength', value: 8, label: 'At least 8 characters' }, ...]

// Error handling
try {
  await login(email, password);
} catch (err) {
  if (err instanceof ApiRequestError) {
    switch (err.error) {
      case 'invalid_credentials': /* wrong email/password */ break;
      case 'locked_out':          /* account locked, err.retryAfter has seconds */ break;
      case 'email_not_confirmed': /* email verification pending */ break;
      case 'sso_required':        /* must use SSO, err.redirectUrl has the URL */ break;
    }
  }
}

Branding

Place a branding.json in your public directory. The AuthLayout component loads it automatically.

{
  "appName": "My App",
  "logoUrl": "/logo.png",
  "primaryColor": "#2563eb",
  "supportEmail": "[email protected]",
  "showForgotPassword": true,
  "customCssUrl": "/custom.css",
  "welcomeTitle": "Welcome to My App",
  "welcomeSubtitle": "Sign in to continue"
}

BrandingConfig fields

| Field | Type | Default | Description | |---|---|---|---| | appName | string | "Authagonal" | Shown in the header and page title | | logoUrl | string \| null | null | Image URL replacing the text header | | primaryColor | string | "#2563eb" | Buttons, links, focus rings via CSS custom properties | | supportEmail | string \| null | null | Contact email shown in the footer | | showForgotPassword | boolean | true | Toggle the forgot password link | | customCssUrl | string \| null | null | URL to additional CSS for deeper styling | | welcomeTitle | LocalizedString | null | Override the login page title | | welcomeSubtitle | LocalizedString | null | Override the login page subtitle |

Localized strings

welcomeTitle and welcomeSubtitle accept either a plain string or an object mapping language codes to strings:

{
  "welcomeTitle": {
    "en": "Welcome to Acme",
    "es": "Bienvenido a Acme",
    "de": "Willkommen bei Acme"
  }
}

Use resolveLocalized() to resolve these in your own components:

import { resolveLocalized, useBranding, useTranslation } from '@drawboard/authagonal-login';

const branding = useBranding();
const { i18n } = useTranslation();
const title = resolveLocalized(branding.welcomeTitle, i18n.language) ?? 'Default Title';

i18n

Built-in support for 8 languages:

| Code | Language | |---|---| | en | English | | zh-Hans | Chinese (Simplified) | | de | German | | fr | French | | es | Spanish | | vi | Vietnamese | | pt | Portuguese | | tlh | Klingon |

Language is auto-detected from the browser and persisted to localStorage. Force a language via query string: ?lng=es.

The useTranslation hook is re-exported from this package to avoid React context duplication. Always import it from @drawboard/authagonal-login, not directly from react-i18next:

// Correct
import { useTranslation } from '@drawboard/authagonal-login';

// Wrong — will get a different i18n instance
import { useTranslation } from 'react-i18next';

Exports

Components

| Export | Description | |---|---| | AuthLayout | Layout wrapper — loads branding, renders language selector, wraps <Outlet /> | | LoginPage | Login form with SSO check, external providers, session detection | | ForgotPasswordPage | Email input → sends reset link | | ResetPasswordPage | Token + new password form with policy validation | | MfaChallengePage | TOTP/passkey/recovery code verification | | MfaSetupPage | QR code scanning, passkey registration, recovery code generation | | RegisterPage | Self-service registration form with email/password | | App | Standalone SPA with full routing (login, register, forgot/reset password, MFA) |

UI Components

| Export | Description | |---|---| | Button | Styled button with variants (default, outline, ghost, etc.) | | Input | Styled text input | | Label | Form label | | Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter | Card layout primitives | | Alert | Alert/notification banner | | Separator | Visual divider | | cn | Tailwind class merge utility |

API client

| Export | Description | |---|---| | login(email, password) | Password login | | logout() | End session | | forgotPassword(email) | Request password reset | | resetPassword(token, password) | Complete password reset | | getSession() | Check current session | | ssoCheck(email) | Check SSO requirement for email domain | | getProviders() | List external identity providers | | getPasswordPolicy() | Fetch password rules | | ApiRequestError | Error class with .error, .retryAfter, .redirectUrl | | mfaVerify(challengeId, method, code) | Verify MFA challenge | | mfaStatus() | Get enrolled MFA methods | | mfaTotpSetup() | Start TOTP enrollment | | mfaTotpConfirm(setupToken, code) | Confirm TOTP enrollment | | mfaWebAuthnSetup() | Start WebAuthn/passkey enrollment | | mfaWebAuthnConfirm(setupToken, attestation) | Confirm passkey enrollment | | mfaRecoveryGenerate() | Generate recovery codes | | mfaDeleteCredential(credentialId) | Remove an MFA credential |

Branding

| Export | Description | |---|---| | loadBranding() | Fetch and parse /branding.json | | BrandingContext | React context for branding config | | useBranding() | Hook to read branding config | | resolveLocalized(value, lang) | Resolve a LocalizedString for a language |

i18n

| Export | Description | |---|---| | i18n | Pre-configured i18next instance | | useTranslation | Re-exported from react-i18next — always import from this package to avoid context duplication |

Types

type LocalizedString = string | Record<string, string> | null;

interface BrandingConfig {
  appName: string;
  logoUrl: string | null;
  primaryColor: string;
  supportEmail: string | null;
  showForgotPassword: boolean;
  showRegistration: boolean;
  customCssUrl: string | null;
  welcomeTitle: LocalizedString;
  welcomeSubtitle: LocalizedString;
  languages: { code: string; label: string }[] | null;
}

interface ExternalProvider {
  connectionId: string;
  name: string;
  loginUrl: string;
}

interface SessionResponse {
  authenticated: boolean;
  userId: string;
  email: string;
  name: string;
}

interface SsoCheckResponse {
  ssoRequired: boolean;
  providerType?: string;
  connectionId?: string;
  redirectUrl?: string;
}

interface PasswordPolicyRule {
  rule: string;
  value: number | null;
  label: string;
}

interface LoginResponse {
  userId?: string;
  email?: string;
  name?: string;
  mfaRequired?: boolean;
  challengeId?: string;
  methods?: string[];
  webAuthn?: object;
  mfaSetupRequired?: boolean;
  setupToken?: string;
  mfaAvailable?: boolean;
}

interface MfaStatusResponse {
  enabled: boolean;
  methods: { id: string; type: string; name: string; createdAt: string; lastUsedAt: string }[];
}

interface MfaTotpSetupResponse {
  setupToken: string;
  qrCodeDataUri: string;
  manualKey: string;
}

License

MIT