Skip to content

OAuth Integration Guide

The OAuth 2.0 Authorization Code flow with PKCE allows your application to authenticate users through Rakomi’s hosted login and consent screens.

Your App Rakomi User
| | |
|-- 1. Generate PKCE ------->| |
|-- 2. Redirect to /oauth/authorize ------------------> |
| |-- 3. Login + Consent ---> |
| |<-- 4. Approve ----------- |
|<-- 5. Callback with code --| |
|-- 6. Exchange code ------->| |
|<-- 7. Tokens --------------| |
| | |
|-- 8. Refresh token ------->| (when access expires) |
|<-- 9. New tokens ----------| |

Before diving into the code, understand that OAuth SDK methods never throw exceptions. They return a VerifyResult:

type VerifyResult<T> =
| { ok: true; data: T } // Success — access data
| { ok: false; error: SdkError }; // Failure — handle error

Always check result.ok before accessing result.data. The error object includes a code, message, suggestion, and docs_url to help you handle each failure mode.

  1. Install the SDK

    Terminal window
    npm install @rakomi/node

    The SDK has a single dependency (jose) and uses Node.js built-in modules for everything else.

  2. Register an OAuth client

    In the Rakomi dashboard, create an OAuth client:

    • Set your redirect URIs (e.g., https://app.example.com/api/auth/callback)
    • Note your client ID and client secret
  3. Configure the SDK

    import { Rakomi } from '@rakomi/node';
    const ca = new RakomiClient({
    apiKey: process.env.RAKOMI_API_KEY!,
    clientId: process.env.OAUTH_CLIENT_ID!,
    clientSecret: process.env.OAUTH_CLIENT_SECRET!,
    baseUrl: process.env.RAKOMI_URL, // optional
    });
  4. Implement the login route

    Generate PKCE + state, store them server-side, and redirect:

    const pkce = ca.generatePKCE();
    const state = ca.generateState();
    // Store verifier + state in HTTP-only cookies (server-side only!)
    setCookie('oauth_verifier', pkce.codeVerifier, { httpOnly: true, secure: true, sameSite: 'lax' });
    setCookie('oauth_state', state, { httpOnly: true, secure: true, sameSite: 'lax' });
    const url = ca.buildAuthorizeUrl({
    redirectUri: 'https://app.example.com/api/auth/callback',
    codeChallenge: pkce.codeChallenge,
    state,
    });
    // Redirect user to Rakomi
    redirect(url);
  5. Implement the callback route

    Verify state, exchange the code for tokens:

    // Verify state matches (CSRF protection)
    const storedState = getCookie('oauth_state');
    if (callbackState !== storedState) {
    return error(403, 'State mismatch — possible CSRF attack');
    }
    const codeVerifier = getCookie('oauth_verifier');
    const result = await ca.exchangeCode({
    code: callbackCode,
    codeVerifier,
    redirectUri: 'https://app.example.com/api/auth/callback',
    });
    if (!result.ok) {
    switch (result.error.code) {
    case 'oauth/invalid_grant':
    return redirect('/login?error=expired');
    case 'oauth/invalid_client':
    throw new Error('OAuth misconfiguration');
    case 'oauth/network_error':
    return error(502, 'Auth server unreachable');
    }
    }
    // Store tokens securely
    const { access_token, refresh_token } = result.data;
    // Clean up PKCE cookies
    deleteCookie('oauth_verifier');
    deleteCookie('oauth_state');
  6. Implement token refresh

    const result = await ca.refreshToken({
    refreshToken: storedRefreshToken,
    });
    if (result.ok) {
    // Update stored tokens — refresh token may have rotated
    saveTokens(result.data.access_token, result.data.refresh_token);
    }
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { Rakomi } from '@rakomi/node';
const ca = new RakomiClient({
apiKey: process.env.RAKOMI_API_KEY!,
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
});
export async function GET() {
const pkce = ca.generatePKCE();
const state = ca.generateState();
const cookieStore = await cookies();
cookieStore.set('oauth_verifier', pkce.codeVerifier, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600 });
cookieStore.set('oauth_state', state, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600 });
const url = ca.buildAuthorizeUrl({
redirectUri: `${process.env.APP_URL}/api/auth/callback`,
codeChallenge: pkce.codeChallenge,
state,
});
redirect(url);
}

