Steam sign-in

Players choose Steam on the hosted login page, sign in on Steam's own page, and return signed in. There is no Steam application to register and no API key or client secret to store.

1. Turn it on in the console

On the project's sign-in methods page, under Steam:

  1. Tick Enable Steam sign-in.
  2. Select Save Steam settings.

Only project owners can change this setting; admins and viewers see it read-only. auth4.dev uses Steam's OpenID sign-in, so no credentials are needed. Your applications need no changes either.

Once Steam is on, players see it in three places:

  • Continue with Steam on the hosted login page, after Discord.
  • Link a Steam account on the page where guests save their progress.
  • Continue with Steam on the device approval page.

Untick the box and save to turn Steam off. The option disappears from all three pages.

What identity data you get

  • Steam tells auth4.dev only the player's SteamID64, a 17-digit number that never changes. It shares no email address, display name or avatar.
  • Because Steam shares no address, hosted login asks Steam players to verify an email address by default. Their tokens then carry email and email_verified.
  • Every token names the method in idp, which is steam for a Steam sign-in, and carries sub, the player's auth4.dev user ID.
  • The SteamID64 is stored as the player's Steam identity in your project. It isn't put in tokens. Key game data by sub, which stays the same whichever method the player used.
  • A Steam account and an email-code or Discord account are separate players. They are joined only when the project turns on account linking and a new Steam account verifies an existing player's address.
  • Steam's response stays on the server. Your application only receives auth4.dev tokens.

The verified email step

Under the default setting, Only when the sign-in method has no email, a Steam player who has no verified email address sees Add your email after Steam. They enter an address and a six-digit code, then return to your app signed in. The address is saved on their account, so later Steam sign-ins skip the step. Players who were signed up before the step existed are asked once, at their next sign-in.

  • The ID token and UserInfo then carry the address in lower case, with email_verified: true, when your app asks for the email scope. idp stayssteam.
  • If another player already uses the address, the page asks for a different one. A returning Steam player's account is never moved, so sub values never change.
  • Set Require a verified email to Off on the application's page to keep Steam players without an address. Their tokens then carry no email claim at all, never an empty one.

The verified email step guide covers the settings and every linking case.

Some players may still have no address, for example when the step is off or a guest saved their progress with Steam. The browser example falls back to the ID:

Excerpt from examples/browser/src/main.ts
const user = await auth4.getUser();
status.textContent = "You are signed in.";
profile.textContent = user.email ?? user.sub;

2. Add a Steam button (optional)

Without changes, players pick Steam from the hosted login page's list. To offer your own Sign in with Steam button, pass a provider to login(). Hosted login then goes straight to Steam. If the application has Steam turned off, the player sees the usual list instead.

Excerpt from examples/browser/src/main.ts
// With a provider, hosted login goes straight to it (if the application has it turned on).
function signInWith(provider?: SocialProvider): void {
  void auth4.login(provider ? { provider } : {}).catch((error: unknown) => {
    status.textContent =
      error instanceof Auth4BrowserError ? error.message : "Unable to start sign-in.";
  });
}

signIn.addEventListener("click", () => signInWith());
signInWithSteam.addEventListener("click", () => signInWith("steam"));

The provider is only a display choice. It grants nothing, and the full example is examples/browser/src/main.ts.

Linking Steam to a guest

A guest can save their progress by linking a Steam account. They keep the same user ID and stop being a guest. Pass provider: "steam" to upgradeGuest() and the upgrade window shows Link a Steam account first. The player can still choose another method. Call it directly in the click handler so the window isn't blocked:

Excerpt from examples/browser/src/main.ts
function saveGuestProgress(provider?: SocialProvider): void {
  auth4.upgradeGuest(provider ? { provider } : {}).then(
    () => void showSignedIn().catch(() => showSignedOut()),
    (error: unknown) => {
      if (!(error instanceof Auth4BrowserError)) throw error;
      if (error.code === "identity_in_use") {
        status.textContent = "That account already exists. Sign in to it, or link another one.";
      } else if (error.code === "invalid_session") {
        showSignedOut("Your guest session ended. Play as a guest again or sign in.");
      } else if (error.code === "popup_blocked") {
        status.textContent = "Allow pop-ups for this page, then try again.";
      }
      // cancelled and window_closed leave the guest session unchanged.
    },
  );
}

saveProgress.addEventListener("click", () => saveGuestProgress());
saveProgressWithSteam.addEventListener("click", () => saveGuestProgress("steam"));

In Unity, pass Auth4Provider.Steam to UpgradeGuestAsync. This works in the system browser on native platforms and in the popup window in WebGL builds:

Excerpt from packages/sdk-unity/Samples~/DeviceLogin/DeviceLoginSample.cs
var result = provider.HasValue
    ? await _client.UpgradeGuestAsync(provider.Value, onProgress, _cancellation.Token)
    : await _client.UpgradeGuestAsync(onProgress, _cancellation.Token);
  • Only a Steam account that is new to your project moves to the guest. If the SteamID64 already belongs to another player, the upgrade fails with identity_in_use and the guest stays as it was. See When the identity is already in use.
  • Accounts are never merged.

Letting Steam players add an email address

A signed-in Steam player who has no address, for example because the step is off or they were a guest, can add one to the same account and verify it with an email code. They keep the same user ID, can then sign in with either Steam or the email address, and their new tokens carry the email claim. Email sign-in must be turned on for the project.

