- What do you want to achieve?
I’m attempting to program ROBLOX’s OAuth 2.0 authentication into my website which will be used for administrative tasks related to a group. - What is the issue?
“token exchange failed: 401” error is being received when attempting to perform token exchange - however this only seems to occur in prod, not dev? See attachments. - What solutions have you tried so far?
I’ve had a look around, and couldn’t seem to find any solution. The API docs aren’t too great.
Hey all. I’ve been working on a web app for an administration system which I’ll end up using for my group as of late. I’ve run into an issue when attempting to set up OAuth 2.0 with the ROBLOX API.
When running in a dev environment on localhost, token exchange is performed fine and I can get user information from the API endpoints (see below).
However, when deployed to a prod environment on Vercel, I run into the following error:
{
"error": "Token exchange failed: 401 - {\"error\":\"invalid_grant\",\"error_description\":\"Authorization code may not be used from this device\"}"
}
I had a look into it, and the fact that I set cookies should mitigate this, yet it doesn’t seem to have an effect. Does anyone know what’s going on? I’ll include some code snippets from my API endpoints.
/api/oauth/initiate
export async function GET() {
const { codeVerifier, codeChallenge } = generateCodeVerifierAndChallenge();
const state = crypto.randomBytes(16).toString('hex');
const CLIENT_ID = process.env.CLIENT_ID!;
const REDIRECT_URI = process.env.REDIRECT_URI!;
const SCOPE = 'openid profile';
const authUrl = buildAuthorizationUrl({
clientId: CLIENT_ID,
redirectUri: REDIRECT_URI,
scope: SCOPE,
codeChallenge,
state,
});
const response = NextResponse.json({ authUrl });
response.cookies.set('oauth_state', state);
response.cookies.set('oauth_verifier', codeVerifier);
return response;
}
/api/oauth/roblox-callback (this is my callback from the OAuth endpoints)
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const code = searchParams.get('code');
const state = searchParams.get('state');
const cookieState = req.cookies.get('oauth_state')?.value;
const codeVerifier = req.cookies.get('oauth_verifier')?.value;
if (!code || !state || state !== cookieState || !codeVerifier) {
return NextResponse.json({
error: 'Invalid state or missing data',
}, { status: 400 });
}
try {
const CLIENT_ID = process.env.CLIENT_ID!;
const REDIRECT_URI = process.env.REDIRECT_URI!;
const tokens = await exchangeCodeForToken({
clientId: CLIENT_ID,
redirectUri: REDIRECT_URI,
code,
codeVerifier,
});
const userInfo = await getUserInfo(tokens.access_token);
const response = NextResponse.json({ tokens, userInfo });
response.cookies.delete('oauth_state');
response.cookies.delete('oauth_verifier');
return response;
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
Here’s a screenshot of the full error for reference.

