// Copyright Epic Games, Inc. All Rights Reserved. using System.Diagnostics; using System.Net.Mime; using System.Text; using System.Text.Json; using EpicGames.OIDC; using IdentityModel.Client; using IdentityModel.OidcClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; using Serilog; namespace OidcToken.Tests { [TestClass] public class OidcTokenTests { private IHost SetupMocks(Action configureCliOptions, Action configureOidcOptions, Func mockTokenClient, ITokenStore tokenStore) { Mock clientFactory = new Mock(); clientFactory.Setup(factory => factory.CreateTokenClient(It.IsAny(), It.IsAny(), It.IsAny())).Returns(mockTokenClient); IHost host = Host.CreateDefaultBuilder() .UseEnvironment("Testing") .ConfigureServices(collection => collection.AddSerilog()) .ConfigureServices( (content, services) => { services.AddOptions().Configure(configureCliOptions).ValidateDataAnnotations(); services.AddOptions().Configure(configureOidcOptions).ValidateDataAnnotations(); services.AddSingleton(); services.AddSingleton(clientFactory.Object); services.AddSingleton(tokenStore); services.AddSingleton(); services.AddHostedService(provider => provider.GetService()!); }) .Build(); return host; } [TestMethod] public async Task AllocateTokenToJsonFile() { const string providerName = "Test-Service"; ProviderInfo providerInfo = new ProviderInfo() { ClientId = "test-client", DisplayName = "test-client", RedirectUri = null, Scopes = "offline profile", }; const string mockRefreshToken = "random-refresh-token"; // TODO: Make the mocked access token into a valid JWT? const string mockAccessToken = "random-token"; DateTimeOffset mockExpiryTime = DateTimeOffset.Now.AddDays(2); Mock tokenStoreMock = new Mock() { CallBase = true }; tokenStoreMock.Setup(store => store.AddRefreshToken(It.Is(providerName, StringComparer.InvariantCultureIgnoreCase), It.Is(mockRefreshToken, StringComparer.InvariantCultureIgnoreCase))).CallBase().Verifiable(Times.Once); tokenStoreMock.Setup(store => store.Save()).CallBase().Verifiable(Times.Once); string tempOutputFileName = Path.GetTempFileName(); try { using IHost host = SetupMocks(cliOptions => { cliOptions.Service = providerName; cliOptions.OutFile = tempOutputFileName; }, oidcTokenOptions => { oidcTokenOptions.Providers = new Dictionary() { { providerName, providerInfo } }; }, () => { Microsoft.Extensions.Logging.ILogger? logger = null; Mock tokenClientMock = new Mock(providerName, providerInfo, TimeSpan.FromMinutes(15) , tokenStoreMock.Object, logger!) { // call into the real OidcTokenClient unless the method has been mocked CallBase = true }; tokenClientMock.Setup(client => client.DoLoginAsync(It.IsAny())).ReturnsAsync(() => new OidcTokenInfo() {AccessToken = mockAccessToken, RefreshToken = mockRefreshToken, TokenExpiry = mockExpiryTime} ); return tokenClientMock.Object; }, tokenStoreMock.Object); TokenService tokenService = host.Services.GetService()!; Assert.IsNotNull(tokenService); await tokenService.Main(); Assert.IsTrue(File.Exists(tempOutputFileName)); await using FileStream fs = File.OpenRead(tempOutputFileName); TokenResultFile? result = JsonSerializer.Deserialize(fs); Assert.AreEqual(mockAccessToken, result!.Token); Assert.AreEqual(mockExpiryTime, result.ExpiresAt); tokenStoreMock.VerifyAll(); } finally { File.Delete(tempOutputFileName); } } [TestMethod] public async Task FailToAllocateToken() { const string providerName = "Test-Service"; ProviderInfo providerInfo = new ProviderInfo() { ClientId = "test-client", DisplayName = "test-client", RedirectUri = new Uri("http://localhost:1234"), ServerUri = new Uri("https://path-to-idp.com"), Scopes = "offline profile", UseDiscoveryDocument = false, }; Mock tokenStoreMock = new Mock() { CallBase = true }; tokenStoreMock.Setup(store => store.AddRefreshToken(It.Is(providerName, StringComparer.InvariantCultureIgnoreCase), It.IsAny())).CallBase().Verifiable(Times.Never); tokenStoreMock.Setup(store => store.Save()).CallBase().Verifiable(Times.Never); const string loginResultErrorMessage = "failed to login"; string tempOutputFileName = Path.GetTempFileName(); try { using IHost host = SetupMocks(cliOptions => { cliOptions.Service = providerName; cliOptions.OutFile = tempOutputFileName; }, oidcTokenOptions => { oidcTokenOptions.Providers = new Dictionary() { { providerName, providerInfo } }; }, () => { Microsoft.Extensions.Logging.ILogger? logger = null; Mock tokenClientMock = new Mock(providerName, providerInfo, TimeSpan.FromMinutes(15) , tokenStoreMock.Object, logger!) { CallBase = true }; tokenClientMock.Setup(client => client.DoCreateOidcClient(It.IsAny())).Returns((OidcClientOptions options) => { Mock oidcClientMock = new Mock(options); oidcClientMock.Setup(client => client.ProcessResponseAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(() => new LoginResult(loginResultErrorMessage)); oidcClientMock.Setup(client => client.PrepareLoginAsync( It.IsAny(), It.IsAny())).ReturnsAsync(() => new AuthorizeState()); return oidcClientMock.Object; }); tokenClientMock.Setup(client => client.CreateHttpServer()).Returns(() => { Mock httpMock = new Mock(); OidcHttpRequest request = new OidcHttpRequest("", MediaTypeNames.Application.FormUrlEncoded, "POST", new MemoryStream(Encoding.Default.GetBytes("input-object")), hasBody: true, Encoding.ASCII); NullOidcHttpResponse response = new NullOidcHttpResponse(new MemoryStream()); httpMock.Setup(listener => listener.ProcessNextRequestAsync()).ReturnsAsync(() => (request, response)); return httpMock.Object; }); tokenClientMock.Setup(client => client.OpenBrowser(It.IsAny())).Returns(Mock.Of()); return tokenClientMock.Object; }, tokenStoreMock.Object); TokenService tokenService = host.Services.GetService()!; Assert.IsNotNull(tokenService); await Assert.ThrowsExceptionAsync(async () => { await tokenService.Main(); }); tokenStoreMock.VerifyAll(); } finally { File.Delete(tempOutputFileName); } } [TestMethod] public async Task SuccessfulToken() { const string providerName = "Test-Service"; ProviderInfo providerInfo = new ProviderInfo() { ClientId = "test-client", DisplayName = "test-client", RedirectUri = new Uri("http://localhost:1234"), ServerUri = new Uri("https://path-to-idp.com"), Scopes = "offline profile", UseDiscoveryDocument = false, }; Mock tokenStoreMock = new Mock() { CallBase = true }; tokenStoreMock.Setup(store => store.AddRefreshToken(It.Is(providerName, StringComparer.InvariantCultureIgnoreCase), It.IsAny())).CallBase().Verifiable(Times.Once); tokenStoreMock.Setup(store => store.Save()).CallBase().Verifiable(Times.Once); string mockAccessToken = "access-token"; string mockRefreshToken = "refresh-token"; DateTimeOffset mockExpiryTime = DateTimeOffset.Now.AddHours(4); string tempOutputFileName = Path.GetTempFileName(); try { Mock httpMock = new Mock(); // buffer to store the response in byte[] responseBuffer = new byte[16 * 1024]; NullOidcHttpResponse response = new NullOidcHttpResponse(new MemoryStream(responseBuffer)); using IHost host = SetupMocks(cliOptions => { cliOptions.Service = providerName; cliOptions.OutFile = tempOutputFileName; }, oidcTokenOptions => { oidcTokenOptions.Providers = new Dictionary() { { providerName, providerInfo } }; }, () => { Microsoft.Extensions.Logging.ILogger? logger = null; Mock tokenClientMock = new Mock(providerName, providerInfo, TimeSpan.FromMinutes(15) , tokenStoreMock.Object, logger!) { CallBase = true }; tokenClientMock.Setup(client => client.DoCreateOidcClient(It.IsAny())).Returns((OidcClientOptions options) => { Mock oidcClientMock = new Mock(options); Mock mockLoginResult = new Mock(); mockLoginResult.Setup(m => m.RefreshToken).Returns(mockRefreshToken); mockLoginResult.Setup(m => m.AccessToken).Returns(mockAccessToken); mockLoginResult.Setup(m => m.AccessTokenExpiration).Returns(mockExpiryTime); oidcClientMock.Setup(client => client.ProcessResponseAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(() => mockLoginResult.Object); oidcClientMock.Setup(client => client.PrepareLoginAsync( It.IsAny(), It.IsAny())).ReturnsAsync(() => new AuthorizeState()); return oidcClientMock.Object; }); tokenClientMock.Setup(client => client.CreateHttpServer()).Returns(() => { OidcHttpRequest request = new OidcHttpRequest("", MediaTypeNames.Application.FormUrlEncoded, "POST", new MemoryStream(Encoding.Default.GetBytes("input-object")), hasBody: true, Encoding.ASCII); httpMock.Setup(listener => listener.ProcessNextRequestAsync()).ReturnsAsync(() => (request, response)).Verifiable(Times.Once); return httpMock.Object; }); tokenClientMock.Setup(client => client.OpenBrowser(It.IsAny())).Returns(Mock.Of()); return tokenClientMock.Object; }, tokenStoreMock.Object); TokenService tokenService = host.Services.GetService()!; Assert.IsNotNull(tokenService); await tokenService.Main(); // check the result page to see what got written string s = Encoding.ASCII.GetString(responseBuffer); // no point in checking the exact html returned just make sure something was written Assert.IsFalse(string.IsNullOrEmpty(s)); Assert.IsTrue(File.Exists(tempOutputFileName)); await using FileStream fs = File.OpenRead(tempOutputFileName); TokenResultFile? result = JsonSerializer.Deserialize(fs); // this does not actually verify that a token is correctly allocated as that requires reaching out to a idp, and we mock away all of those parts. This can only check that the parts we do are working as such these are just the same values as we specified in the mock Assert.AreEqual(mockAccessToken, result!.Token); Assert.AreEqual(mockExpiryTime, result.ExpiresAt); tokenStoreMock.VerifyAll(); } finally { File.Delete(tempOutputFileName); } } } }