A proof-of-concept TypeScript application demonstrating FusionAuth OIDC authentication using the authorization code flow with httpOnly session cookies.
- Docker (docker compose)
- If connecting to AIMS FusionAuth SaaS instance (not local FusionAuth), FusionAuth URL and client ID/secret
- Clone this project
- Create
/fusionauth/.envfrom the contents of/fusionauth/.env.local, e.g.,cp fusionauth/.env.local fusionauth/.env - Build/start the Docker Compose stack consisting of FusionAuth and the AIMS Authentication Example app.
docker compose up
- Access the app at
http://localhost:3000.
- Follow the same local start steps above, but replace the FUSIONAUTH_ISSUER, FUSIONAUTH_CLIENT_ID, and FUSIONAUTH_CLIENT_SECRET values in
docker-compose.ymlwith values provided from the AIMS team.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ React + Vite │────▶│ Express.js │────▶│ FusionAuth │
│ Frontend │◀────│ Backend │◀────│ IdP │
└─────────────────┘ └─────────────────┘ └─────────────────┘
- Frontend: React + Vite + React Router with protected routes
- Backend: Express.js serves frontend app and handles authentication
- Auth: FusionAuth as the OIDC IdP
| Other IdPs | FusionAuth | Notes |
|---|---|---|
| Realm / Organization | Tenant | Isolated user pools with separate config. This project creates a dev tenant. |
| OAuth Client / App Registration | Application | Where you configure OAuth settings, redirect URIs, grants, etc. |
| User Assignment / App Access | Registration | The explicit link between a User and an Application—users must be registered to an app to authenticate. |
| Rules / Actions / Mappers | Lambdas | JavaScript functions to customize tokens, reconcile external IdP users, etc. |
| Custom Attributes / User Metadata | user.data or registration.data | Unstructured JSON. Registration data is app-scoped; user data is global. |
Tenants
FusionAuth is multi-tenant by design. Each tenant has completely isolated users, applications, and configuration. Cross-tenant SSO is possible but opt-in. In this example, we create a separate dev tenant rather than using the default tenant—a best practice to avoid polluting the FusionAuth admin space.
Applications vs Registrations This is the biggest conceptual difference from most IdPs:
- An Application is the OAuth client configuration (client ID, secret, redirect URIs)
- A Registration is a user's membership in that application, including app-specific roles and custom data
Users without a registration for your application will be rejected at login (when requireRegistration: true). This gives you fine-grained control; a user can exist in FusionAuth but not have access to your specific app.
User (global identity)
└── Registration (per-application)
├── roles: ["admin", "editor"]
└── data: { jurisdiction_id: "CA" }
Lambdas (Token Customization)
Lambdas are JavaScript functions that run at specific points in the auth lifecycle. This project uses a JWTPopulate lambda to add a custom claim:
function populate(jwt, user, registration) {
jwt.jurisdiction_id = registration.data.jurisdiction_id;
}Kickstart (Infrastructure as Code)
Kickstart is FusionAuth's declarative provisioning system. The kickstart.json file in this project bootstraps:
- API keys
- Tenants
- Applications with OAuth configuration
- Custom form fields
- Lambdas
- Test users with registrations
This runs automatically on first startup when FUSIONAUTH_APP_KICKSTART_FILE is set. Think of it as a migration that only runs once on an empty database.
Use docker compose down -v to clear the FusionAuth volumes and wipe all of the Kickstart configuration.
OIDC Discovery
FusionAuth fully supports OIDC discovery at /.well-known/openid-configuration. This project uses the openid-client library which auto-configures from the issuer URL—no manual endpoint configuration needed.
Internal vs External Issuer URLs
When running in Docker, the backend talks to FusionAuth via the internal Docker network (http://fusionauth:9011), but redirect URLs sent to the browser must use the external URL (http://localhost:9011). See FUSIONAUTH_ISSUER vs FUSIONAUTH_INTERNAL_ISSUER in the config.
Session vs Token Storage This implementation stores user claims in a server-side session (httpOnly cookie) rather than exposing tokens to the client. The access token could alternatively be stored for API-to-API calls or refresh token rotation.
Self-Registration
Self-registration is disabled in this example (registrationConfiguration.enabled: false). Users must be pre-created with registrations via the Admin UI, API, or Kickstart. Enable this for consumer-facing apps where users sign themselves up.
FusionAuth's admin UI is available at your issuer URL (default: http://localhost:9011). Key locations:
- Applications: Settings → Applications (OAuth config, lambdas, JWT settings)
- Users: Users → [search] → Manage (see registrations per user)
- Registrations: Users → [user] → Registrations tab (app-specific data/roles)
- Lambdas: Customizations → Lambdas
- API Keys: Settings → API Keys
- Themes: Customizations → Themes (login page styling)
This project uses standard OIDC (openid-client) rather than FusionAuth-specific SDKs. FusionAuth also provides:
- @fusionauth/typescript-client: Full API client for user management, admin operations
- @fusionauth/react-sdk: React hooks for client-side auth (alternative approach)
The OIDC approach shown here is portable and recommended when you only need authentication, not user management.
- Authorization code flow
- Secure httpOnly session cookies (
auth-session) - Protected client-side routes with React Router
- State and nonce verification for CSRF protection
- Automatic session management
| Route | Method | Description |
|---|---|---|
/api/login |
GET | Redirects to FusionAuth login |
/api/auth/callback |
GET | OIDC callback - exchanges code for tokens, creates session |
/api/logout |
GET | Clears session and redirects to FusionAuth logout |
/api/user |
GET | Returns current user object or null |
The ./fusionauth/kickstart/kickstart.json file sets up 5 users on the initial start up.
- 1 admin user (admin@example.com)
- 4 application users
Since self-registration is disabled, users must be created via the Admin UI, API, or the provided script.
Due to licensing limitations for local development, it is easiest to create additional users via the ./scripts/add-user.sh script as detailed below.
Using the add-user script:
# Make script executable (first time only)
chmod +x scripts/add-user.sh
# Interactive mode (prompts for all fields)
./scripts/add-user.sh
# Or provide all arguments at once
./scripts/add-user.sh email@example.com username "Full Name" password CA
# With custom user ID (optional)
./scripts/add-user.sh email@example.com username "Full Name" password CA <user-id>Required environment variables (use defaults from script or export):
FUSIONAUTH_API_KEYFUSIONAUTH_ISSUERFUSIONAUTH_TENANT_IDFUSIONAUTH_APPLICATION_ID
The script will:
- Create a user in FusionAuth
- Register the user to the application with the specified
jurisdiction_id - Make the user active for authentication
aims-auth-example/
├── backend/
│ ├── src/
│ │ ├── index.ts # Express server entry point
│ │ ├── config.ts # Environment configuration
│ │ ├── routes/
│ │ │ └── auth.ts # Authentication routes
│ │ └── middleware/
│ │ └── session.ts # Session middleware
│ ├── package.json
│ └── tsconfig.json
├── frontend/
│ ├── src/
│ │ ├── main.tsx # React entry point
│ │ ├── App.tsx # Router configuration
│ │ ├── pages/
│ │ │ ├── Landing.tsx # Public landing page
│ │ │ └── Dashboard.tsx # Protected dashboard
│ │ ├── components/
│ │ │ └── ProtectedRoute.tsx
│ │ └── hooks/
│ │ └── useAuth.ts # Authentication hook
│ ├── index.html
│ ├── package.json
│ └── vite.config.ts
├── scripts/
│ └── add-user.sh # Script to add new FusionAuth users
└── docker-compose.yml # Compose file for spinning up the project
└── Dockerfile # Build instructions for the example application
└── README.md