A modern, full-featured authentication system built with Next.js 16 and React 19, featuring multiple authentication methods and comprehensive user management.
-
🔐 Multiple Authentication Methods
- Email/Password authentication with secure Argon2 hashing
- OAuth integration (configurable providers)
- WebAuthn/Passkeys support for passwordless authentication
-
📧 Email Services
- Email verification
- Password reset functionality
- Email notifications via Resend
-
👤 User Management
- User registration and login
- Account management dashboard
- Admin panel for user administration
- Profile customization with Identicon generation
-
🔒 Security Features
- Iron Session for secure session management
- JWT token support
- Redis-based rate limiting via Upstash
- Scope-based permissions system
- Framework: Next.js 16 with App Router
- UI: React 19
- Database: Drizzle ORM with Neon Postgres (node-postgres)
- Authentication: SimpleWebAuthn, Iron Session, Jose (JWT)
- UI Components: shadcn/ui with base-vega style, built on @base-ui/react
- Styling: Tailwind CSS v4
- Email: Resend
- Storage: Vercel Blob
- Cache/Rate Limiting: Upstash Redis
- Testing: Playwright for E2E tests (against an embedded Postgres cluster)
- Package Manager: Bun
- Bun installed on your system
- A Postgres database (Neon recommended)
- Redis instance (Upstash recommended)
- Resend API key for email functionality
Create a .env.local file in the root directory with the following variables:
# Database
DATABASE_URL=postgresql://user:password@host/dbname?sslmode=require
# Application
NEXT_PUBLIC_APP_NAME=RxLab Auth
NEXT_PUBLIC_APP_URL=http://localhost:3000
# JWT Keys (generate with openssl)
JWT_PRIVATE_KEY=your_jwt_private_key
JWT_PUBLIC_KEY=your_jwt_public_key
# Email
RESEND_API_KEY=your_resend_api_key
# OAuth (optional)
OAUTH_ISSUER_URL=your_oauth_issuer_url
GITHUB_OAUTH_CLIENT_ID=your_github_oauth_client_id
GITHUB_OAUTH_CLIENT_SECRET=your_github_oauth_client_secret
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
# Sign in with Apple (all four required; the provider stays hidden otherwise)
APPLE_OAUTH_SERVICES_ID=your_apple_services_id
APPLE_OAUTH_TEAM_ID=your_apple_team_id
APPLE_OAUTH_KEY_ID=your_apple_key_id
# base64 -i AuthKey_<KEYID>.p8 | tr -d '\n'
APPLE_OAUTH_PRIVATE_KEY=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0t...
# Bundle IDs allowed for native Sign in with Apple (comma-separated)
APPLE_OAUTH_BUNDLE_IDS=com.rxlab.app
# WebAuthn
WEBAUTHN_ORIGIN=http://localhost:3000
WEBAUTHN_RP_ID=localhost
WEBAUTHN_RP_NAME=RxLab Auth
# Redis
UPSTASH_REDIS_REST_URL=your_redis_url
UPSTASH_REDIS_REST_TOKEN=your_redis_tokenRegister these provider callback URLs, using the same origin as
OAUTH_ISSUER_URL:
https://your-auth-host/api/auth/social/github/callback
https://your-auth-host/api/auth/social/google/callback
https://your-auth-host/api/auth/social/apple/callback
Apple differs from the other two in three ways worth knowing up front:
- There is no static client secret. The server mints a short-lived ES256 JWT
from your
.p8signing key on every token exchange, so the Team ID, Key ID, Services ID and private key are all required together. Supply the key base64-encoded (base64 -i AuthKey_<KEYID>.p8 | tr -d '\n') — a single opaque line survives every shell and dashboard intact. A raw PEM, or one flattened with literal\nescapes, is accepted too. - The callback is a cross-site
POST, not a redirect — asking Apple for thename/emailscopes forcesresponse_mode=form_post. The OAuth state cookie is thereforeSameSite=None; Securefor Apple alone, which means the browser flow needs HTTPS even locally; use a tunnel rather thanhttp://localhost. - Apple releases the user's name exactly once, on the first consent. It is captured then or not at all.
Configured providers appear on the web login form and in
GET /api/auth/ui-schema/signin?client_id=<id> under the additive
identityProviders array. Each provider includes absolute iconUrl and
darkIconUrl asset URLs. Native iOS and macOS clients can use the appropriate
icon for the current appearance and pass the returned authorizationParameters
into /api/oauth/authorize to start the selected provider while keeping the
normal authorization-code + PKCE flow back to the app.
When a verified social email matches an existing account, RxLab Auth asks the user to approve the connection before linking it. When no account exists, it asks for confirmation before creating a social-only account. Signed-in users can review and disconnect providers from the Profile page; the final sign-in method cannot be removed until another method, such as a passkey, is available.
iOS and macOS clients (RxAuthSwift) use Apple's own system sheet instead of the browser flow, via a two-step pair that mirrors the native passkey routes:
POST /api/oauth/social/apple/nonce → { session_id, nonce }
POST /api/oauth/social/apple → OAuth token JSON
The client hashes the nonce (SHA-256, hex) into
ASAuthorizationAppleIDRequest.nonce; the server verifies the resulting
identity token against Apple's JWKS, pins the audience to
APPLE_OAUTH_BUNDLE_IDS, re-hashes the nonce to confirm the token belongs to
this request, and burns it. Because a native app has no /social/confirm page
to show, an Apple-verified email that matches an existing account is linked
automatically rather than prompting.
- Clone the repository:
git clone https://github.com/rxtech-lab/rxlab-auth.git
cd rxlab-auth- Install dependencies:
bun install- Set up the database:
# Generate migration files
bun db:generate
# Run migrations
bun db:migrate
# Or push schema directly (development)
bun db:push- Start the development server:
bun devOpen http://localhost:3000 to see the application.
bun dev # Start development server
bun build # Build for production
bun start # Start production server
bun lint # Run ESLint
# Database commands
bun db:generate # Generate Drizzle migrations
bun db:migrate # Run migrations
bun db:push # Push schema to database
bun db:studio # Open Drizzle Studiobunx playwright test # Run all E2E tests
bunx playwright test --ui # Interactive UI mode
bunx playwright test e2e/admin # Run specific test suite
bunx playwright test --debug # Debug mode├── app/ # Next.js App Router pages
│ ├── (auth)/ # Authentication routes (login, register, etc.)
│ ├── account/ # User account management
│ ├── admin/ # Admin panel
│ ├── api/ # API routes
│ └── oauth/ # OAuth integration
├── actions/ # Server actions
├── components/ # React components
│ └── ui/ # shadcn/ui components
├── lib/ # Utilities and configuration
│ ├── auth/ # Authentication logic
│ ├── db/ # Database schema and client
│ ├── email/ # Email templates and sending
│ ├── oauth/ # OAuth providers
│ ├── redis/ # Redis client and rate limiting
│ └── webauthn/ # WebAuthn/Passkeys logic
├── e2e/ # Playwright E2E tests
└── public/ # Static assets
The easiest way to deploy this app is to use the Vercel Platform:
- Push your code to a Git repository
- Import the repository to Vercel
- Configure your environment variables
- Deploy!
Make sure to set up your database and configure all required environment variables before deploying.
This project is private and proprietary.