Authentication Flow

Overview

The Refresh App Web uses FusionAuth as the identity provider (IdP) for all authentication. FusionAuth handles identity verification (login, passwords, MFA, OAuth token issuance), while the application database handles authorization (who can do what, in which tenant/group).

Key Principle: FusionAuth proves "this is Taylor" → Your DB answers "what can Taylor do in Acme Corp?"

Authentication Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         Client Browser                          │
└────────────┬────────────────────────────────────────────────────┘

             │ 1. Initiate Login

┌─────────────────────────────────────────────────────────────────┐
│                    SvelteKit App (Edge)                         │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │              Auth Middleware                              │  │
│  │  • Redirect to FusionAuth                                 │  │
│  │  • Handle callback                                        │  │
│  │  • Resolve identity → user                                │  │
│  └─────────┬─────────────────────────────────────────────────┘  │
└────────────┼────────────────────────────────────────────────────┘

             │ 2. OAuth Flow

┌─────────────────────────────────────────────────────────────────┐
│                         FusionAuth                              │
│  • User authentication (email/password, OAuth, SSO)             │
│  • Self-registration                                            │
│  • MFA enforcement                                              │
│  • JWT token issuance                                           │
└────────────┬────────────────────────────────────────────────────┘

             │ 3. JWT returned

┌─────────────────────────────────────────────────────────────────┐
│                    SvelteKit App (Edge)                         │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │               Identity Resolution                         │  │
│  │  • Validate JWT via JWKS                                  │  │
│  │  • Lookup user_identities by fusionauth_id                │  │
│  │  • Link identity if needed                                │  │
│  │  • Store access token in httpOnly cookie                  │  │
│  │  • Store minimal user info in auth_user cookie            │  │
│  └─────────┬─────────────────────────────────────────────────┘  │
└────────────┼────────────────────────────────────────────────────┘

             │ 4. Database operations with RLS

┌─────────────────────────────────────────────────────────────────┐
│                      Neon Postgres                              │
│  • Validates FusionAuth JWT directly via JWKS                   │
│  • auth_user() resolves fusionauth_id → internal user_id        │
│  • has_permission() checks permissions via auth_permission_cache│
│  • Row-Level Security enforcement                               │
└─────────────────────────────────────────────────────────────────┘

FusionAuth Configuration

FusionAuth Tenant

A single FusionAuth tenant holds all platform users. This is separate from your application's tenant concept (companies/organizations).

Applications

FusionAuth has two applications to separate user authentication from API access:

ReFresh Platform (User Authentication)

For the SvelteKit frontend and user authentication. This is where users log in.

Setting
Value

OAuth grants

Authorization Code, Refresh Token

Redirect URLs

https://app.refresh.tech/auth/callback, http://localhost:5173/auth/callback

Access token TTL

60 minutes

Refresh token TTL

30 days

Self-registration

Enabled

Client authentication

Required

Identity Providers

Google, Microsoft (SSO-first; magic link may be added later)

Login Methods

Current: SSO only (Google, Microsoft)

Future: Magic link (emailed code), potentially other methods

All login paths end the same way: FusionAuth returns a fusionauth_id and email, which the application uses to resolve or create the internal user identity.

ReFresh API - Core (API Authorization)

For API access control, role-based permissions, and future external OAuth apps.

Setting
Value

Purpose

API roles, M2M target, external OAuth

OAuth grants

Authorization Code, Client Credentials, Refresh Token

Access token TTL

60 minutes

Refresh token TTL

30 days

Roles (RBAC for API access):

Role
Description

analytics.read

View all analytics (org, team, risks)

analytics.write

Manage analytics settings

surveys.read

View surveys, sends, schedules

surveys.write

Create/send/schedule surveys

sync.read

Preview sync operations

sync.write

Apply sync operations

backfills.read

View backfill operations

backfills.write

Create/cancel backfills

schedules.read

View EventBridge schedules

schedules.write

Manage schedules

notifications.read

View own notifications

notifications.write

Manage notifications

redaction.write

Use PII redaction service

tenants.read

View tenant info

tenants.write

Manage tenant settings

users.read

View users in tenant

users.write

Manage users

admin

Full API access

OAuth Scopes for External Apps:

Scopes map 1:1 with roles. When a third-party app requests scopes, those become the roles in the token:

