Exchange the code for tokens_
Handle the callback, exchange the authorization code for tokens on the server, and sign the user in.
1 min read
The OAuth2 server redirects back to Vantage's redirect URI with a code and the state. Vantage exchanges that code for tokens on its server, reads the user's profile, and signs them in.
Add the token functions
Extend consumer/src/lib/oauth.ts with the exchange and userinfo calls. The exchange authenticates with the client secret using HTTP Basic auth, which is why it must run on the server.
// consumer/src/lib/oauth.ts (additions)const clientSecret = process.env.OAUTH_CLIENT_SECRET!
export type Tokens = { access_token: string refresh_token: string id_token: string expires_in: number scope: string}
/** Exchange an authorization code for tokens. */export async function exchangeCode(code: string): Promise<Tokens> { const basic = Buffer.from(`${clientId}:${clientSecret}`).toString('base64') const res = await fetch(`${issuer}/token`, { method: 'POST', headers: { Authorization: `Basic ${basic}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: redirectUri, }), }) if (!res.ok) throw new Error(`Token exchange failed: ${res.status}`) return res.json()}
export type UserInfo = { sub: string name?: string email?: string email_verified?: boolean}
/** Read the signed-in user's profile from the userinfo endpoint. */export async function fetchUserInfo(accessToken: string): Promise<UserInfo> { const res = await fetch(`${issuer}/userinfo`, { headers: { Authorization: `Bearer ${accessToken}` }, }) if (!res.ok) throw new Error(`userinfo failed: ${res.status}`) return res.json()}Handle the callback
Create consumer/src/routes/oauth.callback.tsx. Its loader runs on the server: it checks the state against the session, exchanges the code, reads the profile, stores it in the session, and sends the user to the dashboard.
// consumer/src/routes/oauth.callback.tsximport { createFileRoute, redirect } from '@tanstack/react-router'import { createServerFn } from '@tanstack/react-start'import { getRequestUrl } from '@tanstack/react-start/server'import { exchangeCode, fetchUserInfo, vantageSession } from '../lib/oauth'
const handleCallback = createServerFn().handler(async () => { const url = getRequestUrl() const code = url.searchParams.get('code') const state = url.searchParams.get('state') const session = await vantageSession()
// The state must match the value we set in /oauth/start. if (!code || !state || state !== session.data.state) { throw redirect({ to: '/', search: { error: 'invalid_state' } }) }
const tokens = await exchangeCode(code) const user = await fetchUserInfo(tokens.access_token)
await session.update({ accessToken: tokens.access_token, user, state: undefined, })
throw redirect({ to: '/dashboard' })})
export const Route = createFileRoute('/oauth/callback')({ loader: async () => { await handleCallback() }, component: () => null,})The client secret and the tokens live only inside these server functions. The browser only ever holds Vantage's own signed session cookie.
Show the signed-in user

The dashboard reads the user from the session and renders it. Create consumer/src/routes/dashboard.tsx:
// consumer/src/routes/dashboard.tsx (loader)import { createFileRoute, redirect } from '@tanstack/react-router'import { createServerFn } from '@tanstack/react-start'import { vantageSession, type UserInfo } from '../lib/oauth'
const loadUser = createServerFn().handler(async (): Promise<UserInfo> => { const session = await vantageSession() if (!session.data.user) throw redirect({ to: '/' }) return session.data.user as UserInfo})
export const Route = createFileRoute('/dashboard')({ component: Dashboard, loader: async () => ({ user: await loadUser() }),})The component renders user.name, user.email, and user.sub from the loader data. That profile came from TaskFlow, through the authorization code flow you built. Run the whole thing next.
Was this page helpful?
Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.