Unity WebGL custom authentification won't work

Hi.
I’m experiencing an issue with my game made on Unity, specifically - the WebGL build.
I’ve tried everything by now, and got no idea how to troubleshoot.

So, the server side is in typescript, and the sdk is Unity. Android build works perfectly, connecting to a web socket without a hassle.
I do connect to the server through a domain name, rather than IP, and my server is behind reverse proxy (nginx) which routes https requests (443 port) to 7050. So generally, the game connects to https://mygame.com/v2/account/authenticate/custom?create=true& and nginx forwards it to http://172.17.0.1:7350/v2/account/authenticate/custom?create=true&

the code used from the documentation:

client = new Nakama.Client(Scheme, Host, Port, ServerKey, UnityWebRequestAdapter.Instance);

session = await client.AuthenticateCustomAsync(customId);

The documentation describes webgl support for sockets (grpc connection), but there’s nothing about authorization in the documentation. So, I suppose, It has to work from the box, right?

My issue is, that once the WebGL build loads up, it performs a preflight OPTIONS request to the server, receives 200 empty response, and nothing goes next.

Even though i set logging to debug - there’s nothing in nakama’s server log.

how my request looks in browser:

curl -I 'https://game.com/v2/account/authenticate/custom?create=true&' \
  -X 'OPTIONS' \
  -H 'accept: */*' \
  -H 'accept-language: en-US,en;q=0.9,ru;q=0.8' \
  -H 'access-control-request-headers: authorization,content-type' \
  -H 'access-control-request-method: POST' \
  -H 'sec-fetch-dest: empty' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-site: same-site'

here’s nakama’s response

HTTP/2 200
date: Mon, 19 May 2025 09:40:26 GMT
content-length: 0
access-control-allow-headers: Authorization,Content-Type
access-control-allow-origin: *

Seems like it doesn’t even get to the server’s lines


// Enable CORS on all requests.

 CORSHeaders := handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "User-Agent"})
 CORSOrigins := handlers.AllowedOrigins([]string{"*"})
 CORSMethods := handlers.AllowedMethods([]string{http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodDelete})

Could you advice any solution for making unity werbgl build able to authenticate? Thank you.

FIY the nakama settings are as defaults:

socket:
  server_key: defaultkey
  port: 7350
  address: ''
console:
  port: 7351
  address: ''

UPD:

I’ve just changed the code, so the socket connection runs before Authentification:

        try
        {
            client = new Nakama.Client(Scheme, Host, Port, ServerKey, UnityWebRequestAdapter.Instance);
        }
        catch (Exception e)
        {
            Debug.Log($"{e}");
        }

#if UNITY_WEBGL && !UNITY_EDITOR
        ISocketAdapter adapter = new JsWebSocketAdapter();
#else
        ISocketAdapter adapter = new WebSocketAdapter();
#endif

        try
        {
            socket = client.NewSocket(true, adapter);
        }
        catch (Exception e)
        {
            Debug.Log($"{e}");
        }

        Debug.Log($"customId : {customId}");
        try
        {
                client.Timeout = 10;
                session = await client.AuthenticateCustomAsync(customId);
        }
        catch (Exception e)
        {
                Debug.Log($"{e}");
        }

The Chrome’s console writes:

Socket(_baseUri=‘wss://mygame.com/’, _cid=0, IsConnected=False, IsConnecting=False)
customId : User-1

and the last network request from the page ended with

custom?create=true& 200 preflight Preflight 0 B 1.60 s

UPD2:

Tried the same on localhost (127.0.0.1 via http), the same result.
Can anyone point to how to perform authentification in Unity WebGL build, please?

Anyone has a chance to look into the issue?

@moteam the 200 should indicate that the OPTIONS request is successfully responded by the server, I’m not sure why the following socket connection is failing.

I’m also not sure why you’d see:

Socket(_baseUri=‘wss://mygame.com/’, _cid=0, IsConnected=False, IsConnecting=False)
customId : User-1

with your above snippet.

await _socket.ConnectAsync(session);

doesn’t actually connect to the socket, it only creates the socket client with the passed configs, to actually connect, you’d have to run

await _socket.ConnectAsync(session);

with the session you received from the authentication step.

Either way, the authentication goes through regular http request, can you check if client.AuthenticateCustomAsync(customId); is actually reaching the server?

In my experience its best to authenticate first by device ID first to stop duplicate logins from different devices to same account. Then use migration features to allow a secondary authentication. So that 1 device at any given moment is connected to 1 account.
Doing this would also be a great way to further diagnose your issue. I use both unity and Nakama and Facebook features on a project and I still authenticate with device. Here is an example I will try to use proper code block(new to forum):

Blockquote
using Nakama;
using System;
using System.Threading.Tasks;
using UnityEngine;

public class NakamaManager : MonoBehaviour
{
private IClient client;
private ISession session;

async void Start()
{
    // Initialize the Nakama client
    client = new Client("http", "127.0.0.1", 7350, "defaultkey");

    // Attempt to restore a saved session
    string savedToken = PlayerPrefs.GetString("NakamaSession", null);
    if (!string.IsNullOrEmpty(savedToken))
    {
        session = Session.Restore(savedToken);
        if (!session.IsExpired)
        {
            Debug.Log("Session restored successfully.");
            await GetAccountAsync();
            return;
        }
    }

    // Authenticate with device ID and migrate to custom ID
    await AuthenticateAndMigrateAsync();
}

private async Task AuthenticateAndMigrateAsync()
{
    try
    {
        // Step 1: Authenticate with device ID
        string deviceId = SystemInfo.deviceUniqueIdentifier;
        session = await client.AuthenticateDeviceAsync(deviceId, create: true, username: null);
        Debug.Log($"Device authentication successful. User ID: {session.UserId}");

        // Save the session
        PlayerPrefs.SetString("NakamaSession", session.Token);

        // Step 2: Link a custom ID to the account
        string customId = Guid.NewGuid().ToString(); // Generate a unique custom ID
        await client.LinkCustomAsync(session, customId);
        Debug.Log($"Custom ID {customId} linked successfully.");

        // Save the custom ID for future use (optional)
        PlayerPrefs.SetString("NakamaCustomId", customId);

        // Step 3: (Optional) Test authentication with custom ID
        await TestCustomAuthenticationAsync(customId);

        // Fetch account details
        await GetAccountAsync();
    }
    catch (ApiResponseException apiEx)
    {
        Debug.LogError($"API error: {apiEx.Message} (Status: {apiEx.StatusCode})");
    }
    catch (Exception ex)
    {
        Debug.LogError($"Error: {ex.Message}");
    }
}

private async Task TestCustomAuthenticationAsync(string customId)
{
    try
    {
        // Attempt to authenticate with the custom ID
        var newSession = await client.AuthenticateCustomAsync(customId, create: false, username: null);
        Debug.Log($"Custom ID authentication successful. Session Token: {newSession.Token}");
    }
    catch (Exception ex)
    {
        Debug.LogError($"Custom ID authentication failed: {ex.Message}");
    }
}

private async Task GetAccountAsync()
{
    try
    {
        var account = await client.GetAccountAsync(session);
        Debug.Log($"Username: {account.User.Username}");
        Debug.Log($"Display Name: {account.User.DisplayName}");
    }
    catch (Exception ex)
    {
        Debug.LogError($"Failed to get account: {ex.Message}");
    }
}

}