Scope (OAuth)
Role (Token)
Permissions (DB)

analytics.read

analytics.read

analytics:get, analytics:list, analytics.risks:get, etc.

analytics.write

analytics.write

analytics:update, analytics:delete

surveys.read

surveys.read

surveys:get, surveys:list, surveys.sends:get, etc.

surveys.write

surveys.write

surveys:create, surveys:update, surveys.sends:send, etc.

users.read

users.read

users:get, users:list

users.write

users.write

users:create, users:update, users:delete

Effective permissions = intersection of user's roles and app's scopes. The app can only do what both the user is allowed to do AND the scope permits.

Note: Not all roles are available as OAuth scopes for external apps. Some roles (like sync.*, backfills.*, schedules.*) are internal-only and not exposed to external OAuth clients.

Entity Types (M2M Authentication)

Internal Service Entity Type

For service-to-service authentication between internal systems. Internal services have admin role and can access any tenant.

Setting
Value

Permissions

admin

Access token TTL

15 minutes

External API Client Entity Type

For external M2M integrations created by customers. External M2M apps are bound to a specific tenant via the api_clients table.

Setting
Value

Permissions

Based on granted scopes

Access token TTL

60 minutes

Tenant binding

Via api_clients table

External M2M Tenant Binding:

When a customer creates an API integration, the system creates a FusionAuth Entity and stores the tenant binding:

Token Type
Tenant Access Determined By

User token (OAuth)

User's tenant_users membership

External M2M token

api_clients.tenant_id binding

Internal M2M token

admin role, can access any tenant

Entities

Entity Name
Type
Purpose
Permissions

api-core-service

Internal Service

api-core calling its own authenticated endpoints (sync, backfill)

admin

ml-pipelines

Internal Service

ML pipeline service calling api-core

analytics.read, users.read

data-ingestion

Internal Service

HRIS sync and data ingestion

users.write, tenants.read

analytics-service

Internal Service

Analytics processing

analytics.read

Why api-core Needs an Entity

The api-core service (lambdas + ECS in ../api-core) sometimes needs to call its own authenticated API endpoints:

Example: Sync service calling authenticated API:

Entity Grants

Entity grants define which entities can access which target entities/applications:

Caller Entity
Target
Permissions

api-core-service

ReFresh API - Core

admin

ml-pipelines

ReFresh API - Core

analytics.read, users.read

data-ingestion

ReFresh API - Core

users.write, tenants.read

analytics-service

ReFresh API - Core

analytics.read

OAuth Flows

Flow 1: User Login (Browser)

Standard Authorization Code flow for web application users.

FusionAuth JWT contains:

Application does:

  1. Validate JWT signature via FusionAuth JWKS endpoint

  2. Look up user_identities by fusionauth_id or email

  3. Link identity if not yet linked (set fusionauth_id)

  4. Resolve internal user_id

  5. For tenant routes: validate tenant_users membership

Flow 2: Service-to-Service (Client Credentials)

No user involved. Service authenticates as itself using FusionAuth Entities.

M2M Access token contains:

api-core does:

  1. Validate JWT signature

  2. Check permissions claim for its own entity ID

  3. Authorize based on read/write/admin

External applications access the API on behalf of users with explicit consent.

Third-party access token contains:

Your API does:

  1. Validate JWT

  2. Get user's actual permissions from your DB

  3. Get scope-allowed permissions from token

  4. Effective access = intersection (user must have it AND token must allow it)

Flow 4: Token Refresh

Both user tokens and third-party tokens support refresh.

JWT Architecture

FusionAuth JWT → Neon Directly

The FusionAuth JWT is passed directly to Neon for RLS validation. No separate Neon JWT is generated.

Property
Value

Algorithm

RS256

Issuer

FusionAuth instance URL

Lifetime

60 minutes

Validation

Neon validates via FusionAuth JWKS

FusionAuth JWT Claims:

RLS Helper Functions

Neon uses SQL functions to translate FusionAuth JWT claims into internal identities and permissions:

auth_user(p_fusionauth_id text) - Resolves FusionAuth sub to internal user_id:

has_permission(p_session jsonb, p_permissions text[], p_tenant_id uuid) - Returns permission check with bypass info:

The function queries auth_permission_cache which is populated from auth_assignmentauth_roleauth_role_permissionauth_permission.

