The @ave-id/sdk/server module provides server-side helpers for exchanging authorization codes and refresh tokens using your client secret. These functions should only be called from your backend to keep your secret secure.
Never use server helpers in client-side code. Your clientSecret must remain confidential.
Import Server Helpers
import {
exchangeCodeServer ,
refreshTokenServer ,
exchangeDelegatedTokenServer ,
} from "@ave-id/sdk/server" ;
Why Use Server Helpers?
Server-side token exchange is more secure than client-side exchange because:
Client secret protection : Secrets never leave your backend
Token security : Tokens can be stored in HTTP-only cookies
Better control : You can validate and log token exchanges
Quick Start
Exchange an authorization code for tokens on your server:
import { exchangeCodeServer } from "@ave-id/sdk/server" ;
const tokens = await exchangeCodeServer (
{
clientId: "YOUR_CLIENT_ID" ,
clientSecret: process . env . AVE_CLIENT_SECRET ! ,
redirectUri: "https://yourapp.com/callback" ,
},
{ code: "CODE_FROM_CALLBACK" }
);
console . log ( tokens . access_token );
API Reference
exchangeCodeServer
Exchanges an authorization code for access tokens using client credentials.
function exchangeCodeServer (
config : ServerConfig ,
payload : { code : string }
) : Promise < TokenResponse >
Server configuration object Your application’s client ID
Your application’s client secret (keep this secure!)
The redirect URI used in the authorization request
issuer
string
default: "https://aveid.net"
Ave issuer URL
Authorization code from the OAuth callback
Returns: TokenResponse object
JWT version of the access token
OIDC ID token (if openid scope requested)
Refresh token (if offline_access scope requested)
Token lifetime in seconds
Granted scopes (space-separated)
Example:
// app/api/auth/callback/route.ts
import { NextRequest , NextResponse } from "next/server" ;
import { exchangeCodeServer } from "@ave-id/sdk/server" ;
export async function GET ( request : NextRequest ) {
const searchParams = request . nextUrl . searchParams ;
const code = searchParams . get ( "code" );
if ( ! code ) {
return NextResponse . json ({ error: "Missing code" }, { status: 400 });
}
try {
const tokens = await exchangeCodeServer (
{
clientId: process . env . AVE_CLIENT_ID ! ,
clientSecret: process . env . AVE_CLIENT_SECRET ! ,
redirectUri: ` ${ process . env . NEXT_PUBLIC_APP_URL } /api/auth/callback` ,
},
{ code }
);
// Store tokens in HTTP-only cookie
const response = NextResponse . redirect (
new URL ( "/dashboard" , request . url )
);
response . cookies . set ( "ave_access_token" , tokens . access_token , {
httpOnly: true ,
secure: true ,
sameSite: "lax" ,
maxAge: tokens . expires_in ,
});
if ( tokens . refresh_token ) {
response . cookies . set ( "ave_refresh_token" , tokens . refresh_token , {
httpOnly: true ,
secure: true ,
sameSite: "lax" ,
maxAge: 60 * 60 * 24 * 30 , // 30 days
});
}
return response ;
} catch ( error ) {
return NextResponse . json (
{ error: "Token exchange failed" },
{ status: 500 }
);
}
}
import express from "express" ;
import { exchangeCodeServer } from "@ave-id/sdk/server" ;
const app = express ();
app . get ( "/auth/callback" , async ( req , res ) => {
const { code } = req . query ;
if ( ! code || typeof code !== "string" ) {
return res . status ( 400 ). send ( "Missing code" );
}
try {
const tokens = await exchangeCodeServer (
{
clientId: process . env . AVE_CLIENT_ID ! ,
clientSecret: process . env . AVE_CLIENT_SECRET ! ,
redirectUri: "http://localhost:3000/auth/callback" ,
},
{ code }
);
// Store in HTTP-only cookie
res . cookie ( "ave_access_token" , tokens . access_token , {
httpOnly: true ,
secure: process . env . NODE_ENV === "production" ,
maxAge: tokens . expires_in * 1000 ,
});
if ( tokens . refresh_token ) {
res . cookie ( "ave_refresh_token" , tokens . refresh_token , {
httpOnly: true ,
secure: process . env . NODE_ENV === "production" ,
maxAge: 30 * 24 * 60 * 60 * 1000 , // 30 days
});
}
res . redirect ( "/dashboard" );
} catch ( error ) {
res . status ( 500 ). send ( "Authentication failed" );
}
});
refreshTokenServer
Exchanges a refresh token for new access tokens using client credentials.
function refreshTokenServer (
config : ServerConfig ,
payload : { refreshToken : string }
) : Promise < TokenResponse >
Server configuration (same as exchangeCodeServer)
The refresh token from a previous token response
Returns: TokenResponse with fresh tokens
Example:
import { refreshTokenServer } from "@ave-id/sdk/server" ;
// Middleware to refresh expired tokens
app . use ( async ( req , res , next ) => {
const accessToken = req . cookies . ave_access_token ;
const refreshToken = req . cookies . ave_refresh_token ;
// Check if access token is expired (you should decode and check exp claim)
const isExpired = checkIfExpired ( accessToken );
if ( isExpired && refreshToken ) {
try {
const newTokens = await refreshTokenServer (
{
clientId: process . env . AVE_CLIENT_ID ! ,
clientSecret: process . env . AVE_CLIENT_SECRET ! ,
redirectUri: "http://localhost:3000/auth/callback" ,
},
{ refreshToken }
);
// Update cookies
res . cookie ( "ave_access_token" , newTokens . access_token , {
httpOnly: true ,
secure: process . env . NODE_ENV === "production" ,
maxAge: newTokens . expires_in * 1000 ,
});
} catch ( error ) {
// Refresh failed, redirect to login
return res . redirect ( "/login" );
}
}
next ();
});
exchangeDelegatedTokenServer
Exchanges a user’s access token for a delegated token to access third-party resources.
function exchangeDelegatedTokenServer (
config : ServerConfig ,
payload : {
subjectToken : string ;
requestedResource : string ;
requestedScope : string ;
actor ?: Record < string , unknown >;
}
) : Promise < DelegationTokenResponse >
User’s access token from Ave
Target resource identifier (e.g., github.com)
Space-separated scopes for the resource
Actor information for the delegation
Returns: DelegationTokenResponse
Delegated access token for the target resource
Token lifetime in seconds
Granted scopes for the resource
communication_mode
'user_present' | 'background'
Delegation communication mode
Example:
import { exchangeDelegatedTokenServer } from "@ave-id/sdk/server" ;
// API endpoint to get user's GitHub repos
app . get ( "/api/github/repos" , async ( req , res ) => {
const userToken = req . cookies . ave_access_token ;
try {
// Get delegated GitHub token
const delegation = await exchangeDelegatedTokenServer (
{
clientId: process . env . AVE_CLIENT_ID ! ,
clientSecret: process . env . AVE_CLIENT_SECRET ! ,
redirectUri: "http://localhost:3000/auth/callback" ,
},
{
subjectToken: userToken ,
requestedResource: "github.com" ,
requestedScope: "repo" ,
}
);
// Use delegated token to call GitHub API
const githubResponse = await fetch ( "https://api.github.com/user/repos" , {
headers: {
Authorization: `Bearer ${ delegation . access_token } ` ,
},
});
const repos = await githubResponse . json ();
res . json ( repos );
} catch ( error ) {
res . status ( 500 ). json ({ error: "Failed to fetch GitHub repos" });
}
});
Environment Variables
Store your Ave credentials in environment variables:
# .env
AVE_CLIENT_ID = your_client_id
AVE_CLIENT_SECRET = your_client_secret_here
NEXT_PUBLIC_APP_URL = https://yourapp.com
Never commit your .env file to version control. Add it to .gitignore.
Security Best Practices
Use HTTP-only, secure cookies to store tokens: res . cookie ( "ave_access_token" , tokens . access_token , {
httpOnly: true , // Prevent JavaScript access
secure: true , // HTTPS only
sameSite: "lax" , // CSRF protection
maxAge: tokens . expires_in * 1000 ,
});
Always validate that callback URLs match your configured redirect URIs: const allowedRedirects = [
"https://yourapp.com/auth/callback" ,
"https://staging.yourapp.com/auth/callback" ,
];
if ( ! allowedRedirects . includes ( redirectUri )) {
throw new Error ( "Invalid redirect URI" );
}
Use refresh tokens to rotate access tokens before they expire: // Check token expiry before API calls
if ( isTokenExpired ( accessToken ) && refreshToken ) {
const newTokens = await refreshTokenServer ( config , { refreshToken });
// Update stored tokens
}
Rate limit token exchanges
Implement rate limiting to prevent abuse: import rateLimit from "express-rate-limit" ;
const authLimiter = rateLimit ({
windowMs: 15 * 60 * 1000 , // 15 minutes
max: 5 , // 5 requests per window
});
app . get ( "/auth/callback" , authLimiter , async ( req , res ) => {
// Handle callback
});
See Also
OAuth Flow Learn about the OAuth protocol
Client Helpers Initiate login flows from the browser