In the browser, call addEmail() directly in a click handler. It opens the hosted /login/add-email page in a window, waits while the player enters the code, and then replaces the session with a refreshed one:

addEmailButton.addEventListener("click", async () => {
  try {
    const { email } = await auth4.addEmail();
    status.textContent = `Added ${email}.`;
  } catch (error) {
    // "cancelled" and "window_closed" mean the player stopped; nothing changed.
    status.textContent = "Your email address was not added.";
  }
});

In Unity, call AddEmailAsync. It opens the same page in the system browser with the BrowserLauncher and polls until the player finishes:

try
{
    var result = await _client.AddEmailAsync();
    _status = "Added " + result.Email;
}
catch (Auth4AddEmailException error) when (error.Reason == AddEmailFailure.Denied)
{
    _status = "No email address was added.";
}
  • An address that already belongs to another player in your project is refused. The page says only that the address can't be added, so it doesn't reveal who uses it, and the player can try another.
  • Guests save their progress with the guest upgrade instead (guest_account). An account that already has an email address gets email_already_added.
  • Each change is recorded in the audit log as user.email_added. The address itself isn't stored in the audit entry.

The endpoints are described in the API reference.

Changing or removing the address

A player can also change the address or remove it. In the browser, callchangeEmail() or removeEmail()directly in a click handler, as with adding one. They open the hosted/login/change-email or /login/remove-email page and then replace the session with a refreshed one for the same user:

changeEmailButton.addEventListener("click", async () => {
  try {
    const { email } = await auth4.changeEmail();
    status.textContent = `You now sign in with ${email}.`;
  } catch (error) {
    // "cancelled" and "window_closed" mean the player stopped; nothing changed.
    status.textContent = "Your email address was not changed.";
  }
});

removeEmailButton.addEventListener("click", async () => {
  try {
    const { session } = await auth4.removeEmail();
    // session.user has no email now.
    status.textContent = "Your email address was removed.";
  } catch (error) {
    status.textContent =
      error.code === "only_sign_in_method"
        ? "Link another account before you remove your email address."
        : "Your email address was not removed.";
  }
});

In Unity, call ChangeEmailAsync or RemoveEmailAsync. They open the same pages with the BrowserLauncher and poll likeAddEmailAsync:

try
{
    var result = await _client.ChangeEmailAsync();
    _status = "You now sign in with " + result.Email;
}
catch (Auth4AddEmailException error) when (error.Reason == AddEmailFailure.Denied)
{
    _status = "Your email address was not changed.";
}

try
{
    await _client.RemoveEmailAsync();
    _status = "Your email address was removed.";
}
catch (Auth4AddEmailException error) when (error.Reason == AddEmailFailure.OnlySignInMethod)
{
    _status = "Link another account before you remove your email address.";
}
  • A change sends a code to the new address. After the player confirms it, the new address replaces the old one on the same account. A password the player set keeps working with the new address. An address that belongs to another player is refused in the same way as when adding one.
  • A removal needs only the player's confirmation. It is refused withonly_sign_in_method while the address is the only way to sign in, for example when the player has no Steam account, other linked account or passkey. A password set for the address is removed with it.
  • An account without an email address gets email_not_added. Changes are recorded asuser.email_changed and user.email_removed.
  • Adding, changing or removing an address needs a sign-in from the last five minutes. An older session is refused with login_required, which Unity reports asAddEmailFailure.LoginRequired. Sign the player in again, then retry.
  • After a change or removal, the old address gets a notice and the player is signed out on every other device. The page shows which account it changes before the player confirms.

See the API reference for the details.

Device approval

When Steam is on, the device approval page asks the player how to sign in and offers Continue with Steam. After Steam, the player comes back to the same code, takes the verified email step if it applies, and then approves or denies it. The code isn't put in any link along the way. Your game calls SignInWithDeviceAsync as usual; nothing changes on the game's side.

When something goes wrong

  • If the player cancels on Steam, the page says that Steam sign-in was cancelled and lets them choose another method. It doesn't start Steam again by itself, even when your app asked for Steam.
  • If Steam is unreachable or doesn't confirm the sign-in in time, the page reports a temporary problem and offers a retry.
  • If Steam's answer can't be verified, or is replayed, the sign-in is refused. auth4.dev checks every Steam response with Steam before trusting it.
  • If the Steam option doesn't appear, check that Enable Steam sign-in is ticked and saved.

Testing locally

Your app can run on localhost, but Steam sign-in itself always happens on the hosted pages and on Steam, so you need a real Steam account. A free account is enough.

  1. Use a separate project for testing, so test players never mix with real ones. Register your local redirect URI and origin as described in Run the example.
  2. Turn on Steam for that project, then sign in with Steam from your local app.
  3. To test a guest upgrade, start a guest and link a Steam account that hasn't signed in to the project yet. Linking the same account from a second guest shows the identity_in_use path.

If you run the auth4.dev API itself with Wrangler, you can point Steam at a stand-in on your own machine instead. Set this in the ignored apps/api/.dev.vars:

Excerpt from apps/api/.dev.vars.example
# Steam sign-in (AUTH4D-123) needs no credentials. Optional, local runs only: a loopback
# stand-in for steamcommunity.com/openid/login.
# STEAM_LOCAL_MOCK_ORIGIN=http://127.0.0.1:8790

The API honours it only in the local environment, and only for a loopback address. The repository's automated tests answer Steam with a stub and never contact Steam.