When following the guide, select the application with these scopes: read:users
, read:user_idp_tokens
update:users update:users_app_metadata
Don't expose this token to the client-side of your application!
Change all values to your own app's values.
Copy ARES_CLIENT_ID="YOUR_ARES_CLIENT_ID"
ARES_CLIENT_SECRET="YOUR_ARES_CLIENT_SECRET"
ARES_REDIRECT_URI="https://dev-a3r31fmnmhcsjwav.us.auth0.com/login/callback" # Example
AUTH0_APP_URL='https://dev-a3r31fmnmhcsjwav.us.auth0.com' # Example only
AUTH0_CLIENT_ID='YOUR_AUTH0_CLIENT_ID'
AUTH0_CLIENT_SECRET='YOUR_AUTH0_CLIENT_SECRET'
# ... pre-existing environment variables ...
Always keep this route server-side only.
Copy var axios = require('axios').default;
import { getSession } from '@auth0/nextjs-auth0';
import { NextResponse } from 'next/server';
// Get the ARES access token
async function getAresAccessToken(userId) {
try {
// Get Auth0 Management API access token
const tokenResponse = await axios.request({
method: 'POST',
url: `${process.env.AUTH0_APP_URL}/oauth/token`,
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.AUTH0_CLIENT_ID,
client_secret: process.env.AUTH0_CLIENT_SECRET,
audience: `${process.env.AUTH0_APP_URL}/api/v2/`
})
});
// Get user data using the access token
const userData = await axios.request({
method: 'GET',
url: `${process.env.AUTH0_APP_URL}/api/v2/users/${userId}`,
headers: { authorization: `Bearer ${tokenResponse.data.access_token}` }
});
// Always check for fresh tokens in identities after a re-login
const aresIdentity = userData.data.identities.find(identity => identity.connection === 'ARES');
const aresTokensFromMetadata = userData.data.app_metadata?.ares_tokens;
// Check if the identity has tokens (either first login or re-login)
if (aresIdentity?.access_token) {
// If there are no tokens in metadata or the access token in identity is different
// from the one in metadata, update the metadata with the new tokens
if (!aresTokensFromMetadata || aresIdentity.access_token !== aresTokensFromMetadata.access_token) {
console.log('Updating ARES tokens in metadata after sign-in');
// Store tokens in app_metadata because the tokens in identities can't be updated
await axios.request({
method: 'PATCH',
url: `${process.env.AUTH0_APP_URL}/api/v2/users/${userId}`,
headers: {
Authorization: `Bearer ${tokenResponse.data.access_token}`,
'Content-Type': 'application/json'
},
data: {
app_metadata: {
ares_tokens: {
access_token: aresIdentity.access_token,
refresh_token: aresIdentity.refresh_token
}
}
}
});
return {
access_token: aresIdentity.access_token,
refresh_token: aresIdentity.refresh_token
};
}
}
return aresTokensFromMetadata || {};
} catch (error) {
console.error('Error getting ARES access token:', error);
throw error;
}
}
// Refresh the ARES access token
async function refreshAresToken(refreshToken, userId) {
try {
const response = await fetch('https://oauth.joinares.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
client_id: process.env.ARES_CLIENT_ID,
client_secret: process.env.ARES_CLIENT_SECRET,
grant_type: 'refresh_token',
refresh_token: refreshToken,
redirect_uri: process.env.ARES_REDIRECT_URI
})
});
const newTokens = await response.json();
// Check if the refresh token request returned an error
if (newTokens.error) {
console.error('Refresh token error:', newTokens.error, newTokens.error_description);
throw new Error(`ARES refresh token error: ${newTokens.error_description || newTokens.error}`);
}
// Get Auth0 Management API access token to update metadata
const tokenResponse = await axios.request({
method: 'POST',
url: `${process.env.AUTH0_APP_URL}/oauth/token`,
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.AUTH0_CLIENT_ID,
client_secret: process.env.AUTH0_CLIENT_SECRET,
audience: `${process.env.AUTH0_APP_URL}/api/v2/`
})
});
// Update tokens in app_metadata
await axios.request({
method: 'PATCH',
url: `${process.env.AUTH0_APP_URL}/api/v2/users/${userId}`,
headers: {
Authorization: `Bearer ${tokenResponse.data.access_token}`,
'Content-Type': 'application/json'
},
data: {
app_metadata: {
ares_tokens: {
access_token: newTokens.access_token,
refresh_token: newTokens.refresh_token
}
}
}
});
return newTokens;
} catch (error) {
console.error('Error refreshing ARES token:', error);
// Suggest the user to re-authenticate
throw new Error(
`Failed to refresh ARES token. Please try signing out and signing in again. Error: ${error.message}`
);
}
}
// Fetch the ARES user data
async function fetchARESUserData(accessToken, refreshToken, userId) {
try {
const ARESUrl = 'https://oauth.joinares.com/v1/user';
let response = await fetch(ARESUrl, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
if (response.status === 403 || response.status === 401) {
// Token expired, refresh and retry
try {
const newTokens = await refreshAresToken(refreshToken, userId);
response = await fetch(ARESUrl, {
headers: {
Authorization: `Bearer ${newTokens.access_token}`
}
});
} catch (refreshError) {
console.error('Error refreshing token during API call:', refreshError);
throw refreshError; // Let the main API handler deal with this error
}
}
if (!response.ok) {
throw new Error(`ARES API error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching ARES user data:', error);
throw error;
}
}
export async function GET(request) {
try {
const session = await getSession(request);
const userId = session?.user?.sub;
if (!userId) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const aresTokens = await getAresAccessToken(userId);
if (!aresTokens.access_token) {
return NextResponse.json(
{
error: 'ARES access token not found',
requiresReauth: true
},
{ status: 401 }
);
}
try {
const aresData = await fetchARESUserData(aresTokens.access_token, aresTokens.refresh_token, userId);
return NextResponse.json(aresData);
} catch (error) {
// Special handling for token errors
if (
error.message.includes('ARES refresh token error') ||
error.message.includes('Failed to refresh ARES token')
) {
return NextResponse.json(
{
error: error.message,
requiresReauth: true
},
{ status: 401 }
);
}
throw error; // Re-throw other errors to be caught by outer catch
}
} catch (error) {
console.error('Error in GET /api/ARES:', error);
return NextResponse.json({ error: error.message }, { status: error.response?.status || 500 });
}
}
export async function POST(request) {
try {
const session = await getSession(request);
const userId = session?.user?.sub;
if (!userId) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const aresTokens = await getAresAccessToken(userId);
if (!aresTokens.access_token) {
return NextResponse.json(
{
error: 'ARES access token not found',
requiresReauth: true
},
{ status: 401 }
);
}
// Parse request body
const { usage, credits } = await request.json();
try {
const UsageUrl = 'https://oauth.joinares.com/v1/partner/usage';
let ARESResponse = await fetch(UsageUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${aresTokens.access_token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_id: process.env.ARES_CLIENT_ID,
usage,
credits
})
});
if (ARESResponse.status === 403 || ARESResponse.status === 401) {
// Token expired, refresh and retry
try {
const newTokens = await refreshAresToken(aresTokens.refresh_token, userId);
ARESResponse = await fetch(UsageUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${newTokens.access_token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_id: process.env.ARES_CLIENT_ID,
usage,
credits
})
});
} catch (refreshError) {
// Special handling for token errors
if (
refreshError.message.includes('ARES refresh token error') ||
refreshError.message.includes('Failed to refresh ARES token')
) {
return NextResponse.json(
{
error: refreshError.message,
requiresReauth: true
},
{ status: 401 }
);
}
throw refreshError;
}
}
if (!ARESResponse.ok) {
return NextResponse.json({ error: `ARES API error: ${ARESResponse.status}` }, { status: ARESResponse.status });
}
const ARESData = await ARESResponse.json();
return NextResponse.json(ARESData);
} catch (error) {
// Special handling for token errors
if (
error.message.includes('ARES refresh token error') ||
error.message.includes('Failed to refresh ARES token')
) {
return NextResponse.json(
{
error: error.message,
requiresReauth: true
},
{ status: 401 }
);
}
throw error;
}
} catch (error) {
console.error('Error in POST /api/ARES:', error);
return NextResponse.json({ error: error.message }, { status: error.response?.status || 500 });
}
}
With a user signed in with ARES, you can call the API endpoint you just configured to access the user's data from ARES's server.
Copy const [userData, setUserData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchAresUser = async () => {
try {
const response = await fetch('/api/ARES');
if (!response.ok) {
throw new Error('Failed to fetch ARES user data');
}
const data = await response.json();
setUserData(data.user);
} catch (err) {
setError(err.message);
console.error('Error fetching ARES user data:', err);
}
};
fetchAresUser();
}, []);
A GET request to your /api/ARES endpoint should return the user's information in json format, including the number of credits the user has left.
Copy {
message: 'Success',
user: {
id: 'f7133790-9d1a-4afc-a510-f069675d4b15',
email: 'example@gmail.com',
name: '',
credits: 22
}
}
IMPORTANT: If you check whether or not a user has a subscription before allowing them to use a certain feature on your app, make sure to disable that for users signed in with ARES in order for them to be able to pay-per-use with their ARES credits without having a subscription to your service.
Only call the API for users who signed in with ARES, otherwise the API will return an error.
Here's an example of a React button that earns 1 ARES credit for your app (and deducts the credit from the user's account).
Copy "use client";
import React from "react";
const handleAresUsageUpdate = async () => {
try {
// Make sure to configure this API route first (at the beginning of this page).
const response = await fetch("/api/ARES", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
usage: "test-button", // Replace with actual usage reason. Must be consistent across usages. (e.g. every time the user uses the test button, use value "test-button", every time the user uses a different button, use value "different-button".
credits: 1, // Replace with actual credits value
}),
});
if (!response.ok) {
throw new Error("Failed to update ARES usage");
}
const data = await response.json();
} catch (error) {
}
};
const UsageButton = () => {
return (
<button
onClick={handleAresUsageUpdate}
>
Update Usage
</button>
);
};
export default UsageButton;