Callback route (app/api/auth/callback/route.ts)

Section titled “Callback route (app/api/auth/callback/route.ts)”
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
import { Rakomi } from '@rakomi/node';
const ca = new RakomiClient({
apiKey: process.env.RAKOMI_API_KEY!,
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
});
export async function GET(request: NextRequest) {
const code = request.nextUrl.searchParams.get('code');
const state = request.nextUrl.searchParams.get('state');
const cookieStore = await cookies();
const storedState = cookieStore.get('oauth_state')?.value;
const codeVerifier = cookieStore.get('oauth_verifier')?.value;
if (!code || !state || state !== storedState || !codeVerifier) {
redirect('/login?error=invalid_callback');
}
const result = await ca.exchangeCode({
code,
codeVerifier,
redirectUri: `${process.env.APP_URL}/api/auth/callback`,
});
cookieStore.delete('oauth_verifier');
cookieStore.delete('oauth_state');
if (!result.ok) {
redirect(`/login?error=${result.error.code}`);
}
// Store tokens in session/cookie
cookieStore.set('access_token', result.data.access_token, { httpOnly: true, secure: true, sameSite: 'lax' });
if (result.data.refresh_token) {
cookieStore.set('refresh_token', result.data.refresh_token, { httpOnly: true, secure: true, sameSite: 'lax' });
}
redirect('/dashboard');
}
import express from 'express';
import cookieParser from 'cookie-parser';
import { Rakomi } from '@rakomi/node';
const app = express();
app.use(cookieParser());
const ca = new RakomiClient({
apiKey: process.env.RAKOMI_API_KEY!,
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
});
app.get('/auth/login', (req, res) => {
const pkce = ca.generatePKCE();
const state = ca.generateState();
res.cookie('oauth_verifier', pkce.codeVerifier, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600000 });
res.cookie('oauth_state', state, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600000 });
const url = ca.buildAuthorizeUrl({
redirectUri: `${process.env.APP_URL}/auth/callback`,
codeChallenge: pkce.codeChallenge,
state,
});
res.redirect(url);
});
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== req.cookies.oauth_state) {
return res.status(403).json({ error: 'State mismatch' });
}
const result = await ca.exchangeCode({
code: code as string,
codeVerifier: req.cookies.oauth_verifier,
redirectUri: `${process.env.APP_URL}/auth/callback`,
});
res.clearCookie('oauth_verifier');
res.clearCookie('oauth_state');
if (!result.ok) {
return res.redirect(`/login?error=${result.error.code}`);
}
// Store tokens in session
req.session.accessToken = result.data.access_token;
req.session.refreshToken = result.data.refresh_token;
res.redirect('/dashboard');
});

Handle each error type differently for the best user experience:

const result = await ca.exchangeCode({ code, codeVerifier, redirectUri });
if (!result.ok) {
const { code, message, suggestion, docs_url } = result.error;
switch (code) {
case 'oauth/invalid_grant':
// Code expired or already used — send user back to login
return redirect('/login?error=expired');
case 'oauth/invalid_client':
// Credentials wrong — log and alert ops
console.error('OAuth client misconfigured:', message);
return error(500, 'Authentication configuration error');
case 'oauth/invalid_request':
// Missing params — likely a bug
console.error('OAuth request error:', message, suggestion);
return error(400, 'Invalid authentication request');
case 'oauth/network_error':
// Transient — retry with backoff
return error(502, 'Authentication service temporarily unavailable');
default:
console.error(`Unexpected OAuth error [${code}]: ${message}`);
return error(500, 'Authentication failed');
}
}