AbsoluteJS

Citra

A curated collection of OAuth 2.0 provider configurations, each bundled with the correct endpoints and request details. Ready-to-use foundation for secure authentication in TypeScript applications.

View on GitHub

#Why Citra?

InterchangeabilityAll OAuth 2.0 providers follow the same flow. Citra abstracts this into a unified interface.
Type SafetyTypeScript generics and type guards catch configuration mistakes at compile time.

Inspired by Arctic, Citra reduces boilerplate and minimizes integration errors by enforcing a uniform configuration approach.

#Installation

BASH
bun install citra

#Getting Started

Citra uses strong TypeScript typing to help you build OAuth clients safely. Each provider includes its own typed configuration schema, ensuring you can't pass unsupported parameters or omit required ones.

TS
import { createOAuth2Client } from 'citra';

const googleClient = await createOAuth2Client('google', {
  clientId: 'YOUR_CLIENT_ID',
  clientSecret: 'YOUR_CLIENT_SECRET',
  redirectUri: 'https://yourapp.com/auth/callback'
});

#Building the Authorization URL

Generate a fully customized authorization URL for redirecting users to the provider's login page. Every option is strongly typed and context-aware, with full control over PKCE, scopes, and provider-specific parameters.

TS
const currentState = generateState();
const codeVerifier = generateCodeVerifier();
const authUrl = await googleClient.createAuthorizationUrl({
	codeVerifier,
	scope: ['profile', 'openid'],
	searchParams: [
		['access_type', 'offline'],
		['prompt', 'consent']
	],
	state: currentState
});

// Store state and PKCE verifier in HttpOnly cookies
const headers = new Headers();
headers.set('Location', authUrl.toString());
headers.append(
	'Set-Cookie',
	`oauth_state=${currentState}; HttpOnly; Path=/; Secure; SameSite=Lax`
);
headers.append(
	'Set-Cookie',
	`pkce_code_verifier=${codeVerifier}; HttpOnly; Path=/; Secure; SameSite=Lax`
);

#Handling the Callback

Exchange the authorization code and PKCE verifier for an OAuth2 token response:

TS
// Parse callback URL parameters
const request = new Request('https://yourapp.com/auth/callback');
const params = new URL(request.url).searchParams;
const code = params.get('code');
const callback_state = params.get('state');

// Retrieve stored state and code verifier from cookies
const cookieHeader = request.headers.get('cookie') ?? '';
const cookies = cookieHeader.trim()
  ? Object.fromEntries(
      cookieHeader
        .split('; ')
        .filter(c => c.includes('='))
        .map(c => c.split('='))
    )
  : {};

const stored_state = cookies['oauth_state'];
const codeVerifier = cookies['pkce_code_verifier'];

// Validate required cookies are present
if (!stored_state) {
  throw new Error('Missing oauth_state cookie');
}
if (!codeVerifier) {
  throw new Error('Missing pkce_code_verifier cookie');
}

// Validate state to prevent CSRF attacks
if (!callback_state || callback_state !== stored_state) {
  throw new Error('Invalid state mismatch');
}

// Validate code is present
if (!code) {
  throw new Error('Authorization code not found');
}

// Exchange authorization code for tokens
const tokenResponse = await googleClient.validateAuthorizationCode({
  code,
  codeVerifier
});

#Fetching the User Profile

Exchange the access token for user information:

TS
// Get the access token from server session
const session = await getSession(request);
const accessToken = session.accessToken;

const profile = await googleClient.fetchUserProfile(accessToken);
console.log(profile);

#Token Management

If supported by the provider, you can refresh and revoke tokens:

TS
// Get the refresh token from server session
const session = await getSession(request);
const refreshToken = session.refreshToken;

if (refreshToken) {
  const newTokens = await googleClient.refreshAccessToken(refreshToken);
}
TS
// Get the access token from server session
const session = await getSession(request);
const accessToken = session.accessToken;

if (isRevocableProviderOption('google')) {
  await googleClient.revokeToken(accessToken);
}

#OIDC Client

Beyond the per-provider OAuth clients, Citra ships a discovery-driven OpenID Connect client for connecting to any compliant IdP at runtime — the foundation for enterprise SSO.

TS
import { createOIDCClient } from 'citra';

// Discovery-driven: only the issuer + client credentials. The authorize, token,
// userinfo, and JWKS endpoints are resolved at runtime from
// {issuer}/.well-known/openid-configuration. Works with any compliant IdP.
const client = await createOIDCClient({
  issuer: 'https://acme.okta.com',
  clientId: 'YOUR_CLIENT_ID',
  clientSecret: 'YOUR_CLIENT_SECRET',
  redirectUri: 'https://yourapp.com/auth/callback'
});

