AbsoluteJS
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?

Interchangeability

All OAuth 2.0 providers follow the same flow. Citra abstracts this into a unified interface.

Type Safety

TypeScript 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
1import { createOAuth2Client } from 'citra';
2
3const googleClient = await createOAuth2Client('google', {
4  clientId: 'YOUR_CLIENT_ID',
5  clientSecret: 'YOUR_CLIENT_SECRET',
6  redirectUri: 'https://yourapp.com/auth/callback'
7});

#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
1const currentState = generateState();
2const codeVerifier = generateCodeVerifier();
3const authUrl = await googleClient.createAuthorizationUrl({
4	codeVerifier,
5	scope: ['profile', 'openid'],
6	searchParams: [
7		['access_type', 'offline'],
8		['prompt', 'consent']
9	],
10	state: currentState
11});
12
13// Store state and PKCE verifier in HttpOnly cookies
14const headers = new Headers();
15headers.set('Location', authUrl.toString());
16headers.append(
17	'Set-Cookie',
18	`oauth_state=${currentState}; HttpOnly; Path=/; Secure; SameSite=Lax`
19);
20headers.append(
21	'Set-Cookie',
22	`pkce_code_verifier=${codeVerifier}; HttpOnly; Path=/; Secure; SameSite=Lax`
23);

#Handling the Callback

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

TS
1// Parse callback URL parameters
2const request = new Request('https://yourapp.com/auth/callback');
3const params = new URL(request.url).searchParams;
4const code = params.get('code');
5const callback_state = params.get('state');
6
7// Retrieve stored state and code verifier from cookies
8const cookieHeader = request.headers.get('cookie') ?? '';
9const cookies = cookieHeader.trim()
10  ? Object.fromEntries(
11      cookieHeader
12        .split('; ')
13        .filter(c => c.includes('='))
14        .map(c => c.split('='))
15    )
16  : {};
17
18const stored_state = cookies['oauth_state'];
19const codeVerifier = cookies['pkce_code_verifier'];
20
21// Validate required cookies are present
22if (!stored_state) {
23  throw new Error('Missing oauth_state cookie');
24}
25if (!codeVerifier) {
26  throw new Error('Missing pkce_code_verifier cookie');
27}
28
29// Validate state to prevent CSRF attacks
30if (!callback_state || callback_state !== stored_state) {
31  throw new Error('Invalid state mismatch');
32}
33
34// Validate code is present
35if (!code) {
36  throw new Error('Authorization code not found');
37}
38
39// Exchange authorization code for tokens
40const tokenResponse = await googleClient.validateAuthorizationCode({
41  code,
42  codeVerifier
43});

#Fetching the User Profile

Exchange the access token for user information:

TS
1// Get the access token from server session
2const session = await getSession(request);
3const accessToken = session.accessToken;
4
5const profile = await googleClient.fetchUserProfile(accessToken);
6console.log(profile);

#Token Management

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

TS
1// Get the refresh token from server session
2const session = await getSession(request);
3const refreshToken = session.refreshToken;
4
5if (refreshToken) {
6  const newTokens = await googleClient.refreshAccessToken(refreshToken);
7}
TS
1// Get the access token from server session
2const session = await getSession(request);
3const accessToken = session.accessToken;
4
5if (isRevocableProviderOption('google')) {
6  await googleClient.revokeToken(accessToken);
7}

#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
1import { createOIDCClient } from 'citra';
2
3// Discovery-driven: only the issuer + client credentials. The authorize, token,
4// userinfo, and JWKS endpoints are resolved at runtime from
5// {issuer}/.well-known/openid-configuration. Works with any compliant IdP.
6const client = await createOIDCClient({
7  issuer: 'https://acme.okta.com',
8  clientId: 'YOUR_CLIENT_ID',
9  clientSecret: 'YOUR_CLIENT_SECRET',
10  redirectUri: 'https://yourapp.com/auth/callback'
11});
12
13const { url, state, codeVerifier, nonce } = await client.createAuthorizationUrl({
14  scope: ['openid', 'email', 'profile']
15});
16// …redirect, then on callback:
17const tokens = await client.validateAuthorizationCode({ code, codeVerifier });
18const 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
1import { verifyIdToken } from 'citra';
2
3// In-house JWKS verification (RS256 / ES256) via WebCrypto — no 'jose' dependency.
4// Checks the signature against the issuer's JWKS plus iss / aud / exp (+skew) /
5// nonce / sub. Network-free if you pass a cached JWKS.
6const claims = await verifyIdToken(tokens.id_token, {
7  issuer: 'https://acme.okta.com',
8  audience: 'YOUR_CLIENT_ID',
9  nonce
10});
11
12console.log(claims.sub, claims.email);

#Supported Providers

Citra supports 73 OAuth 2.0 providers:

42 logo42
Amazon Cognito logoAmazon Cognito
AniList logoAniList
Apple logoApple
Atlassian logoAtlassian
Attio logoAttio
Auth0 logoAuth0
Authentik logoAuthentik
Autodesk logoAutodesk
Battle.net logoBattle.net
Bitbucket logoBitbucket
Box logoBox
Bungie logoBungie
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 ID logoMicrosoft Entra ID
monday.com logomonday.com
MyAnimeList logoMyAnimeList
Naver logoNaver
Notion logoNotion
Okta logoOkta
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
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