OAuth Integration Guide
How OAuth works with Rakomi
Section titled “How OAuth works with Rakomi”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 ----------| |Understanding the Result type
Section titled “Understanding the Result type”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 errorAlways 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.
Step-by-step integration
Section titled “Step-by-step integration”-
Install the SDK
Terminal window npm install @rakomi/nodeThe SDK has a single dependency (
jose) and uses Node.js built-in modules for everything else. -
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
- Set your redirect URIs (e.g.,
-
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}); -
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 Rakomiredirect(url); -
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 securelyconst { access_token, refresh_token } = result.data;// Clean up PKCE cookiesdeleteCookie('oauth_verifier');deleteCookie('oauth_state'); -
Implement token refresh
const result = await ca.refreshToken({refreshToken: storedRefreshToken,});if (result.ok) {// Update stored tokens — refresh token may have rotatedsaveTokens(result.data.access_token, result.data.refresh_token);}
Next.js App Router example
Section titled “Next.js App Router example”Login route (app/api/auth/login/route.ts)
Section titled “Login route (app/api/auth/login/route.ts)”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');}Express example
Section titled “Express example”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');});Error handling
Section titled “Error handling”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'); }}