1. Install dependencies
Your app will need the following configuration:
const config = {
clientId: // Client ID from auth.civic.com
redirectUrl: 'http://localhost:3000/auth/callback', // change to your domain when deploying
postLogoutRedirectUrl: 'http://localhost:3000/' // The postLogoutRedirectUrl is the URL where the user will be redirected after successfully logging out from Civic's auth server.
};
redirectUrl
and postLogoutRedirectUrl
must be absolute URLs.
3. 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().register(fastifyCookie, {
secret: "my-secret", // should be changed in production
parseOptions: {}
})
class FastifyCookieStorage extends CookieStorage {
constructor(private request: FastifyRequest, private reply: FastifyReply) {
super();
}
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);
}
}
// 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);
});
4. 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());
});
5. Create the Callback Endpoint
This endpoint handles successful logins and creates the session
import { resolveOAuthAccessCode } from '@civic/auth/server';
fastify.get<{
Querystring: { code: string, state: string }
}>('/auth/callback', async (request, reply) => {
await request.civicAuth.resolveOAuthAccessCode(request.query.code, request.query.state);
reply.redirect('/admin/hello');
});
6. 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';
app.get('/auth/logout', async (req: Request, res: Response) => {
const url = await request.civicAuth.buildLogoutRedirectUrl();
res.redirect(url.toString());
});
7. Add an Authentication Hook
This hook protects routes that require login.
import { isLoggedIn } from '@civic/auth/server';
fastify.addHook('preHandler', async (request, reply) => {
// apply to whichever routes need it
if (!request.url.includes('/admin')) return;
const loggedIn = await request.civicAuth.isLoggedIn();
if (!loggedIn) return reply.status(401).send({ error: 'Unauthorized' });
});
8. 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}!`;
});
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.