Full Player Account Authentication package Tutorial and Explanation for beginners with SessionTokenExists caching

A Quick Guide to Setting Up Unity Player Accounts

I struggled a bit getting Player Accounts to work with Unity, so I thought I’d share a step-by-step guide for those starting out.

Setup Steps:

  1. Install the Authentication Package:

    • Open the Package Manager and install the Authentication package.
  2. Configure Unity Services:

    • After it imports, click the “Service” tab (next to Edit and Window tabs).
    • Hover over “Unity Player Account” and click “Configure”. Select both Android/iOS and PC.
  3. Enable player Accounts in the cloud:

    • To enable the Player Account service, you need to first enable Player Accounts in the Unity cloud.
    • Visit Cloud.Unity.com, select your project, navigate to the Authentication service, and then enable Player Accounts.
    • This step is crucial for the Player Account service to work properly in your project.
  4. Prepare Legal Documents:

    • Generate a Terms of Service and Privacy Policy with tools like ChatGPT.
    • Upload the documents to Google Docs. Then use the Doc’s generated link when setting up player accounts in the cloud.

Understanding the Code:

Many beginners get confused by this, so here’s a breakdown of the core concepts you need to know to understand the code:

  • SignInAnonymouslyAsync:

    • This is not just for guest logins. It allows you to sign in to a cached session.
    • If the player has signed in before using PlayerAccountService.Instance.StartSignInAsync() and AuthenticationService.Instance.SignInWithUnityAsync, you can call SignInAnonymouslyAsync in the Start or Awake methods so the player doesn’t need to sign in again unless they sign out.
  • PlayerAccountService.Instance.StartSignInAsync():

    • This triggers the browser to open, allowing the player to sign in with their Apple, Gmail, or email/password. After successful sign-in, you get an Access Token which you can use to authenticate the player with AuthenticationService.Instance.SignInWithUnityAsync.

The Complete Script:

using Unity.Services.Core;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Unity.Services.Authentication;
using Unity.Services.Authentication.PlayerAccounts;

class PlayerAccountsDemo : MonoBehaviour
{
    [SerializeField] TextMeshProUGUI m_StatusText;
    [SerializeField] Button SignInBTN;
    [SerializeField] Button DeleteAccountBTN;
    [SerializeField] Button SignOutBTN;

    async void Awake()
    {
        SignInBTN.onClick.AddListener(StartSignInAsync);
        DeleteAccountBTN.onClick.AddListener(OpenAccountPortal);
        SignOutBTN.onClick.AddListener(SignOut);
        await UnityServices.InitializeAsync();
        PlayerAccountService.Instance.SignedIn += OnSignedIn;
        AuthenticationService.Instance.SignedIn += OnSignedIn2;

        // Check if a cached player exists that had logged in before in a previous game session
        if (AuthenticationService.Instance.SessionTokenExists)
        {
            Debug.Log("Cached session exists. Attempting to restore session...");
            try
            {
                // SignInAnonymouslyAsync isn't just for guest accounts. This is how session token is used apparently.
                await AuthenticationService.Instance.SignInAnonymouslyAsync();
            }
            catch (RequestFailedException ex)
            {
                Debug.LogException(ex);
                m_StatusText.text = "Failed to restore session.";
            }
        }
        else
        {
            m_StatusText.text = "Please sign in.";
        }
    }

    async void OnSignedIn()
    {
        // Need to wait a bit or access token returns empty string
        await System.Threading.Tasks.Task.Delay(100);
        // Need to sign in again with AuthenticationService, this time with the access token we got from PlayerAccountService's sign-in.
        await AuthenticationService.Instance.SignInWithUnityAsync(PlayerAccountService.Instance.AccessToken);
        Debug.Log("Saved session!");
    }

    void OnSignedIn2()
    {
        m_StatusText.text = $"PlayerId: {AuthenticationService.Instance.PlayerId}";
    }

    async void StartSignInAsync()
    {
        if (!PlayerAccountService.Instance.IsSignedIn)
        {
            try
            {
               // This will provide an access token, which we will use to authenticate the player 
               // with AuthenticationService's SignInWithUnityAsync method. This occurs inside the OnSignedIn() 
              // method, to which we have subscribed for handling the sign-in process.
                await PlayerAccountService.Instance.StartSignInAsync();
            }
            catch (RequestFailedException ex)
            {
                Debug.LogException(ex);
            }
        }
    }

