OathWall

Godot OAuth Login: Add Social Sign-In With Oathwall

The Oathwall Team5 min read

There's no Oathwall SDK for Godot. What there is instead: a login flow that's just HTTP, and a Godot node — HTTPRequest — built to speak it. If you can open a URL and parse JSON, you can add "Sign in with Google" (or Discord, or Steam) to a Godot 4 game without a backend, without a client secret in your export, and without writing an OAuth implementation.

You'll do a little more by hand than a Unity dev, almost all of it around the deep link that carries the result home. This guide is the full wiring in GDScript, and it's honest about the rough edge.

The flow, in Godot terms

Four steps, mapped to nodes you already have:

  1. Open the browser with OS.shell_open() — sends the player to the hosted sign-in page.
  2. Catch the ticket when the OS hands your game a mygame://auth?ticket=… URL after login.
  3. Exchange the ticket for tokens with an HTTPRequest.
  4. Read the player from /auth/me, then store the refresh token for next launch.

The confidential OAuth exchange runs on Oathwall's servers, so none of it lives in your project. If you want the protocol-level version of this contract, OAuth login without an SDK lays out every endpoint; here we make it concrete.

1. Open the sign-in page

The provider is part of the URL, and your public appKey names the app:

extends Node

const SSO_BASE := "https://sso.oathwall.com"
const APP_KEY := "your_public_app_key"

func start_login(provider: String) -> void:
    # Opens the hosted sign-in page in the player's default browser.
    var url := "%s/auth/%s/start?appKey=%s" % [SSO_BASE, provider, APP_KEY]
    OS.shell_open(url)

start_login("google"), start_login("discord"), start_login("steam") — the string is the only thing that changes. Open the system browser, not an in-app one: Google and others reject embedded webviews for OAuth.

2. Catch the ticket

This is the part Godot doesn't do for you. After the player approves, Oathwall redirects the browser to the callback you registered in the dashboard — use a custom scheme like mygame://auth. The browser then asks the OS to open that scheme, and the OS launches (or focuses) your game with the URL attached.

On desktop, that URL arrives as a command-line argument:

func _ready() -> void:
    # When the OS launches the game via mygame://auth?ticket=XYZ,
    # the full URL comes in as a launch argument.
    for arg in OS.get_cmdline_args():
        if arg.begins_with("mygame://"):
            _handle_callback(arg)

func _handle_callback(url: String) -> void:
    var query := url.get_slice("?", 1)      # ticket=XYZ&...
    for pair in query.split("&"):
        var kv := pair.split("=")
        if kv.size() == 2 and kv[0] == "ticket":
            consume_ticket(kv[1].uri_decode())

Two honest caveats, because this is where Godot integrations trip:

  • Register the scheme with the OS. A mygame:// link does nothing until the operating system knows your game handles it — a registry entry on Windows, a CFBundleURLTypes entry on macOS, a .desktop MimeType on Linux. Your installer sets this up. This is exactly the plumbing a Unity SDK hides.
  • Handle the second launch. If the game is already running when the scheme fires, the OS may start a second instance instead of focusing the first. Godot has no built-in single-instance guard, so you'll want a lock file or a small local socket so the running game picks up the ticket and the new process exits. Skip this and a player who's already in-game gets a duplicate window.

On mobile, you register the scheme in the Android/iOS export settings (intent filter / URL type) and read it through the export's deep-link handling rather than the command line. For a WebGL build there's no scheme at all — set an https:// callback and read the ticket from window.location.search. The mechanics per platform are the same ones described in deep linking for games.

3. Exchange the ticket for tokens

Now it's plain HTTP. HTTPRequest is a node, so add it to the tree, fire the request, and await its signal:

func consume_ticket(ticket: String) -> void:
    var http := HTTPRequest.new()
    add_child(http)

    var headers := PackedStringArray(["Content-Type: application/json"])
    var payload := JSON.stringify({ "ticket": ticket })
    http.request("%s/auth/consume" % SSO_BASE, headers,
        HTTPClient.METHOD_POST, payload)

    var response: Array = await http.request_completed
    http.queue_free()

    # response = [result, response_code, headers, body]
    if response[1] != 200:
        push_error("Login failed: HTTP %d" % response[1])
        return

    var body: PackedByteArray = response[3]
    var tokens: Dictionary = JSON.parse_string(body.get_string_from_utf8())
    # tokens = { "access_token": "...", "refresh_token": "...", "expires_in": 3600 }

    save_refresh_token(tokens["refresh_token"])
    fetch_profile(tokens["access_token"])