The bypass_rls flag is true for users with Platform or Internal roles (like Super Admin, Developer), allowing them to access data across all tenants for support purposes.

Why Direct JWT Validation?

Previous approach (removed): App generated a custom Neon JWT with internal user_id and tenant_id claims.

Current approach: FusionAuth JWT passed directly to Neon.

Benefits:

  1. Future-proof - Database schemas will move to api-core; RLS logic stays database-side

  2. Simpler - No custom JWT generation in the app

  3. Consistent - Same token used for API and database

  4. Maintainable - Identity resolution logic lives in database functions

Identity Resolution

When a FusionAuth JWT arrives, the application must resolve it to an internal user. FusionAuth creates the user on their side during sign-up (via SSO), then the application links to an internal identity.

User Creation Flow

Pre-Provisioning (Refresh Internal Only)

Refresh employees are pre-provisioned in FusionAuth by an admin:

  1. Admin creates FusionAuth user with work email

  2. Admin assigns user to appropriate FusionAuth groups (Engineering, Support, etc.)

  3. When employee logs in via SSO, they already have refresh.* roles in their JWT

Customer employees are never pre-provisioned in FusionAuth. HRIS sync creates user_identities records with fusionauth_id = NULL, and the link is made on first login.

Resolution Logic

Case-Insensitive Email Matching

Critical: Email matching must be case-insensitive throughout the system.

Implementation:

  • Store emails in lowercase (normalized on insert)

  • Compare using LOWER() or case-insensitive collation

  • Apply to: identity resolution, account linking, notification routing

See User Management for complete identity lifecycle details.

Tenant Context

URL Path-Based Tenant Selection

Tenant context comes from the URL path parameter, not headers:

Personal vs Tenant Routes

Route Type
Tenant Context
User ID Used
Example Routes

Personal

Not in URL

Primary user_id

/personal/*, /settings/account

Tenant

:tenantId in URL

Work user_id for that tenant

/tenants/{id}/dashboard, /tenants/{id}/surveys

Token Management

Tokens are stored in httpOnly cookies for security. The application middleware handles token refresh automatically.

Cookie
Purpose
httpOnly
Lifetime

auth_access_token

FusionAuth JWT for Neon RLS

Yes

60 min

auth_refresh_token

Token refresh

Yes

30 days

auth_user

Minimal session info (identity only)

No

60 min

active_tenant_id

Current tenant context

No

Session

auth_user Cookie Contents:

Token Refresh Strategy

  1. Middleware checks auth_access_token expiry on each request

  2. If expired/near-expiry, uses auth_refresh_token to get new tokens

  3. New tokens stored in cookies automatically

  4. If refresh fails, user redirected to FusionAuth login

Why Cookies vs Bearer Tokens?

Approach
Pros
Cons

Cookies (current)

Automatic on all requests, httpOnly security, no client-side code

CSRF protection needed

Bearer tokens

Explicit control, works cross-domain

Client must manage storage, XSS risk

Cookies are preferred for server-rendered SvelteKit apps where most requests originate from the server.

Security Considerations

JWT Validation

  1. Validate signature using FusionAuth JWKS endpoint

  2. Check exp claim for expiration

  3. Verify aud claim matches your application ID

  4. Verify iss claim matches FusionAuth URL

JWKS Endpoint

FusionAuth exposes public keys at /.well-known/jwks.json. Cache this with appropriate TTL (1 hour recommended).

Self-Registration Security

FusionAuth self-registration is enabled, but:

  • New users get no tenant access by default

  • Tenant access only comes from HRIS sync or admin invitation

  • Personal accounts have limited capabilities

Troubleshooting

Common Issues

Problem: User cannot log in

  • Check FusionAuth application credentials

  • Verify redirect URIs match exactly

  • Check FusionAuth tenant is active

Problem: Identity not linking

  • Verify email comparison uses case-insensitive matching (LOWER())

  • Confirm emails are stored normalized (lowercase)

  • Check for duplicate FusionAuth users

  • Review HRIS sync logs

Problem: Tenant access denied

  • Verify tenant_users record exists with status = 'active'

  • Check user is using correct identity for that tenant

  • Verify URL path contains correct :tenantId parameter

Problem: M2M authentication failing

  • Verify Entity credentials

  • Check Entity Grants are configured

  • Verify target entity ID in scope


Last updated: December 2025

Last updated