bwh-auth
v0.3.0
Published
Shared React auth UI and browser WebAuthn helpers for BWH applications.
Readme
bwh-auth
Shared headless React auth components for BWH applications.
This package owns auth-specific React behavior and browser helpers. It does not import or bundle a UI kit, Blade page wrappers, or application Vite entrypoints. Consumers must inject their own UI components and mount these components from app-owned pages/entrypoints.
The companion Laravel API package is
bherila/auth-laravel.
Ownership boundary
bwh-auth exports React components only. Laravel apps should keep their own Blade files such as resources/views/auth/login.blade.php and Vite entrypoints such as resources/js/auth/login.tsx; those app files import and mount these shared components.
Components
LoginFormSignupFormPasswordResetRequestFormResetPasswordFormChangePasswordFormTwoFactorFormPasskeyLoginButtonPasskeySection
Relying-party helpers
relyingApplicationsFrom(value) validates the sibling-application list an identity provider
reports into one that is safe to render, and safeApplicationHref(url) is the scheme check
behind it. Neither touches the DOM, so an app can feed them a #app-initial-data payload, an
Inertia shared prop, or anything else it already has.
import { relyingApplicationsFrom } from 'bwh-auth';
const applications = relyingApplicationsFrom(initialData.applications);Rendering these entries means putting provider-supplied text into an href. javascript:
and data: URLs pass a server-side URL-validity check but execute rather than navigate, so
the scheme is parsed rather than prefix-matched — a startsWith test can be walked past with
leading control characters or unexpected case — and the parsed form is returned, so what
is rendered is exactly what was validated. Unrecognised entries are dropped rather than
throwing: this is navigation chrome, and one malformed entry must not take down the page it
appears on.
Install
Install the published package from npm:
pnpm add bwh-authFor a deliberately pinned version:
pnpm add [email protected]Commit pnpm-lock.yaml; consumers do not need a GitHub tarball URL.
When installing locally during package development, a path dependency is still useful:
pnpm add bwh-auth@file:../auth-reactInstall peer dependencies in the consuming app:
pnpm add lucide-react react react-domReact and React DOM are usually already present in Laravel/Vite apps.
Component Injection
Higher-level components require injected components so each app can use its own shadcn/Base UI components and Vite can tree-shake cleanly.
import { LoginForm, type AuthComponentInput } from "bwh-auth"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function getShadcnComponents() {
return {
Button,
Card,
CardContent,
CardDescription: ({ ...props }) => <div {...props} />,
CardHeader,
CardTitle,
Input,
Label,
// Extra keys are allowed so this helper can be shared across features.
Textarea,
Dialog,
} satisfies AuthComponentInput
}
export function LoginPage() {
return <LoginForm components={getShadcnComponents()} />
}Custom Signup Fields
SignupForm is field-driven so apps can keep app-specific registration concepts, such as invite codes or policy checkboxes, while reusing the shared auth form behavior.
<SignupForm
components={getShadcnComponents()}
submitMode="native"
fields={[
{ name: 'first_name', label: 'First Name', required: true, autoComplete: 'given-name' },
{ name: 'last_name', label: 'Last Name', required: true, autoComplete: 'family-name' },
{ name: 'email', label: 'Email', type: 'email', required: true, autoComplete: 'email' },
{ name: 'password', label: 'Password', type: 'password', required: true, minLength: 8, autoComplete: 'new-password' },
{ name: 'password_confirmation', label: 'Confirm Password', type: 'password', required: true, minLength: 8, autoComplete: 'new-password' },
{ name: 'invite_code', label: 'Season Invite Code', required: true },
{ name: 'agreement', label: 'I agree to keep this program confidential.', type: 'checkbox', required: true },
]}
/>Signup fields can use hiddenWhen for flows such as passwordless signup, where the app submits passwordless=1 and hides password fields:
const fields = [
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'passwordless', label: 'Use a passkey instead of a password', type: 'checkbox' },
{ name: 'password', label: 'Password', type: 'password', required: true, hiddenWhen: (values) => Boolean(values.passwordless) },
{ name: 'password_confirmation', label: 'Confirm Password', type: 'password', required: true, hiddenWhen: (values) => Boolean(values.passwordless) },
]For fetch-based signup flows, onSuccess receives both the server result and submitted values. That lets apps create the user first, then enroll a passkey through the shared WebAuthn helper:
import { SignupForm, registerPasskey } from 'bwh-auth'
<SignupForm
components={getShadcnComponents()}
submitMode="fetch"
fields={fields}
onSuccess={async (result, values) => {
if (values.passwordless) {
await registerPasskey({ endpoints: { csrfToken } })
}
window.location.assign(result.redirect || '/')
}}
/>Passkey sign-in
LoginForm can include an explicit passkey sign-in button and can opt into WebAuthn conditional UI so passkeys appear in the browser autofill menu for the email field. Conditional UI uses PublicKeyCredential.isConditionalMediationAvailable(), starts a conditional navigator.credentials.get() request, and sets the email input autocomplete to username webauthn when supported.
<LoginForm
components={getShadcnComponents()}
enablePasskeys
enablePasskeyAutofill
/>The explicit passkey button and conditional autofill both default to the Laravel package routes:
POST /api/passkeys/auth/optionsPOST /api/passkeys/auth
Password Change
ChangePasswordForm is intended for authenticated settings pages or dialogs. The consuming app owns the wrapper and backend endpoint; the form posts to /api/change-password by default and accepts endpoints.changePassword for apps that use a different route.
<ChangePasswordForm
components={getShadcnComponents()}
endpoints={{ csrfToken }}
onSuccess={() => setMessage('Password changed successfully.')}
onError={setError}
/>Endpoint Defaults
Auth forms default to the Laravel package API routes where the Laravel package owns the endpoint:
POST /api/auth/forgot-passwordPOST /api/auth/reset-passwordPOST /api/change-passwordPOST /api/auth/two-factor/verifyPOST /api/auth/two-factor/resendPOST /api/auth/two-factor/report/:token
Passkey components default to the Laravel package routes:
GET /api/passkeysPOST /api/passkeys/register/optionsPOST /api/passkeys/registerDELETE /api/passkeys/:idPOST /api/passkeys/auth/optionsPOST /api/passkeys/auth
Releasing
Prepare a release from this package directory:
pnpm releaseThe release script:
- requires a clean git working tree
- bumps
package.jsonversion, defaulting topatch - runs
pnpm install --lockfile-only - runs typecheck and build
- creates
release/bwh-auth-VERSION.tgz - commits the version bump
- creates and pushes a signed tag like
bwh-auth-v0.1.1
The release workflow verifies the tag signature, publishes to npm through its trusted publisher, and creates a GitHub Release with the tarball as a secondary artifact.
Version bump options:
pnpm release patch
pnpm release minor
pnpm release major
pnpm release --version=0.2.0
pnpm release --dry-run