Files
UnrealEngineUWP/Engine/Source/Programs/OidcToken/Program.cs
joakim lindqvist fc0de60d6c OidcToken - Added ability to provide some context to the error page which we will use to explain how to apply for access and get support.
Improved error message when we are not able to find wslview (spawning a browser in the windows session when using linux via WSL), also fixed slow startup in wsl when having a cwd that included a lot of files (due to filesystemwatcher being applied by default for all files called appsettings.json)
Lastly we will abort the task that is waiting for the browser invoke when the timeout occurs which should help resolve UE-211237

#jira UE-211237

[CL 33294842 by joakim lindqvist in ue5-main branch]
2024-04-29 02:54:05 -04:00

179 lines
7.3 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.OIDC;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
namespace OidcToken
{
class Program
{
static async Task<int> Main(string[] args)
{
if (args.Any(s => s.Equals("--help") || s.Equals("-help")) || args.Length == 0)
{
// print help
Console.WriteLine("Usage: OidcToken [options]");
Console.WriteLine();
Console.WriteLine("Options: ");
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.");
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");
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");
return 0;
}
// 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");
ConfigurationBuilder configBuilder = new();
configBuilder.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", false, false)
.AddCommandLine(args);
IConfiguration config = configBuilder.Build();
TokenServiceOptions options = new();
config.Bind(options);
GetHordeAuthConfigResponse? hordeAuthConfig = null;
if (options.HordeUrl != null)
{
hordeAuthConfig = ReadHordeConfigurationAsync(options.HordeUrl).Result;
if (hordeAuthConfig.IsAnonymous())
{
// Indicate to the caller that auth is disabled.
return 11;
}
}
await Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(builder =>
{
builder.AddConfiguration(config);
if (hordeAuthConfig != null && !String.IsNullOrEmpty(hordeAuthConfig.ProfileName))
{
Dictionary<string, string?> values = new Dictionary<string, string?>();
values[nameof(TokenServiceOptions.Service)] = hordeAuthConfig.ProfileName;
builder.AddInMemoryCollection(values);
}
})
.ConfigureLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
if (!options.ResultToConsole)
{
loggingBuilder.AddConsole();
}
})
.ConfigureServices(
(content, services) =>
{
IConfiguration configuration = content.Configuration;
services.AddOptions<TokenServiceOptions>().Bind(configuration).ValidateDataAnnotations();
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();
services.AddSingleton<OidcTokenManager>();
services.AddTransient<ITokenStore>(TokenStoreFactory.CreateTokenStore);
services.AddHostedService<TokenService>();
})
.RunConsoleAsync();
return 0;
}
class GetHordeAuthConfigResponse
{
public string Method { get; set; } = String.Empty;
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>();
public bool IsAnonymous() => Method.Equals("Anonymous", StringComparison.OrdinalIgnoreCase);
}
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");
}
}
if (!authConfig.IsAnonymous())
{
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();
}
}
return authConfig;
}
}
}