Player authentication with Epic Account Services (EAS)

Rajen Kishna, Technical Account Manager, Epic Games |
October 5, 2021
Now that we have our solution structure set up in Visual Studio 2019, we can start implementing Epic Online Services calls. We'll start by authenticating with Epic Account Services (EAS). This article will cover:
 

Auth vs. Connect Interface

A common source of confusion is when to use the Auth Interface and when to use the Connect Interface, as they both offer Login functions that follow a similar pattern. They both serve a very specific purpose, however:

Auth Interface
  • The Auth Interface handles authentication to an Epic Account. As such, it requires Epic Account Services being set up.
  • Authentication through the Auth Interface provides access to Friends, Presence, and E-commerce functionality in EOS.
  • The Auth Interface uses Epic Account IDs, which are unique to Epic Accounts.
Connect Interface
  • The Connect Interface handles authentication to Epic Game Services. It is provider-agnostic, and can thus be used with a number of Identity Providers (such as Epic Games, Steam, Xbox Live, etc.).
  • The Connect Interface does not rely on Epic Accounts but instead uses a Product User ID (PUID) that is unique to a particular product in your organization.

The easiest way to think about these two Interfaces is that the Auth Interface deals with Epic Accounts and the related social graph APIs and the Connect Interface deals with the unique user IDs that are created on behalf of your game and must be linked to an external identity. Because the IDs used by the Connect Interface are not a social graph, they can be used for cross-play and cross-progression connected to multiple identities and can be used without the Auth Interface entirely.

Lastly, there are also scenarios where you may want to use either of these interfaces where a user does not exist (yet). For example, you can use both Auth and Connect Interfaces via Web APIs on a server to verify product ownership or create & manage voice chat rooms. Another scenario would be if you don’t want to force players to log in to an account before using EOS, in which case you’d use the Device ID APIs of the Connect Interface to create a persistent pseudo-account that players can use to play the game immediately. We’ll go into this Device ID scenario more in a later article.

Setting up for EAS in the Developer Portal

As we do want to use the Auth Interface to authenticate a user, get/set their presence information, and display their friends, we first have to set up EAS by configuring an Application in the Developer Portal. There are three parts to configure an Application: Brand Settings, Permissions, and Linked Clients. We'll only need to complete the latter two for now, as Brand Settings are only needed to publish our product on Epic Games Store.
 
  1. Log in to the Developer Portal at https://dev.epicgames.com/portal/.
  2. Navigate to your product and click on Epic Account Services in the left menu. Review and accept the terms if you agree.
  3. Under Epic Account Services you will see a placeholder Application for your product already created. Click on the Configure button to set it up.
  4. As we’re not publishing our sample to Epic Games Store, we can skip over the Brand Settings tab and click on the Permissions tab on the top right.
  5. This is where you can set the permissions our application can request from the user. For now, we'll leave only the Basic Profile permission enabled, as we'll enable other permissions later when we implement that functionality. Click on Save to confirm.
  6. On the Clients tab we can select the Client(s) that are associated with this Application. Check the Client we set up previously in the Select Clients drop-down and click on Save to confirm.
  7. Lastly, click on the Back button on the top left to return to the Developer Portal.
Developer Portal Application Configured
Application configured

Implementing Auth login

As we’ve previously built out our MVVM architecture, we can start implementing the Auth login and logout functionality.
 
  1. Open MainViewModel.cs in the ViewModels folder and add the following members:

private string _accountId;
public string AccountId
{
    get { return _accountId; }
    set { SetProperty(ref _accountId, value); }
}

