Issue with AuthenticateAppleAsync from quick login

We are successfully linking apple id with user nakama accounts, but when returning to the app after initial account creation, we get the apple quick login flow, and when we try to AuthenticateAppleAsync with the returned credentials, we get server error:401 and ApiResponseException: Server key invalid. Any idea why this would fail?

Nakama 2.13 Unity SDK 2.6.0

@oscargoldman does the key used by the client match the configured key on the server?

which key are we referring to? the socket.server_key used to initialize the Client object?

@oscargoldman yep.

yes, absolutely. All other nakama api calls are behaving as expected.

@oscargoldman can you share the Unity code you’ve written to obtain the credentials from the Unity plugin and pass them into the client SDK?

Before we do that, since it’s quite a lot of code, I’ve just looked up the api for this in the nakama source:
///
public async Task AuthenticateAppleAsync(string username, string token, Dictionary<string, string> vars) {
var response = await _apiClient.AuthenticateAppleAsync(username, string.Empty, new ApiAccountApple {Token = token, _vars = vars});
return new Session(response.Token, response.Created);
}

What is the vars Dictionary expecting?

oh looks like that’s the .net api, but the same question applies to the unity sdk. the authenticateAppleAsync has the username, token, and vars paramaters. What do we need for vars?

@oscargoldman The vars parameter is extra information you might include with the authentication request to be cached alongside the session. It’s not required for authentication, and will not be the cause of your issue.

The "Server key invalid." error message is only returned if you make an authentication request with the wrong server key. As @novabyte said can you share the code you use to retrieve Apple Sign In credentials and pass them to the authentication call?

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using AppleAuth;
using AppleAuth.Native;
using AppleAuth.Enums;
using AppleAuth.Extensions;
using AppleAuth.Interfaces;
using System.Text;

namespace com.skylightgames.lyrical.business.login_system
{
public class AppleSignInManager
{
private static AppleSignInManager _instance;

    private const string PLAYERPREFS_APPLE_USER_KEY = "AppleSignInManager.AppleUserId";

    private IAppleAuthManager _appleAuthManager;
    private Action _onSignInComplete;

    public IAppleIDCredential appleIdCredential { get; private set; }

    public string token 
    { 
        get
        {
            if (appleIdCredential != null)
                return Encoding.UTF8.GetString(appleIdCredential.IdentityToken);
            return null;
        } 
    }

    public bool isLoggedIn { get; private set; }
    public event Action QUICK_LOGIN_SUCCESS;
    public event Action QUICK_LOGIN_FAILED;

    public event Action CREDENTIALS_REVOKED;
    public event Action CREDENTIALS_ERROR;

    public static AppleSignInManager GetInstance() {
        if (_instance == null)
        {
            _instance = new AppleSignInManager();
        }
        return _instance;
    }

    public void init()
    {
        isLoggedIn = false;
        // If the current platform is supported
        if (AppleAuthManager.IsCurrentPlatformSupported)
        {
            // Creates a default JSON deserializer, to transform JSON Native responses to C# instances
            var deserializer = new PayloadDeserializer();
            // Creates an Apple Authentication manager with the deserializer
            this._appleAuthManager = new AppleAuthManager(deserializer);

            this._appleAuthManager.SetCredentialsRevokedCallback(result =>
            {
                CREDENTIALS_REVOKED?.Invoke();
                Debug.Log("Received revoked callback " + result);
                PlayerPrefs.DeleteKey(PLAYERPREFS_APPLE_USER_KEY);
            });

            // If we have an Apple User Id available, get the credential status for it
            if (PlayerPrefs.HasKey(PLAYERPREFS_APPLE_USER_KEY))
            {
                var storedAppleUserId = PlayerPrefs.GetString(PLAYERPREFS_APPLE_USER_KEY);
                this.checkCredentialStatus(storedAppleUserId);
            }
            // If we do not have an stored Apple User Id, attempt a quick login
            else
            {
                this.attemptQuickLogin();
            }

            Game.GAME_UPDATE_EVENT -= onUpdate;
            Game.GAME_UPDATE_EVENT += onUpdate;
        }
    }

    private static void onUpdate()
    {
        if (_instance != null && _instance._appleAuthManager != null)
        {
            _instance._appleAuthManager.Update();
        }
    }


    //so we'd initialize this is in SkylightLyriko2D if the platform is ios

    //then we'll need to check and see the status of the current credentials
    //if they are still valid I think we do the quick login flow on app startup.
    //if they are not valid or don't exist, then we wait for Main.scene to load,
    //and when the user clicks Sign In To Apple we get the credentials, and then login via
    //Nakama


    private void checkCredentialStatus (string appleUserId)
    {
        // If there is an apple ID available, we should check the credential state
        this._appleAuthManager.GetCredentialState(
            appleUserId,
            state =>
            {
                switch (state)
                {
                // If it's authorized, login with that user id
                case CredentialState.Authorized:
                        Debug.Log($"AppleSignInManager::checkCredentialStatus: authorized");
                        this.attemptQuickLogin();
                        break;

                // If it was revoked, or not found, we need a new sign in with apple attempt
                // Discard previous apple user id
                case CredentialState.Revoked:
                case CredentialState.NotFound:
                        Debug.Log($"AppleSignInManager::checkCredentialStatus: revoked or not found");
                        PlayerPrefs.DeleteKey(PLAYERPREFS_APPLE_USER_KEY);
                        isLoggedIn = false;
                        CREDENTIALS_REVOKED?.Invoke();
                        //SIGN_IN_COMPLETED?.Invoke();
                        break;
                }
            },
            error =>
            {
                var authorizationErrorCode = error.GetAuthorizationErrorCode();
                isLoggedIn = false;
                CREDENTIALS_ERROR?.Invoke();
                Debug.Log($"AppleSignInManager::checkCredentialStatus: Error while trying to get credential state " + authorizationErrorCode.ToString() + " " + error.ToString());
            });
    }


