Aller au contenu principal

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:

RoleLevelDescription
ADMIN0Full access to the organization
DIRECTION1Leadership — extended read access + approval
MANAGER2Operational management (invoices, quotes, clients)
HR3Human resources (leave, payslips)
ACCOUNTANT3Accounting (FEC, reconciliations)
COLLABORATOR4Limited access (their own data)
CLIENT99Client 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

RouteMethodDescription
/api/auth/[...nextauth]GET/POSTNextAuth handlers (login, callback, session)
/api/auth/accept-invitePOSTAccept a team invitation
/api/auth/forgot-passwordPOSTRequest a password reset
/api/auth/reset-passwordPOSTReset the password (with token)
/api/auth/verify-emailGETVerify the email address (link in the email)
/api/auth/resend-verificationPOSTResend the verification email
/api/auth/mobile/loginPOSTSign in from the mobile app (JWT)
/api/auth/mobile/refreshPOSTRefresh the mobile token
/api/auth/mobile/delete-accountDELETEDelete 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 32

Rate 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

  1. Install the provider (if needed) — NextAuth v5 ships the common providers.

  2. Add the environment variables in .env and src/env.js:

// src/env.js — server section
NEW_PROVIDER_CLIENT_ID: z.string().optional(),
NEW_PROVIDER_CLIENT_SECRET: z.string().optional(),
  1. 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,
  }),
],
  1. Add the button on the login page (src/app/login/page.tsx).

  2. Test: verify the full flow (login → callback → session → dashboard).


◀ Architecture · Contents · Database ▶