    void SignOut()
    {
        AuthenticationService.Instance.SignOut();
        PlayerAccountService.Instance.SignOut();
        AuthenticationService.Instance.ClearSessionToken();
        m_StatusText.text = "Signed out successfully.";
        Debug.Log("User signed out. Session cleared.");
    }

    // For deleting the account.
    void OpenAccountPortal()
    {
        Application.OpenURL(PlayerAccountService.Instance.AccountPortalUrl);
    }
}
2 Likes

This is indicative of an issue with the code in place. There needn’t be any waiting done. There is also absolutely no guarantee whatsoever that 100 ms or even 10,000 ms is sufficient time - you HAVE to wait for a proper event if you ever run into such an issue otherwise this code will only work for 8 out of 10 users (figuratively speaking).

Perhaps there is another method like GetAccessTokenAsync() that you need to call and await, or another corresponding event, or the call order is incorrect or not awaited.

The use of Task.Delay() anywhere revolving async calls in Unity is practically always a code smell. Furthermore, Task.Delay() will not work in web builds (freezes indefinitely).

Yes, it’s a poor solution. But its the only solution.

For some reason, PlayerAccountService.Instance.SignedIn += OnSignedIn is triggered before the token is initialized. Interestingly, the official authentication example uses the same code, which implies that subscribing to this event should theoretically work because the token should already be initialized. However, in practice, it doesn’t. This is an innate problem with Unity’s authentication package when using player accounts and should be addressed by Unity’s team.

Btw, you can use an Invoke method with a delay as a substitute for Task.Delay to give the token enough time to initialize. A safe delay option would be a 1-second delay but I’ve found that a delay of 100 milliseconds is sufficient enough. This approach isn’t ideal, but it ensures the token is ready when needed.

Again… The whole point of subscribing to PlayerAccountService.Instance.SignedIn was because the token should have been initialized by the time the event is triggered. This is necessary since we can’t directly use
await AuthenticationService.Instance.SignInWithUnityAsync(PlayerAccountService.Instance.AccessToken);
immediately after
await PlayerAccountService.Instance.StartSignInAsync(); untill the token is initialized.

It’d be great if someone would make a github issue about this. Assuming Authentication is available on github.

For now, the only solution is to either repeatedly retry executing
await AuthenticationService.Instance.SignInWithUnityAsync(PlayerAccountService.Instance.AccessToken);
until it succeeds, or introduce a delay before its execution.

There is also one more solution we can use since PlayerAccountService.Instance.AccessToken returns an empty string until it has a valid value. We can continuously check if the token has been initialized with a value before calling
await AuthenticationService.Instance.SignInWithUnityAsync(PlayerAccountService.Instance.AccessToken);.

If anyone has a better solution then I’m all ears. Since a GetAccessTokenAsync() doesn’t exist as far as I’m aware.

Hi @Charity_2,

Thanks for the post! It is always great to get a signal like this about pain points.

We are in strong agreement about some of the points of clarity you raised - including things like how SignInAnonymouslyAsync behaves - and we are working on the documentation and samples to improve onboarding with Authentication.

To confirm, you shouldn’t need a delay in the PlayerAccountService callback and I’m not sure why it’s not working without one, but we are looking into that to see if something is not behaving as intended.

I’ve never into this issue myself, and not seen other reports of it, despite using a very similar flow in my own scratch projects:

// Note: This example does not include appropriate exception handling
using UnityEngine;
using Unity.Services.Authentication;
using Unity.Services.Authentication.PlayerAccounts;
using Unity.Services.Core;

public class SignInWithUnityExample : MonoBehaviour
{
  async void Awake()
  {
    await UnityServices.InitializeAsync();
    PlayerAccountService.Instance.SignedIn += PlayerAccountSignedInEventHandler;
    AuthenticationService.Instance.Expired += ExpiredEventHandler;

    // Normally this function would be called after clicking a button,
    // I'm just calling it here in Awake() as an example
    SignIn();
  }

