2022-06-29 07:31:01 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
2024-04-16 14:55:33 -04:00
using EpicGames.OIDC ;
2022-06-29 07:31:01 -04:00
using Microsoft.Extensions.Configuration ;
2024-04-16 14:55:33 -04:00
using Microsoft.Extensions.Configuration.Memory ;
2022-06-29 07:31:01 -04:00
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Hosting ;
using Microsoft.Extensions.Logging ;
2024-04-16 14:55:33 -04:00
using System ;
using System.Collections.Generic ;
using System.IO ;
2022-06-29 07:31:01 -04:00
using System.Linq ;
2024-04-16 14:55:33 -04:00
using System.Net.Http ;
using System.Net.Http.Json ;
using System.Text.Json ;
using System.Threading.Tasks ;
2024-05-02 05:00:59 -04:00
using Serilog ;
using Serilog.Events ;
using System.Runtime.InteropServices ;
2022-06-29 07:31:01 -04:00
namespace OidcToken
{
class Program
{
2024-04-27 13:00:28 -04:00
static async Task < int > Main ( string [ ] args )
2022-06-29 07:31:01 -04:00
{
if ( args . Any ( s = > s . Equals ( "--help" ) | | s . Equals ( "-help" ) ) | | args . Length = = 0 )
{
// print help
2024-04-16 14:55:33 -04:00
Console . WriteLine ( "Usage: OidcToken [options]" ) ;
2022-06-29 07:31:01 -04:00
Console . WriteLine ( ) ;
Console . WriteLine ( "Options: " ) ;
2024-04-16 14:55:33 -04:00
Console . WriteLine ( " --Service <serviceName> - Indicate which OIDC service you intend to connect to. The connection details of the service is configured in appsettings.json/oidc-configuration.json." ) ;
Console . WriteLine ( " --HordeUrl <url> - Specifies the URL of a Horde server to read configuration from." ) ;
2022-06-29 07:31:01 -04:00
Console . WriteLine ( " --Mode [Query/GetToken] - Switch mode to allow you to preview operation without triggering user interaction (result can be used to determine if user interaction is required)" ) ;
Console . WriteLine ( " --OutFile <path> - Path to create json file of result" ) ;
Console . WriteLine ( " --ResultToConsole [true/false] - If true the resulting json file is output to stdout (and logs are not created)" ) ;
Console . WriteLine ( " --Unattended [true/false] - If true we assume no user is present and thus can not rely on their input" ) ;
Console . WriteLine ( " --Zen [true/false] - If true the resulting refresh token is posted to Zens token endpoints" ) ;
2022-07-01 04:36:09 -04:00
Console . WriteLine ( " --Project <path> - Project can be used to tell oidc token which game its working in to allow us to read game specific settings" ) ;
2024-04-27 13:00:28 -04:00
return 0 ;
2022-06-29 07:31:01 -04:00
}
2024-04-29 02:54:05 -04:00
// disable reloadConfigOnChange in this process, as this can cause issues under wsl and we disable this for all configuration we actually load anyway
Environment . SetEnvironmentVariable ( "DOTNET_hostBuilder:reloadConfigOnChange" , "false" ) ;
2022-06-29 07:31:01 -04:00
ConfigurationBuilder configBuilder = new ( ) ;
configBuilder . SetBasePath ( AppContext . BaseDirectory )
. AddJsonFile ( "appsettings.json" , false , false )
. AddCommandLine ( args ) ;
IConfiguration config = configBuilder . Build ( ) ;
2022-07-01 04:36:09 -04:00
TokenServiceOptions options = new ( ) ;
config . Bind ( options ) ;
2024-04-16 14:55:33 -04:00
GetHordeAuthConfigResponse ? hordeAuthConfig = null ;
if ( options . HordeUrl ! = null )
2022-07-01 04:36:09 -04:00
{
2024-04-16 14:55:33 -04:00
hordeAuthConfig = ReadHordeConfigurationAsync ( options . HordeUrl ) . Result ;
2024-04-27 13:00:28 -04:00
if ( hordeAuthConfig . IsAnonymous ( ) )
{
// Indicate to the caller that auth is disabled.
return 11 ;
}
2022-07-01 04:36:09 -04:00
}
2022-06-29 07:31:01 -04:00
await Host . CreateDefaultBuilder ( args )
2024-05-02 05:00:59 -04:00
. UseSerilog ( ( context , configuration ) = >
{
configuration . ReadFrom . Configuration ( context . Configuration ) ;
if ( ! options . ResultToConsole )
{
configuration . WriteTo . Console ( restrictedToMinimumLevel : LogEventLevel . Information ) ;
}
// configure logging output directory match expectation per platform
if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
{
configuration . WriteTo . File ( Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData ) , "UnrealEngine\\Common\\OidcToken\\Logs\\oidc-token.log" ) , rollingInterval : RollingInterval . Day , restrictedToMinimumLevel : LogEventLevel . Debug , retainedFileCountLimit : 7 ) ;
}
else if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
{
configuration . WriteTo . File ( Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . ApplicationData ) , "Epic/UnrealEngine/Common/OidcToken/Logs/oidc-token.log" ) , rollingInterval : RollingInterval . Day , restrictedToMinimumLevel : LogEventLevel . Debug , retainedFileCountLimit : 7 ) ;
}
else if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Linux ) )
{
configuration . WriteTo . File ( Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . ApplicationData ) , "Epic/UnrealEngine/Common/OidcToken/Logs/oidc-token.log" ) , rollingInterval : RollingInterval . Day , restrictedToMinimumLevel : LogEventLevel . Debug , retainedFileCountLimit : 7 ) ;
}
} )
2022-06-29 07:31:01 -04:00
. ConfigureAppConfiguration ( builder = >
{
builder . AddConfiguration ( config ) ;
2024-04-16 14:55:33 -04:00
if ( hordeAuthConfig ! = null & & ! String . IsNullOrEmpty ( hordeAuthConfig . ProfileName ) )
{
Dictionary < string , string? > values = new Dictionary < string , string? > ( ) ;
values [ nameof ( TokenServiceOptions . Service ) ] = hordeAuthConfig . ProfileName ;
builder . AddInMemoryCollection ( values ) ;
}
2022-06-29 07:31:01 -04:00
} )
. ConfigureServices (
( content , services ) = >
{
IConfiguration configuration = content . Configuration ;
services . AddOptions < TokenServiceOptions > ( ) . Bind ( configuration ) . ValidateDataAnnotations ( ) ;
2024-04-16 14:55:33 -04:00
IConfiguration serviceConfig ;
if ( hordeAuthConfig ! = null )
{
Dictionary < string , string? > values = new Dictionary < string , string? > ( ) ;
values [ $"{nameof(OidcTokenOptions.Providers)}:{hordeAuthConfig.ProfileName}:{nameof(ProviderInfo.DisplayName)}" ] = hordeAuthConfig . ProfileName ;
values [ $"{nameof(OidcTokenOptions.Providers)}:{hordeAuthConfig.ProfileName}:{nameof(ProviderInfo.ServerUri)}" ] = hordeAuthConfig . ServerUrl ;
values [ $"{nameof(OidcTokenOptions.Providers)}:{hordeAuthConfig.ProfileName}:{nameof(ProviderInfo.ClientId)}" ] = hordeAuthConfig . ClientId ;
values [ $"{nameof(OidcTokenOptions.Providers)}:{hordeAuthConfig.ProfileName}:{nameof(ProviderInfo.RedirectUri)}" ] = hordeAuthConfig . LocalRedirectUrls ! [ 0 ] ;
serviceConfig = new ConfigurationBuilder ( ) . AddInMemoryCollection ( values ) . Build ( ) ;
}
else
{
// guess where the engine directory is based on the assumption that we are running out of Engine\Binaries\DotNET\OidcToken\<platform>
DirectoryInfo engineDir = new DirectoryInfo ( Path . Combine ( AppContext . BaseDirectory , "../../../../../Engine" ) ) ;
if ( ! engineDir . Exists )
{
// try to see if engine dir can be found from the current code path Engine\Source\Programs\OidcToken\bin\<Configuration>\<.net-version>
engineDir = new DirectoryInfo ( Path . Combine ( AppContext . BaseDirectory , "../../../../../../../Engine" ) ) ;
if ( ! engineDir . Exists )
{
throw new Exception ( $"Unable to guess engine directory so unable to continue running. Starting directory was: {AppContext.BaseDirectory}" ) ;
}
}
serviceConfig = ProviderConfigurationFactory . ReadConfiguration ( engineDir , ! string . IsNullOrEmpty ( options . Project ) ? new DirectoryInfo ( options . Project ) : null ) ;
}
services . AddOptions < OidcTokenOptions > ( ) . Bind ( serviceConfig ) . ValidateDataAnnotations ( ) ;
2022-06-29 07:31:01 -04:00
services . AddSingleton < OidcTokenManager > ( ) ;
2024-05-02 05:00:59 -04:00
services . AddSingleton < ITokenStore > ( TokenStoreFactory . CreateTokenStore ) ;
2022-06-29 07:31:01 -04:00
services . AddHostedService < TokenService > ( ) ;
} )
. RunConsoleAsync ( ) ;
2024-04-27 13:00:28 -04:00
2024-05-02 08:57:47 -04:00
return Environment . ExitCode ;
2022-06-29 07:31:01 -04:00
}
2024-04-16 14:55:33 -04:00
class GetHordeAuthConfigResponse
{
2024-04-27 13:00:28 -04:00
public string Method { get ; set ; } = String . Empty ;
2024-04-16 14:55:33 -04:00
public string ProfileName { get ; set ; } = null ! ;
public string ServerUrl { get ; set ; } = null ! ;
public string ClientId { get ; set ; } = null ! ;
public List < string > LocalRedirectUrls { get ; set ; } = new List < string > ( ) ;
2024-04-27 13:00:28 -04:00
public bool IsAnonymous ( ) = > Method . Equals ( "Anonymous" , StringComparison . OrdinalIgnoreCase ) ;
2024-04-16 14:55:33 -04:00
}
static async Task < GetHordeAuthConfigResponse > ReadHordeConfigurationAsync ( Uri hordeUrl )
{
// Read the configuration settings from the Horde server
GetHordeAuthConfigResponse ? authConfig ;
using ( HttpClient httpClient = new HttpClient ( ) )
{
using HttpRequestMessage request = new HttpRequestMessage ( HttpMethod . Get , new Uri ( hordeUrl , "api/v1/server/auth" ) ) ;
using HttpResponseMessage response = await httpClient . SendAsync ( request ) ;
response . EnsureSuccessStatusCode ( ) ;
JsonSerializerOptions jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true } ;
authConfig = await response . Content . ReadFromJsonAsync < GetHordeAuthConfigResponse > ( jsonOptions ) ;
if ( authConfig = = null )
{
throw new InvalidDataException ( "Server returned an empty auth config object" ) ;
}
}
2024-04-27 13:00:28 -04:00
if ( ! authConfig . IsAnonymous ( ) )
2024-04-16 14:55:33 -04:00
{
2024-04-27 13:00:28 -04:00
string? localRedirectUrl = authConfig . LocalRedirectUrls . FirstOrDefault ( ) ;
if ( String . IsNullOrEmpty ( authConfig . ServerUrl ) | | String . IsNullOrEmpty ( authConfig . ClientId ) | | String . IsNullOrEmpty ( localRedirectUrl ) )
{
throw new Exception ( "No auth server configuration found" ) ;
}
if ( String . IsNullOrEmpty ( authConfig . ProfileName ) )
{
authConfig . ProfileName = hordeUrl . Host . ToString ( ) ;
}
2024-04-16 14:55:33 -04:00
}
return authConfig ;
}
2022-06-29 07:31:01 -04:00
}
}