The ticket is single-use and expires in five minutes, so exchange it as soon as you catch it. You get back a JWT access_token (good for an hour) and an opaque refresh_token (good for 30 days).

4. Read the player and stay signed in

Send the access token as a bearer header to learn who the player is:

func fetch_profile(access_token: String) -> void:
    var http := HTTPRequest.new()
    add_child(http)

    var headers := PackedStringArray(["Authorization: Bearer " + access_token])
    http.request("%s/auth/me" % SSO_BASE, headers, HTTPClient.METHOD_GET)

    var response: Array = await http.request_completed
    http.queue_free()

    var user: Dictionary = JSON.parse_string(
        (response[3] as PackedByteArray).get_string_from_utf8())
    # user = { "id", "email", "name", "picture", "provider", "linkedProviders" }
    print("Signed in: %s" % user["name"])

Use user["id"] as your stable player key — it's the value that survives an email change and stays constant across linked providers. Don't assume email or picture is present; Steam never sends an email, and plenty of players hide theirs.

For the return trip, store the refresh token where a curious player can't read it. Godot's user:// directory with an encrypted file works:

const TOKEN_PATH := "user://oathwall.session"

func save_refresh_token(token: String) -> void:
    var f := FileAccess.open_encrypted_with_pass(
        TOKEN_PATH, FileAccess.WRITE, OS.get_unique_id())
    f.store_line(token)
    f.close()

On the next launch, load that token and call POST /auth/refresh before showing the login screen — a returning player never sees the browser. Remember that refresh rotates: the response carries a new refresh token, and the old one stops working, so overwrite your saved copy each time. Why not plain ConfigFile or a prefs-style store? The same reason it's a bad idea in any engine — a refresh token is a live credential, not a setting.

The honest summary

A Unity dev gets this as a package: drop in the SDK, call one method, done. On Godot you write maybe a hundred and fifty lines — the browser open, the deep-link catch, three HTTP calls, and the token store — and the deep-link registration is genuinely the annoying part. But the hard, security-sensitive half of OAuth still isn't yours: the client secret and the token exchange live on Oathwall's servers, so your Godot export ships nothing but a public key.

The Oathwall service is engine-independent; the packaged SDK (on the Unity Asset Store) is the Unity-only convenience layer. The free plan — one app, ten users — is enough to build and test the whole flow above.

Next: the endpoint-by-endpoint reference for any engine, authentication without a backend for why there's no server to run, or deep linking for the return trip that Godot leaves to you.

Frequently asked questions

Is there an Oathwall SDK for Godot?
Not today — the packaged SDK is Unity-only. But Oathwall's login is a plain HTTP flow, and Godot's HTTPRequest node plus OS.shell_open cover everything you need. This guide wires it up in GDScript. You do more by hand than a Unity dev does, mostly around the deep link, but the service itself works the same from any engine.
Which providers work with a Godot game?
All of them — the provider is just a string in the sign-in URL. Google, Apple, Steam, Discord, GitHub, Epic, Twitch, X, Microsoft, and Facebook all work the same way from Godot, because the OAuth exchange happens on Oathwall's servers, not in your game. You enable the ones you want in the dashboard.
Do I need a client secret in my Godot project?
No. That's the point of using a hosted service. The client secret stays on Oathwall's servers; your Godot build only carries the public appKey. Nothing sensitive ships in the export, so there's nothing extractable to leak.
How does the login result get back into a Godot game?
Through a deep link. Oathwall sends the browser to a URL you register — a custom scheme like mygame://auth?ticket=. On desktop you register that scheme with the OS and read it from OS.get_cmdline_args() on launch; on mobile you handle it in the export's URL-scheme setup. It's the one part Godot doesn't hand you for free, and this guide covers it.