const { url, state, codeVerifier, nonce } = await client.createAuthorizationUrl({
  scope: ['openid', 'email', 'profile']
});
// …redirect, then on callback:
const tokens = await client.validateAuthorizationCode({ code, codeVerifier });
const profile = await client.fetchUserProfile(tokens.access_token);

ID tokens are verified in-house against the issuer's JWKS (RS256 / ES256) via WebCrypto — no extra crypto dependency:

TS
import { verifyIdToken } from 'citra';

// In-house JWKS verification (RS256 / ES256) via WebCrypto — no 'jose' dependency.
// Checks the signature against the issuer's JWKS plus iss / aud / exp (+skew) /
// nonce / sub. Network-free if you pass a cached JWKS.
const claims = await verifyIdToken(tokens.id_token, {
  issuer: 'https://acme.okta.com',
  audience: 'YOUR_CLIENT_ID',
  nonce
});

console.log(claims.sub, claims.email);

#Supported Providers

Citra supports 78 OAuth 2.0 providers:

42 logo42
Amazon Cognito logoAmazon Cognito
AniList logoAniList
Apple logoApple
Atlassian logoAtlassian
Attio logoAttio
Auth0 logoAuth0
Authentik logoAuthentik
Autodesk logoAutodesk
Azure AD B2C logoAzure AD B2C
Battle.net logoBattle.net
Bitbucket logoBitbucket
Box logoBox
Bungie logoBungie
Calendly logoCalendly
Close logoClose
Coinbase logoCoinbase
Discord logoDiscord
Donation Alerts logoDonation Alerts
Dribbble logoDribbble
Dropbox logoDropbox
Epic Games logoEpic Games
Etsy logoEtsy
Facebook logoFacebook
Figma logoFigma
Gitea logoGitea
GitHub logoGitHub
GitLab logoGitLab
GoHighLevel logoGoHighLevel
Google logoGoogle
HubSpot logoHubSpot
Intuit logoIntuit
Kakao logoKakao
Keycloak logoKeycloak
Kick logoKick
Lichess logoLichess
LINE logoLINE
Linear logoLinear
LinkedIn logoLinkedIn
Mastodon logoMastodon
Mercado Libre logoMercado Libre
Mercado Pago logoMercado Pago
Microsoft Entra External ID logoMicrosoft Entra External ID
Microsoft Entra ID logoMicrosoft Entra ID
monday.com logomonday.com
MyAnimeList logoMyAnimeList
Naver logoNaver
Notion logoNotion
Okta logoOkta
onSpark logoonSpark
osu! logoosu!
Patreon logoPatreon
Pipedrive logoPipedrive
Polar logoPolar
Polar Access Link logoPolar Access Link
Polar Team Pro logoPolar Team Pro
Reddit logoReddit
Roblox logoRoblox
Salesforce logoSalesforce
Shikimori logoShikimori
Slack logoSlack
Slack User logoSlack User
Spotify logoSpotify
Start.gg logoStart.gg
Strava logoStrava
Synology logoSynology
TikTok logoTikTok
Tiltify logoTiltify
Tumblr logoTumblr
Twitch logoTwitch
Twitter / X logoTwitter / X
VK logoVK
Withings logoWithings
WorkOS logoWorkOS
Yahoo logoYahoo
Yandex logoYandex
Zoho logoZoho
Zoom logoZoom

Current package surface

What ships today

citrav0.29.11 · betaAuth & IdentitynpmSource

Outcomes

What you can build

Table of Contents

Introduction

Introduction

Citra is a curated collection of OAuth 2.0 provider configurations, each bundled with the correct endpoints and request details. It provides a ready-to-use foundation for integrating secure authentication into JavaScript and TypeScript applications. See the complete Citra guide for installation, provider configuration, and integration patterns.

Why Citra?

Interchangeability: All OAuth 2.0 providers follow the same authorization flow, and Citra abstracts this process into a unified interface (see arctic interchangeability issue).

Hardening checklist

Production guidance

Make every external boundary explicitPin the deployed citra version, replace example or memory-backed dependencies with durable implementations, bound external calls, protect credentials, and emit enough evidence to retry or recover safely.

Follow in order

Troubleshooting path

1
Trace from the first failed boundary
Reproduce the smallest canonical citra example, confirm the supported entry point and version in the API explorer, then inspect the first boundary that did not produce its documented result.