  public async void SignIn()
  {
    // Sign in returning player using the Session Token stored after sign in.
    // Players don't need to sign in with Unity Player Account again; using 
    // this method will re-authorise them (they will keep the same Player ID).
    if (AuthenticationService.Instance.SessionTokenExists)
    {
      await AuthenticationService.Instance.SignInAnonymouslyAsync();
      Debug.Log($"Returning Player ID: {AuthenticationService.Instance.PlayerId}");
      Debug.Log($"Returning Player is SignedIn: {AuthenticationService.Instance.IsSignedIn}");
      Debug.Log($"Returning Player is Authorized: {AuthenticationService.Instance.IsAuthorized}");
      return;
    }

    // This launches a web browser to prompt a player to sign in
    // If they have previously signed in on another device  - or have been
    // signed out of this device - then they will be signed back in with the
    // same Player ID they had before. 
    if (!PlayerAccountService.Instance.IsSignedIn)
    {
      await PlayerAccountService.Instance.StartSignInAsync();
    }
  }

  public async void ExpiredEventHandler() {
    // You should only get here if a session fails to automatically be refreshed
    // e.g. due to a network error, or a player being offline
    // You would probably want to explicitly handle this condition, but how 
    // depends on the game.
    if (!AuthenticationService.Instance.IsSignedIn)
    {
      await AuthenticationService.Instance.SignInAnonymouslyAsync();
    }
  }

  async void PlayerAccountSignedInEventHandler()
  {
    // This links accounts and signs them in using Unity Authentication,
    // which will generate a Player ID and create a session Token which is
    // stored in Player Prefs, which can be used to re-authorize a player.
    await AuthenticationService.Instance.SignInWithUnityAsync(PlayerAccountService.Instance.AccessToken);
    Debug.Log($"Player Accounts State: <b>{(PlayerAccountService.Instance.IsSignedIn ? "Signed in" : "Signed out")}</b>");
    Debug.Log($"Player Accounts Access token: <b>{(string.IsNullOrEmpty(PlayerAccountService.Instance.AccessToken) ? "Missing" : "Exists")}</b>\n");
    Debug.Log($"Player ID: {AuthenticationService.Instance.PlayerId}");
    Debug.Log($"Player is SignedIn: {AuthenticationService.Instance.IsSignedIn}");
    Debug.Log($"Player is Authorized: {AuthenticationService.Instance.IsAuthorized}");
  }

  public void SignOut()
  {
    AuthenticationService.Instance.ClearSessionToken();
  }

}

It’s possible there some subtle difference in behaviour between the examples that I’m missing, or that it’s indeed a code of code in the Authentication SDK not behaving as expected - we’ll try and replicate the issue you are experiencing and take a look at what the Authentication SDK is doing under the hood.

1 Like

Hello! Thank you for the code sample. I tried your SignOut method which clears the session token. But I’m getting this error when I do so:
“-AuthenticationException Invalid state for this operation. The player is already signed in.
-Unity.Services.Authentication.AuthenticationServiceInternal.ClearSessionToken()”

This this the recommended way to sign out?

1 Like

Hmm… I have had no issues with this.

However it may be caused by ClearSessionToken being on the last line:

So change this:

AuthenticationService.Instance.SignOut();
PlayerAccountService.Instance.SignOut();
AuthenticationService.Instance.ClearSessionToken();

To this:

AuthenticationService.Instance.ClearSessionToken();
AuthenticationService.Instance.SignOut();
PlayerAccountService.Instance.SignOut();

Do tell me if this fixed the error or not.

Edit 1:
I asked chat gpt and it said clearing the session token before signing out the authentication service might cause issues with the sign-out process

so nvm…

Edit 2:
I shared the error message with ChatGPT, and it said the problems lies elsewhere. That you should sign out before attempting to sign in again.
(You Signed in. Then without signing out. Tried to sign in again.)
To solve the issue, you could simply sign out before another sign-in attempt is initiated in the OnSignedIn() method like this:

async void OnSignedIn()
{
    if (AuthenticationService.Instance.IsSignedIn) 
    {
         AuthenticationService.Instance.SignOut();
    }
    await System.Threading.Tasks.Task.Delay(100);
    await AuthenticationService.Instance.SignInWithUnityAsync(PlayerAccountService.Instance.AccessToken);
    Debug.Log("Session saved!");
}