    private void attemptQuickLogin()
    {
        var quickLoginArgs = new AppleAuthQuickLoginArgs();
        Debug.Log("AppleSignInManager::attemptQuickLogin: attempt quick login");
        // Quick login should succeed if the credential was authorized before and not revoked
        this._appleAuthManager.QuickLogin(
            quickLoginArgs,
            credential =>
            {
                isLoggedIn = false;
                // If it's an Apple credential, save the user ID, for later logins
                appleIdCredential = credential as IAppleIDCredential;
                if (appleIdCredential != null)
                {
                    PlayerPrefs.SetString(PLAYERPREFS_APPLE_USER_KEY, credential.User);
                    isLoggedIn = true;
                }
                QUICK_LOGIN_SUCCESS?.Invoke();
                Debug.Log("AppleSignInManager::attemptQuickLogin: Quick login successful");
            },
            error =>
            {
                // If Quick Login fails, we should show the normal sign in with apple menu, to allow for a normal Sign In with apple
                var authorizationErrorCode = error.GetAuthorizationErrorCode();
                isLoggedIn = false;
                QUICK_LOGIN_FAILED?.Invoke();
                Debug.LogWarning("AppleSignInManager::attemptQuickLogin: Quick Login Failed " + authorizationErrorCode.ToString() + " " + error.ToString());
            });
    }

    private void signInWithApple()
    {
        var loginArgs = new AppleAuthLoginArgs(LoginOptions.IncludeEmail | LoginOptions.IncludeFullName);

        this._appleAuthManager.LoginWithAppleId(
            loginArgs,
            credential =>
            {
                Debug.Log("AppleSignInManager::signInWithApple:Sign in with Apple complete ");
                // If a sign in with apple succeeds, we should have obtained the credential with the user id, name, and email, save it
                PlayerPrefs.SetString(PLAYERPREFS_APPLE_USER_KEY, credential.User);
                appleIdCredential = credential as IAppleIDCredential;
                isLoggedIn = true;
                _onSignInComplete?.Invoke();
                _onSignInComplete = null;
            },
            error =>
            {
                var authorizationErrorCode = error.GetAuthorizationErrorCode();
                Debug.Log("AppleSignInManager::signInWithApple:Sign in with Apple failed " + authorizationErrorCode.ToString() + " " + error.ToString());
                isLoggedIn = false;
                _onSignInComplete?.Invoke();
                _onSignInComplete = null;

            });
    }


    public void connect(Action<object> tokenHandler)
    {
        if(isLoggedIn)
        {
            if (appleIdCredential != null)
                tokenHandler?.Invoke(Encoding.UTF8.GetString(appleIdCredential.IdentityToken));
            else
                tokenHandler?.Invoke(null);
        }
        else
        {
            _onSignInComplete = () =>
            {
                if (isLoggedIn)
                {
                    if (appleIdCredential != null)
                        tokenHandler?.Invoke(Encoding.UTF8.GetString(appleIdCredential.IdentityToken));
                    else
                        tokenHandler?.Invoke(null);
                }
            };
            signInWithApple();
        }
    }

}

}

the above is our apple sign in manager, we can see that we successfully quick login for a returning user who has already authorized apple via sign in. The resulting event triggers this function in our nakama manager:

public static async Task signInApple(string token)
{
NakamaResponse response = new NakamaResponse();
try
{
Debug.Log("NakamaManager::signInApple: start - token:{token}"); session = await _instance.client.AuthenticateAppleAsync(null, token, null); Debug.Log(“NakamaManager::signInApple: process session - token:{token}”);

            response.payload = await processSession(session);
        }
        catch (ApiResponseException e)
        {
            Debug.Log($"NakamaManager::signInApple server error: {e.StatusCode}");
            Debug.LogException(e);
            response.error = UserDataManager.ERROR_SERVER;
        }
        catch (Exception e)
        {
            Debug.Log($"NakamaManager::signInApple internet error");
            Debug.LogException(e);
            response.error = UserDataManager.ERROR_INTERNET;
        }
        return response;
    }

We can see in the xcode console that the nakama signInWithApple function receives the token

And on a clean install of our app prior to a user signing in with apple, nakama works perfectly and all the native and custom endpoints are working, using the same server key.

What’s also maybe strange is there is no error reported in the nakama console

Can anyone confirm that it’s the appleIdCredentials.IdentityToken that we use for AuthenticateAppleAsync? we’re using Encoding.UTF8.GetString on the byte array to create a string for the nakama call.

@oscargoldman we’ve identified the issue lies in our .NET and are pushing a fix to it in the next day or so.

oh man, thank you so much for the news. we were going a little crazy with this one. Look forward to the patch!

@oscargoldman Thanks for filing the issue, we’ve merged this PR into the .NET client which resolves the issue and can update the Unity client with the new Nakama .DLL on Monday https://github.com/heroiclabs/nakama-dotnet/pull/44

Hi, any update on the Nakama Unity SDK patch?

@oscargoldman yep, this commit has your fix:

We haven’t done the release through the Asset Store yet (that will come at some later, undetermined date) so I recommend depending on the commit using the Unity Package Manager instructions outlined in the README.md. This is the preferred dependency method anyway.