1. Install dependencies
npm install @civic/auth @fastify/cookie @fastify/cors
Minimal Configuration
const config = {
clientId: "YOUR_CLIENT_ID", // Client ID from auth.civic.com
redirectUrl: 'https://your-backend.com/auth/callback' // change to your domain when deploying
};
Note: All URLs must be absolute URLs.
3. Set up CORS (for frontend integration)
If your frontend runs on a different domain/port, configure CORS to enable cross-origin cookie sharing:
import fastifyCors from '@fastify/cors';
await fastify.register(fastifyCors, {
origin: [
"http://localhost:5173", // frontend (local development)
"http://localhost:3020", // backend (local development)
"https://abc123.ngrok.io", // ngrok tunnel (for cross-origin testing)
"https://your-frontend.com", // production frontend
],
credentials: true, // Allow cookies to be sent cross-origin
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie'],
exposedHeaders: ['Set-Cookie'],
});
Important: Cross-origin cookies (different ports/domains) require HTTPS to work properly. For local development with separate frontend/backend ports, use ngrok or similar service to create HTTPS tunnels:
# Terminal 1: Start your backend
npm start
# Terminal 2: Create HTTPS tunnel to your backend
ngrok http 3020
# Use the https://abc123.ngrok.io URL as your backend URL
# Terminal 3: Create HTTPS tunnel to your frontend
ngrok http 5173
# Use the https://xyz456.ngrok.io URL as your frontend URL
The cookie storage automatically detects HTTPS and sets secure: true
+ sameSite: "none"
for cross-origin compatibility. Without HTTPS, cross-origin cookies will not be saved by the browser.
4. Set up Cookies
Civic Auth uses cookies for storing the login state by default
import Fastify, { FastifyReply, FastifyRequest } from 'fastify';
import fastifyCookie from '@fastify/cookie';
import { CookieStorage, CivicAuth } from '@civic/auth/server';
const fastify = Fastify();
await fastify.register(fastifyCookie, {
secret: "my-secret", // should be changed in production
parseOptions: {}
});
class FastifyCookieStorage extends CookieStorage {
constructor(private request: FastifyRequest, private reply: FastifyReply) {
// Detect if we're running on HTTPS (production) or HTTP (localhost)
const isHttps = request.headers['x-forwarded-proto'] === 'https' ||
request.protocol === 'https';
super({
secure: isHttps, // Use secure cookies for HTTPS
sameSite: isHttps ? "none" : "lax", // none for HTTPS cross-origin, lax for localhost
httpOnly: false, // Allow frontend JavaScript to access cookies
path: "/", // Ensure cookies are available for all paths
});
}
async get(key: string): Promise<string | null> {
return Promise.resolve(this.request.cookies[key] ?? null);
}
async set(key: string, value: string): Promise<void> {
await this.reply.setCookie(key, value, this.settings);
// Store for immediate access within same request
this.request.cookies[key] = value;
}
async delete(key: string): Promise<void> {
await this.reply.clearCookie(key);
}
}
// attach an instance of the cookie storage and civicApi to each request
fastify.decorateRequest('storage', null);
fastify.decorateRequest('civicAuth', null);
fastify.addHook('preHandler', async (request, reply) => {
request.storage = new FastifyCookieStorage(request, reply);
request.civicAuth = new CivicAuth(request.storage, config);
});
5. Create a Login Endpoint
This endpoint will handle login requests, build the Civic login URL and redirect the user to it.
import { buildLoginUrl } from '@civic/auth/server';
fastify.get('/', async (request, reply) => {
const url = await request.civicAuth.buildLoginUrl();
return reply.redirect(url.toString());
});
6. Create the Callback Endpoint
This endpoint handles successful logins and creates the session
fastify.get<{
Querystring: { code: string, state: string }
}>('/auth/callback', async (request, reply) => {
try {
const result = await request.civicAuth.handleCallback({
code: request.query.code,
state: request.query.state,
req: request,
});
if (result.redirectTo) {
return reply.redirect(result.redirectTo);
}
if (result.content) {
return reply.type('text/html').send(result.content);
}
return reply.status(500).send({ error: "Internal server error" });
} catch (error) {
return reply.redirect("/?error=auth_failed");
}
});
7. Create a Logout Endpoint
This endpoint will handle logout requests, build the Civic logout URL and redirect the user to it.
import { buildLogoutRedirectUrl } from '@civic/auth/server';
fastify.get('/auth/logout', async (request, reply) => {
try {
const urlString = await request.civicAuth.buildLogoutRedirectUrl({
postLogoutRedirectUrl: 'https://your-frontend.com/' // Optional: where to redirect after logout
});
await request.civicAuth.clearTokens();
// Convert to URL object to modify parameters
const url = new URL(urlString);
// Remove the state parameter to avoid it showing up in the frontend URL
url.searchParams.delete("state");
return reply.redirect(url.toString());
} catch (error) {
console.error("Logout error:", error);
// If logout URL generation fails, clear tokens and redirect to home
await request.civicAuth.clearTokens();
return reply.redirect("/");
}
});
8. Add an Authentication Hook
This hook protects routes that require login.
import { isLoggedIn } from '@civic/auth/server';
const authHook = async (request: FastifyRequest, reply: FastifyReply) => {
const loggedIn = await request.civicAuth.isLoggedIn();
if (!loggedIn) return reply.status(401).send({ error: 'Unauthorized' });
};
fastify.addHook('preHandler', async (request, reply) => {
// apply to whichever routes need it
if (request.url.includes('/admin')) {
await authHook(request, reply);
}
});
9. Use the Session
If needed, get the logged-in user information.
import { user } from '@civic/auth/server';
fastify.get('/admin/hello', async (request, reply) => {
const user = await request.civicAuth.getUser();
return `Hello, ${user?.name}!`;
});
10. Frontend Integration (Vanilla JavaScript)
Use the @civic/auth/vanillajs
client with your backend:
import { CivicAuth } from "@civic/auth/vanillajs";
// Configure client to use your backend for login URLs
const authClient = await CivicAuth.create({
loginUrl: "https://your-backend.com/auth/login-url", // Your backend endpoint
});
// Now authentication works through your backend
const { user } = await authClient.startAuthentication();
Add this endpoint to expose login URLs:
fastify.get("/auth/login-url", async (request, reply) => {
const loginUrl = await request.civicAuth.buildLoginUrl();
return reply.send({ loginUrl: loginUrl.toString() });
});
Advanced Configuration
For more advanced use cases, you can include additional optional parameters in your configuration:
const config = {
clientId: "YOUR_CLIENT_ID", // Client ID from auth.civic.com
redirectUrl: 'https://your-backend.com/auth/callback', // OAuth callback URL
postLogoutRedirectUrl: 'https://your-frontend.com/', // Where to redirect after logout (Optional)
loginSuccessUrl: 'https://your-frontend.com/', // Optional: redirect Single Page Applications back to frontend after auth (optional)
oauthServer: 'https://auth.civic.com/oauth' // Optional: OAuth server URL (for development/testing)
};
Parameter | Required | Description |
---|
clientId | Yes | Client ID from auth.civic.com |
redirectUrl | Yes | OAuth callback URL where Civic redirects after authentication |
postLogoutRedirectUrl | No | Where to redirect users after logout |
loginSuccessUrl | No | Redirect Single Page Applications back to frontend after successful authentication |
oauthServer | No | OAuth server URL (useful for development/testing environments) |
PKCE and Client Secrets
Civic Auth uses PKCE (Proof Key for Code Exchange), to protect users and clients from unauthorized access to user information. This, alongside domain registration for apps in production environments, mean that you don’t need to provide a client secret in your backend.
When using the Civic Auth SDK, PKCE is handled entirely by the library.