Authentication
NextAuth v5 with Google, Microsoft and Credentials. JWT, encryption, rate limiting.
Overview
HeartCo uses NextAuth v5 (Auth.js) with a JWT strategy (no database sessions). The session lasts 24 hours.
Main file: src/server/auth/config.ts
Providers
1. Google OAuth
GOOGLE_CLIENT_ID="xxxx.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="GOCSPX-xxxx"Configuration in the Google Cloud Console:
- Create a project → APIs & Services → Credentials → OAuth 2.0 Client ID
- Authorized redirect URI:
http://localhost:3000/api/auth/callback/google - In production:
https://app.your-domain.com/api/auth/callback/google
2. Microsoft Entra ID
MICROSOFT_CLIENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
MICROSOFT_CLIENT_SECRET="xxxx"
MICROSOFT_TENANT_ID="common"Configuration in the Azure portal:
- Azure AD → App registrations → New registration
- Redirect URI:
http://localhost:3000/api/auth/callback/microsoft-entra-id - Type: Web
Note: HeartCo automatically blocks personal Microsoft accounts (Consumer Tenant). Only organizational accounts are accepted.
3. Credentials (Email + Password)
Classic email/password authentication with:
- Rate limiting: 10 attempts per IP every 15 minutes (Upstash Redis)
- Timing-attack protection:
bcrypt.compare()executed even if the user does not exist - Email normalization: automatic lowercase + trim
- Email verification: required before sign-in
Sign-up flow
Register (/register)
│
▼
Account + Organization creation
│
▼
Verification email (Resend)
│
▼
Verify Email (/verify-email)
│
▼
Onboarding (/onboarding)
│ - Organization name
│ - Industry
│ - Initial setup
▼
Dashboard (/dashboard)
For OAuth providers (Google, Microsoft), the email is automatically verified and the user goes straight to onboarding.
Roles
HeartCo defines 7 roles with a strict hierarchy:
| Role | Level | Description |
|---|---|---|
ADMIN | 0 | Full access to the organization |
DIRECTION | 1 | Leadership — extended read access + approval |
MANAGER | 2 | Operational management (invoices, quotes, clients) |
HR | 3 | Human resources (leave, payslips) |
ACCOUNTANT | 3 | Accounting (FEC, reconciliations) |
COLLABORATOR | 4 | Limited access (their own data) |
CLIENT | 99 | Client portal only (no internal permission) |
The HR and ACCOUNTANT roles share the same hierarchy level (3) but have different permissions. See Permissions.
Authentication API routes
| Route | Method | Description |
|---|---|---|
/api/auth/[...nextauth] | GET/POST | NextAuth handlers (login, callback, session) |
/api/auth/accept-invite | POST | Accept a team invitation |
/api/auth/forgot-password | POST | Request a password reset |
/api/auth/reset-password | POST | Reset the password (with token) |
/api/auth/verify-email | GET | Verify the email address (link in the email) |
/api/auth/resend-verification | POST | Resend the verification email |
/api/auth/mobile/login | POST | Sign in from the mobile app (JWT) |
/api/auth/mobile/refresh | POST | Refresh the mobile token |
/api/auth/mobile/delete-account | DELETE | Delete the account (mobile app) |
Security
Token Encryption
OAuth tokens (Google, Microsoft) are encrypted at rest with AES-256-GCM before being stored in the database:
TOKEN_ENCRYPTION_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 64 hex characters (32 bytes) — Generate with: openssl rand -hex 32Rate Limiting
Credentials sign-in attempts are limited to 10 per IP over 15 minutes:
- Backend: Upstash Redis (sliding window)
- Fallback: in-memory counter if Redis is unavailable
OAuth token auto-refresh
Google and Microsoft tokens are automatically refreshed if they expire within the next 5 minutes. No user action is required.
Open-redirect protection
The NextAuth callback verifies that the redirect URL is same-origin. External redirects are blocked.
Session
The JWT session contains:
interface Session {
user: {
id: string; // User ID
name: string;
email: string;
image: string | null;
role: string; // Role within the organization
organizationId: string | null; // Organization ID
emailVerified: boolean;
emailAccountId?: string | null;
};
}Server-side access:
// In a tRPC router
const userId = ctx.session.user.id;
const orgId = ctx.session.user.organizationId;
const role = ctx.session.user.role;Client-side access:
import { useSession } from "next-auth/react";
const { data: session } = useSession();
console.log(session?.user.role);Adding a new OAuth provider
-
Install the provider (if needed) — NextAuth v5 ships the common providers.
-
Add the environment variables in
.envandsrc/env.js:
// src/env.js — server section
NEW_PROVIDER_CLIENT_ID: z.string().optional(),
NEW_PROVIDER_CLIENT_SECRET: z.string().optional(),- Configure the provider in
src/server/auth/config.ts:
import NewProvider from "next-auth/providers/new-provider";
providers: [
// ... existing providers
NewProvider({
clientId: env.NEW_PROVIDER_CLIENT_ID,
clientSecret: env.NEW_PROVIDER_CLIENT_SECRET,
}),
],-
Add the button on the login page (
src/app/login/page.tsx). -
Test: verify the full flow (login → callback → session → dashboard).