Authentication Developer Guide

This guide provides implementation code for working with authentication. For conceptual understanding, see:

Overview

1. User authenticates with FusionAuth → receives JWT
2. Application validates JWT via JWKS
3. Application resolves FusionAuth identity to internal user
4. Application stores tokens in httpOnly cookies
5. FusionAuth JWT passed directly to Neon for RLS
6. Neon uses auth_user() and is_authenticated() for access control

Setup

Environment Variables

# FusionAuth Configuration
FUSIONAUTH_URL=https://auth.refresh.tech
FUSIONAUTH_APP_ID=<application-id>
FUSIONAUTH_CLIENT_SECRET=<client-secret>
FUSIONAUTH_TENANT_ID=<fusionauth-tenant-id>

# Neon validates FusionAuth JWT directly via JWKS - no custom JWT keys needed
# PRIVATE_JWK_JSON and PUBLIC_JWK_JSON are no longer required

FusionAuth SDK Setup

JWT Validation

Validating FusionAuth JWTs

Extracting Token from Request

Identity Resolution

Resolving FusionAuth ID to Internal User

Getting Work User ID for Tenant

Permission Checking

Permissions follow the principle of least privilege with granular resource:action patterns. User roles are resolved via auth_assignmentauth_roleauth_role_permissionauth_permission, then each API endpoint checks for its specific required permission.

How Permissions Are Resolved

Permissions are resolved ONCE per request in hooks.server.ts and stored in locals:

Key implementation details:

  1. User is identified via FusionAuth JWT sub claim → user_auth_identities

  2. getUserPermissionsFromAuth() queries auth_permission_cache for the user

  3. If cache is stale, it's repopulated from auth tables via triggers

  4. If ANY role has bypass_rls=true, bypassRls is true

  5. Permissions are NOT stored in cookies (to avoid 4KB limit)

Permission Format

Resolving Permissions

Permissions are resolved from the auth tables based on the user's role assignments:

The auth_permission_cache table is populated by database triggers whenever auth tables change. This provides fast permission lookups without complex joins at request time.

Checking Specific Permission

Middleware Implementation

Auth Middleware

Type Definitions

UserSession Type

Usage in Routes

Protected API Route

Each endpoint checks for exactly one granular permission. This follows least privilege—document:list does NOT imply document:get, and document:update does NOT imply document:delete.

Page Load Function

Personal Route (No Tenant Required)

Database Client with FusionAuth JWT

The FusionAuth access token is passed directly to Neon. No custom JWT generation is needed.

Why no custom Neon JWT?

Previously, the app generated a custom JWT with user_id and tenant_id claims. Now:

  1. FusionAuth JWT is passed directly to Neon

  2. Neon validates via FusionAuth JWKS (/.well-known/jwks.json)

  3. RLS uses auth_user() to resolve FusionAuth sub → internal user_id

  4. RLS uses has_permission() to check permissions via auth_permission_cache

This is more future-proof as database schemas move to api-core.

OAuth Callback Implementation

Token Refresh

Token refresh is handled automatically by middleware. When the access token is near expiry, the middleware refreshes it using the refresh token cookie.

Troubleshooting

JWT Validation Failing

Problem: jwtVerify throws error

Solutions:

  1. Check FusionAuth URL is correct

  2. Verify JWKS endpoint is accessible

  3. Check token hasn't expired

  4. Verify audience matches app ID

Identity Not Linking

Problem: New user created instead of linking to HRIS identity

Solutions:

  1. Verify email comparison uses case-insensitive matching (LOWER())

  2. Verify HRIS sync created user_identities record

  3. Check fusionauth_id is NULL on pre-created identity

  4. Confirm emails are stored normalized (lowercase)

Permission Denied

Problem: 403 errors on API calls

Solutions:

  1. Verify tenant_users record exists with status = 'active'

  2. Check role has required permission via auth_role_permission table

  3. Verify URL path contains correct :tenantId parameter

  4. Check user is using correct identity for that tenant

Neon RLS Issues

Problem: RLS blocking queries

Solutions:

  1. Verify FusionAuth access token is being passed to Neon via authToken

  2. Check Neon is configured with FusionAuth JWKS URL

  3. Verify auth_user() function is returning correct internal user_id

  4. Check has_permission() is receiving correct permissions array

  5. Verify user_auth_identities record exists for the FusionAuth user

  6. Check auth_permission_cache is populated for the user

  7. Verify auth_assignment exists linking user to appropriate roles


Last updated: December 2025

Last updated