private string _displayName;
public string DisplayName
{
    get { return _displayName; }
    set { SetProperty(ref _displayName, value); }
}
 
  1. Open MainWindow.xaml and create a simple UI to display the AccountId and DisplayName we'll get back from the Auth Interface, and create the login and logout buttons.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <GridGrid.Row="0">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <StackPanel Grid.Column="1">
            <StackPanel Orientation="Horizontal">
                <Button Width="100" Height="23" Margin="2" Command="{Binding AuthLogin}" Content="Auth Login" />
                <Button x:Name="AuthLogoutButton" Width="100" Height="23" Margin="2" Command="{Binding AuthLogout}" Content="Auth Logout" />
            </StackPanel >
        </StackPanel >

        <StackPanel Grid.Column="0">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="AccountId:" Margin="2" />
                <TextBox Text="{Binding AccountId}" IsReadOnly="True" Margin="2" />
            </StackPanel >
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="DisplayName:" Margin="2" />
                <TextBox Text="{Binding DisplayName}" IsReadOnly="True" Margin="2" />
            </StackPanel >
        </StackPanel >
    </Grid>

    <TabControl Grid.Row="1" Margin="0,10,0,0">
    </TabControl>

    <Grid Grid.Row="2" Height="18">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <TextBlock Margin="2,0,0,2" Grid.Column="0" Text="{Binding StatusBarText}" />
        <ProgressBar Grid.Column="1" Height="18" Width="100" Visibility="{Binding StatusBarText, Converter={StaticResource StringToVisibilityConverter}}" IsIndeterminate="{Binding StatusBarText, Converter={StaticResource StringToBooleanConverter}}" />
    </Grid>
</Grid>
 
  • Aside from the Auth UI, we've added a status bar at the bottom where we can display some information and show a progress bar while making service calls to visually indicate network activity, as almost all calls are asynchronous. The progress bar uses our converters to control its visibility.
  • We'll use the TabControl in later articles to add Game Services functionality, but for now your UI should look like this:
 VS2019 MainWindow Auth UI
MainWindow Auth UI
 
  1. Next, open MainWindow.xaml.cs and add the following line in the MainWindow() constructor to set the data context, so our XAML bindings will work as intended:

DataContext = ViewModelLocator.Main;
 
  1. Now we'll need to edit our application settings to implement the Auth functionality. Open ApplicationSettings.cs and add the following members:

public LoginCredentialType LoginCredentialType { get; private set; } = LoginCredentialType.AccountPortal;
public string Id { get; private set; } = "";
public string Token { get; private set; } = "";

public ExternalCredentialType ExternalCredentialType { get; private set; } = ExternalCredentialType.Epic;

public AuthScopeFlags ScopeFlags
{
    get
    {
        return AuthScopeFlags.BasicProfile;
    }
}
  • In our sample, we'll use the AccountPortal LoginCredentialType, as it'll enable us to quickly log in using a browser. Note that there are other types available for other use cases.
  • We'll also limit our authentication to Epic Accounts, but know that other ExternalCredentialType values are supported (such as Nintendo, Steam, Discord, etc.). 
  • Lastly, we define the AuthScopeFlags to indicate what information we'll access from the user. This should correspond to the Permissions set on our Application in the Developer Portal.
 
  1. Now we’ll add the following to the Initialize() method, so we properly handle these new members if passed via command-line arguments:

LoginCredentialType = commandLineArgs.ContainsKey("-logincredentialtype") ? (LoginCredentialType)System.Enum.Parse(typeof(LoginCredentialType), commandLineArgs.GetValueOrDefault("-logincredentialtype")) : LoginCredentialType;
Id = commandLineArgs.ContainsKey("-id") ? commandLineArgs.GetValueOrDefault("-id") : Id;
Token = commandLineArgs.ContainsKey("-token") ? commandLineArgs.GetValueOrDefault("-token") : Token;
ExternalCredentialType = commandLineArgs.ContainsKey("-externalcredentialtype") ? (ExternalCredentialType)System.Enum.Parse(typeof(ExternalCredentialType), commandLineArgs.GetValueOrDefault("-externalcredentialtype")) : ExternalCredentialType;
 
  1. Create a folder called Services, which will hold our EOS interaction logic, and add a class called AuthService.cs:

public static class AuthService
{
    public static void AuthLogin()
    {
        ViewModelLocator.Main.StatusBarText = "Getting auth interface...";

        var authInterface = App.Settings.PlatformInterface.GetAuthInterface();
        if (authInterface == null)
        {
            Debug.WriteLine("Failed to get auth interface");
            ViewModelLocator.Main.StatusBarText = string.Empty;
            return;
        }

        var loginOptions = new LoginOptions()
        {
            Credentials = new Credentials()
            {
                Type = App.Settings.LoginCredentialType,
                Id = App.Settings.Id,
                Token = App.Settings.Token,
                ExternalType = App.Settings.ExternalCredentialType
            },
            ScopeFlags = App.Settings.ScopeFlags
        };

        ViewModelLocator.Main.StatusBarText = "Requesting user login...";

        authInterface.Login(loginOptions, null, (LoginCallbackInfo loginCallbackInfo) =>
        {
            Debug.WriteLine($"Auth login {loginCallbackInfo.ResultCode}");

            if (loginCallbackInfo.ResultCode == Result.Success)
            {
                ViewModelLocator.Main.StatusBarText = "Auth login successful.";

                ViewModelLocator.Main.AccountId = loginCallbackInfo.LocalUserId.ToString();

                var userInfoInterface = App.Settings.PlatformInterface.GetUserInfoInterface();
                if (userInfoInterface ==  null)
                {
                    Debug.WriteLine("Failed to get user info interface");
                    return;
                }

                var queryUserInfoOptions = new QueryUserInfoOptions()
                {
                    LocalUserId = loginCallbackInfo.LocalUserId,
                    TargetUserId = loginCallbackInfo.LocalUserId
                };

                ViewModelLocator.Main.StatusBarText = "Getting user info...";

                userInfoInterface.QueryUserInfo(queryUserInfoOptions, null, (QueryUserInfoCallbackInfo queryUserInfoCallbackInfo) =>
                {
                    Debug.WriteLine($"QueryUserInfo {queryUserInfoCallbackInfo.ResultCode}");

                    if (queryUserInfoCallbackInfo.ResultCode == Result.Success)
                    {
                        ViewModelLocator.Main.StatusBarText = "User info retrieved.";

                        var copyUserInfoOptions = new CopyUserInfoOptions()
                        {
                            LocalUserId = queryUserInfoCallbackInfo.LocalUserId,
                            TargetUserId = queryUserInfoCallbackInfo.TargetUserId
                        };

                        var result = userInfoInterface.CopyUserInfo(copyUserInfoOptions, out var userInfoData);
                        Debug.WriteLine($"CopyUserInfo: {result}");

                        if (userInfoData != null)
                        {
                            ViewModelLocator.Main.DisplayName = userInfoData.DisplayName;
                        }

                        ViewModelLocator.Main.StatusBarText = string.Empty;
                        ViewModelLocator.RaiseAuthCanExecuteChanged();
                    }
                });
            }
            else if (Common.IsOperationComplete(loginCallbackInfo.ResultCode))
            {
                Debug.WriteLine("Login failed: " + loginCallbackInfo.ResultCode);
            }
        });
    }
}

This is the meat of our Auth login implementation, so let's go through it:
 
  • Throughout the function, we'll use ViewModelLocator.Main.StatusBarText to set the status bar text in our UI, which will automatically show the progress bar when the text is not empty.
  • We use PlatformInterface.GetAuthInterface() to get an instance of the Auth Interface, which we can use throughout the function.
  • All Interface functions in EOS require an "options" class to be instantiated, which gets passed to the service call, often accompanied by a callback method event handler. For example, we instantiate LoginOptions() with the credentials and scopes from ApplicationSettings.cs and pass it to authInterface.Login().
  • We pass a lambda callback method to authInterface.Login() to handle the login response, which gets triggered by having the App.Settings.PlatformInterface?.Tick() call in MainWindow.xaml.cs when our timer updates.
  • In the callback, we check the ResultCode and if it equals Result.Success, we know the login call succeeded. If not, we write a failure message and corresponding ResultCode to the Debug output.
  • On a successful login, we store the loginCallbackInfo.LocalUserId in our MainViewModel instance, so we can use it in other service calls in the application.
  • Lastly, we use PlatformInterface.GetUserInfoInterface() to get an instance of the UserInfo Interface, which we can use to get additional user info (such as their DisplayName) through userInfoInterface.QueryUserInfo().

You'll notice that the ViewModelLocator.RaiseAuthCanExecuteChanged(); cannot resolve, which we'll tackle next.
 
  1. Create a folder called Commands in the root of our project and add a class called AuthLoginCommand.cs. This is the command that will be triggered from the login button in MainWindow.xaml:

public class AuthLoginCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return string.IsNullOrWhiteSpace(ViewModelLocator.Main.AccountId);
    }

    public override void Execute(object parameter)
    {
        AuthService.AuthLogin();
    }
}
 
  1. Open MainViewModel.cs and add the following member:

public AuthLoginCommand AuthLogin { get; set; }
 
  1. Additionally, add the following line to instantiate the Command in the MainViewModel() constructor:

AuthLogin = new AuthLoginCommand();
 
  1. Now we'll open ViewModelLocator.cs and implement the RaiseAuthCanExecuteChanged() function, so we can make sure the login button is only enabled when a user is not logged in:

public static void RaiseAuthCanExecuteChanged()
{
    Main.AuthLogin.RaiseCanExecuteChanged();
}

Now you can run the application and click on the Login button to trigger a browser window to authenticate using your Epic Account. Since our application is only in the development phase, you can only log in using Epic Accounts that are part of your Organization in the Developer Portal. Once you log in, you'll see a notice that this is an Unverified Application, as we have not gone through Brand Review. When continuing, you see that the user consent dialog, which shows the scope(s) the product is requesting access for, in our case the basic profile information. Upon clicking Allow, the browser will close and the callback method will get triggered, which retrieves the DisplayName and shows it, alongside the AccountId, in our app's UI.
 
Auth Unverified Application Auth User Consent
Unverified Application warning and user consent dialog 

Implementing Auth logout

Now that we have our structure in place, implementing logout is a bit quicker:
 
  1. Open AuthService.cs and add a Logout() method:

public static void AuthLogout()
{
    var logoutOptions = new LogoutOptions()
    {
        LocalUserId = EpicAccountId.FromString(ViewModelLocator.Main.AccountId)
    };

    App.Settings.PlatformInterface.GetAuthInterface().Logout(logoutOptions, null, (LogoutCallbackInfo logoutCallbackInfo) =>
    {
        Debug.WriteLine($"Logout {logoutCallbackInfo.ResultCode}");

        if (logoutCallbackInfo.ResultCode == Result.Success)
        {
            ViewModelLocator.Main.StatusBarText = "Logout successful.";

            var deletePersistentAuthOptions = new DeletePersistentAuthOptions();
            App.Settings.PlatformInterface.GetAuthInterface().DeletePersistentAuth(deletePersistentAuthOptions, null, (DeletePersistentAuthCallbackInfo deletePersistentAuthCallbackInfo) =>
            {
                Debug.WriteLine($"DeletePersistentAuth {logoutCallbackInfo.ResultCode}");

                if (logoutCallbackInfo.ResultCode == Result.Success)
                {
                    ViewModelLocator.Main.StatusBarText = "Persistent auth deleted.";

                    ViewModelLocator.Main.AccountId = string.Empty;
                    ViewModelLocator.Main.DisplayName = string.Empty;

                    ViewModelLocator.Main.StatusBarText = string.Empty;
                    ViewModelLocator.RaiseAuthCanExecuteChanged();
                }
            });
        }
        else if (Common.IsOperationComplete(logoutCallbackInfo.ResultCode))
        {
            Debug.WriteLine("Logout failed: " + logoutCallbackInfo.ResultCode);
        }
    });
}
 
  1. Add a new class called AuthLogoutCommand.cs to the Commands folder:

public class AuthLogoutCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return !string.IsNullOrWhiteSpace(ViewModelLocator.Main.AccountId);
    }

    public override void Execute(object parameter)
    {
        AuthService.AuthLogout();
    }
}
 
  1. Add a member and instantiation to MainViewModel.cs for our new command:

public AuthLoginCommand AuthLogin { get; set; }
public AuthLogoutCommand AuthLogout { get; set; }

public MainViewModel()
{
    AuthLogin = new AuthLoginCommand();
    AuthLogout = new AuthLogoutCommand();
}
 
  1. And finally, add the following line to the RaiseAuthCanExecuteChanged() method in ViewModelLocator.cs:

Main.AuthLogout.RaiseCanExecuteChanged();

Hit F5 to run the application and you'll see the Auth Logout button is disabled until you complete the login flow, and afterward it can be used to successfully log out.

Get the code

You can find the code for this article below. Make sure you follow step five and ten of the Setting up our C# solution section in the Setting up a C# solution for EOS in Visual Studio 2019 article to add the SDK to the solution, and you edit ApplicationSettings.cs to include your SDK credentials.
 
In the next article, we’ll look into getting and setting user presence information. Don't forget, you can always find a list of all the articles in this series in the series reference.

    We succeed when you succeed

    Epic believes in an open, integrated games community. By offering our online services to everyone for free, we aim to empower more developers to serve their own player communities.