Skip to content

Instantly share code, notes, and snippets.

@Whats-A-MattR
Last active November 5, 2025 11:04
Show Gist options
  • Save Whats-A-MattR/5bce5574e568e8d8e6be55cf692df3a1 to your computer and use it in GitHub Desktop.
Save Whats-A-MattR/5bce5574e568e8d8e6be55cf692df3a1 to your computer and use it in GitHub Desktop.
Steam Authentication in Better-Auth

Steam Authetication in Better-Auth

Adapted by Better-Auth in a PR

Better-Auth currently does not have a Steam provider. If you've used Steam authentication before, you may understand why. It doesn't exactly comply with OAuth or OIDC standards. For example

  • Steam Authentiation does not respect state.
  • Steam does not require a Client ID or Client Secret, or even require an Application registration with redirect whitelisting.
  • Steam does not have an OIDC endpoint, you must use your own API Key to make requests to the ISteamUser/GetPlayerSummaries/v002/ api once recieving the callback from steam.
  • Steam does not return an email address, as we retrieve the user profile from a 'public' endpoint - as to say we only have access to what would otherwise be visible from the users steamcommunity/profile page.

Because of this non-compliance, making it work wiht a spec-compliant adapter is a non starter.

The implementation

Due to not complying with OAuth2 or OIDC, we need to instead create a plugin.

Gotcha

An important detail is that we use the steamid and a placeholder to populate the required email field.

import type { steamAuthPlugin } from './index';
import type { BetterAuthClientPlugin } from 'better-auth';
export const steamAuthClient = () => {
return {
id: 'steam-auth-client',
$InferServerPlugin: {} as ReturnType<typeof steamAuthPlugin>,
} satisfies BetterAuthClientPlugin;
};
import { createAuthEndpoint } from 'better-auth/api';
import { type BetterAuthPlugin, type User } from 'better-auth';
import { setSessionCookie } from 'better-auth/cookies';
import { z } from 'zod';
const STEAM_BASE_URL = 'https://api.steampowered.com/';
export interface SteamAuthPluginOptions {
steamApiKey: string;
}
export const steamAuthPlugin = (
cfg: SteamAuthPluginOptions,
): BetterAuthPlugin => ({
id: 'steamAuthPlugin',
endpoints: {
signInWithSteam: createAuthEndpoint(
'/sign-in/steam',
{
method: 'POST',
body: z.object({
callbackURL: z.string().optional(),
errorCallbackURL: z.string().optional(),
newUserCallbackURL: z.string().optional(),
disableRedirect: z.boolean().optional(),
}),
},
async (ctx) => {
const returnUrl = `${ctx.context.baseURL}/steam/callback`;
const openidURL =
`https://steamcommunity.com/openid/login?` +
`openid.ns=${encodeURIComponent('http://specs.openid.net/auth/2.0')}&` +
`openid.mode=checkid_setup&` +
`openid.return_to=${encodeURIComponent(returnUrl)}&` +
`openid.realm=${encodeURIComponent(ctx.context.baseURL)}&` +
`openid.identity=${encodeURIComponent('http://specs.openid.net/auth/2.0/identifier_select')}&` +
`openid.claimed_id=${encodeURIComponent('http://specs.openid.net/auth/2.0/identifier_select')}&`;
return new Response(
JSON.stringify({
url: openidURL,
redirect: !ctx.body.disableRedirect,
}),
{
headers: { 'Content-Type': 'application/json' },
},
);
},
),
steamCallback: createAuthEndpoint(
'/steam/callback',
{ method: 'GET' },
async (ctx) => {
if (!ctx?.request?.url) {
return Response.redirect(
`${`${ctx.context.baseURL}/error`}?error=missing_request_url`,
);
}
const callbackUrl = new URL(ctx?.request?.url);
const params = Object.fromEntries(callbackUrl.searchParams.entries());
const verifyRes = await fetch(
'https://steamcommunity.com/openid/login',
{
method: 'POST',
body: new URLSearchParams({
...Object.fromEntries(callbackUrl.searchParams.entries()),
'openid.mode': 'check_authentication',
}),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
const verifyText = await verifyRes.text();
if (!verifyText.includes('is_valid:true')) {
return Response.redirect(
`${`${ctx.context.baseURL}/error`}?error=steam_openid_validation_failed`,
);
}
const steamid = params['openid.claimed_id']?.split('/').pop();
if (!steamid) {
return Response.redirect(
`${`${ctx.context.baseURL}/error`}?error=steamid_missing`,
);
}
const profileUrl = new URL(
`ISteamUser/GetPlayerSummaries/v0002/?key=${steamApiKey}&steamids=${steamid}`
const profileRes = await fetch(profileUr.toString(), STEAM_BASE_URL)
if (!profileRes.ok) {
return Response.redirect(
`${`${ctx.context.baseURL}/error`}?error=steam_profile_fetch_failed`,
);
}
const profile = await profileRes.json()
let account = await ctx.context.internalAdapter.findAccount(steamid);
let user: User | null = null;
if (!account) {
console.log('No account found for Steam ID, creating new account');
user = await ctx.context.internalAdapter.createUser({
name: profile.personaname || 'Unknown',
email: `${steamid}@placeholder.com`,
emailVerified: false,
image: profile.avatarfull || '',
});
account = await ctx.context.internalAdapter.createAccount({
accountId: steamid,
providerId: 'steam',
userId: user.id,
});
} else {
user = await ctx.context.internalAdapter.findUserById(account.userId);
}
if (!user || !account) {
return Response.redirect(
`${`${ctx.context.baseURL}/error`}?error=user_or_account_not_found`,
);
}
const session = await ctx.context.internalAdapter.createSession(
user.id,
ctx.request,
);
await setSessionCookie(ctx, {
session,
user,
});
const hostname = new URL(ctx.context.baseURL);
const url = new URL('/dashboard', hostname);
console.log('Redirecting to:', url.toString());
throw ctx.redirect(url.toString());
},
),
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment