namespace Ocelot.AcceptanceTests { using IdentityServer4.Test; using Shouldly; using IdentityServer4.AccessTokenValidation; using IdentityServer4.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Ocelot.Configuration.File; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Security.Claims; using TestStack.BDDfy; using Xunit; public class ClaimsToQueryStringForwardingTests : IDisposable { private IWebHost _servicebuilder; private IWebHost _identityServerBuilder; private readonly Steps _steps; private Action _options; private string _identityServerRootUrl; private string _downstreamQueryString; public ClaimsToQueryStringForwardingTests() { _steps = new Steps(); var identityServerPort = RandomPortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _options = o => { o.Authority = _identityServerRootUrl; o.ApiName = "api"; o.RequireHttpsMetadata = false; o.SupportedTokens = SupportedTokens.Both; o.ApiSecret = "secret"; }; } [Fact] public void should_return_response_200_and_foward_claim_as_query_string() { var user = new TestUser() { Username = "test", Password = "test", SubjectId = "registered|1231231", Claims = new List { new Claim("CustomerId", "123"), new Claim("LocationId", "1"), }, }; int port = RandomPortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new FileRoute { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = port, }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = "Test", AllowedScopes = new List { "openid", "offline_access", "api", }, }, AddQueriesToRequest = { {"CustomerId", "Claims[CustomerId] > value"}, {"LocationId", "Claims[LocationId] > value"}, {"UserType", "Claims[sub] > value[0] > |"}, {"UserId", "Claims[sub] > value[1] > |"}, }, }, }, }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt, user)) .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200)) .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("CustomerId: 123 LocationId: 1 UserType: registered UserId: 1231231")) .BDDfy(); } [Fact] public void should_return_response_200_and_foward_claim_as_query_string_and_preserve_original_string() { var user = new TestUser() { Username = "test", Password = "test", SubjectId = "registered|1231231", Claims = new List { new Claim("CustomerId", "123"), new Claim("LocationId", "1"), }, }; int port = RandomPortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new FileRoute { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = port, }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = "Test", AllowedScopes = new List { "openid", "offline_access", "api", }, }, AddQueriesToRequest = { {"CustomerId", "Claims[CustomerId] > value"}, {"LocationId", "Claims[LocationId] > value"}, {"UserType", "Claims[sub] > value[0] > |"}, {"UserId", "Claims[sub] > value[1] > |"}, }, }, }, }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt, user)) .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200)) .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/?test=1&test=2")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("CustomerId: 123 LocationId: 1 UserType: registered UserId: 1231231")) .And(_ => ThenTheQueryStringIs("?test=1&test=2&CustomerId=123&LocationId=1&UserId=1231231&UserType=registered")) .BDDfy(); } private void ThenTheQueryStringIs(string queryString) { _downstreamQueryString.ShouldBe(queryString); } private void GivenThereIsAServiceRunningOn(string url, int statusCode) { _servicebuilder = new WebHostBuilder() .UseUrls(url) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseUrls(url) .Configure(app => { app.Run(async context => { _downstreamQueryString = context.Request.QueryString.Value; StringValues customerId; context.Request.Query.TryGetValue("CustomerId", out customerId); StringValues locationId; context.Request.Query.TryGetValue("LocationId", out locationId); StringValues userType; context.Request.Query.TryGetValue("UserType", out userType); StringValues userId; context.Request.Query.TryGetValue("UserId", out userId); var responseBody = $"CustomerId: {customerId} LocationId: {locationId} UserType: {userType} UserId: {userId}"; context.Response.StatusCode = statusCode; await context.Response.WriteAsync(responseBody); }); }) .Build(); _servicebuilder.Start(); } private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType, TestUser user) { _identityServerBuilder = new WebHostBuilder() .UseUrls(url) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseUrls(url) .ConfigureServices(services => { services.AddLogging(); services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryApiScopes(new List { new ApiScope(apiName, "test"), new ApiScope("openid", "test"), new ApiScope("offline_access", "test"), new ApiScope("api.readOnly", "test"), }) .AddInMemoryApiResources(new List { new ApiResource { Name = apiName, Description = "My API", Enabled = true, DisplayName = "test", Scopes = new List() { "api", "openid", "offline_access", }, ApiSecrets = new List() { new Secret { Value = "secret".Sha256(), }, }, UserClaims = new List() { "CustomerId", "LocationId", "UserType", "UserId", }, }, }) .AddInMemoryClients(new List { new Client { ClientId = "client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = new List {new Secret("secret".Sha256())}, AllowedScopes = new List { apiName, "openid", "offline_access" }, AccessTokenType = tokenType, Enabled = true, RequireClientSecret = false, }, }) .AddTestUsers(new List { user, }); }) .Configure(app => { app.UseIdentityServer(); }) .Build(); _identityServerBuilder.Start(); _steps.VerifyIdentiryServerStarted(url); } public void Dispose() { _servicebuilder?.Dispose(); _steps.Dispose(); _identityServerBuilder?.Dispose(); } } }