File: Integration\MockOpenIdAuthority.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using Xunit;
 
namespace Aspire.Dashboard.Tests.Integration;
 
internal static class MockOpenIdAuthority
{
    /// <summary>
    /// Creates a mock authority (identity provider) that will handle the basics of the protocol, for use in automated tests.
    /// </summary>
    public static async Task<Authority> CreateAsync()
    {
        var webHost = new WebHostBuilder()
            .ConfigureServices(services => services.AddRouting())
            .UseKestrel(options =>
            {
                // Bind to loopback on a random available port
                options.Listen(IPAddress.Loopback, 0);
            })
            .Configure(app =>
            {
                // Based on code from https://github.com/dotnet/aspnetcore/blob/3f99d45b0b7d8f0427a3d98acc63098694613362/src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs#L37-L94
 
                app.UseRouting();
                app.UseEndpoints(endpoints =>
                {
                    var issuer = "";
                    var lastCode = "";
                    var jwtHandler = new JsonWebTokenHandler();
 
                    endpoints.MapGet(
                        ".well-known/openid-configuration",
                        (HttpRequest request, [FromHeader] string host) =>
                        {
                            issuer = $"{(request.IsHttps ? "https" : "http")}://{host}";
                            return Results.Json(new
                            {
                                issuer,
                                authorization_endpoint = $"{issuer}/authorize",
                                token_endpoint = $"{issuer}/token",
                            });
                        });
 
                    endpoints.MapGet(
                        "authorize",
                        (string redirect_uri, string? state, string? prompt, bool? preservedExtraQueryParams) =>
                        {
                            // Require interaction so silent sign-in does not skip RedirectToLogin.razor.
                            if (prompt == "none")
                            {
                                return Results.Redirect($"{redirect_uri}?error=interaction_required&state={state}");
                            }
 
                            // Verify that the extra query parameters added by RedirectToLogin.razor are preserved.
                            if (preservedExtraQueryParams != true)
                            {
                                return Results.Redirect($"{redirect_uri}?error=invalid_request&error_description=extraQueryParams%20not%20preserved&state={state}");
                            }
 
                            lastCode = Random.Shared.Next().ToString(CultureInfo.InvariantCulture);
                            return Results.Redirect($"{redirect_uri}?code={lastCode}&state={state}");
                        });
 
                    endpoints.MapPost(
                        "token",
                        ([FromForm] string code) =>
                        {
                            if (string.IsNullOrEmpty(lastCode) && code != lastCode)
                            {
                                return Results.BadRequest("Bad code");
                            }
 
                            return Results.Json(new
                            {
                                token_type = "Bearer",
                                scope = "openid profile",
                                expires_in = 3600,
                                id_token = jwtHandler.CreateToken(new SecurityTokenDescriptor
                                {
                                    Issuer = issuer,
                                    Audience = "s6BhdRkqt3",
                                    Claims = new Dictionary<string, object>
                                    {
                                        ["sub"] = "248289761001",
                                        ["name"] = "Jane Doe",
                                    },
                                }),
                            });
                        }).DisableAntiforgery();
                });
            })
            .ConfigureLogging(logging =>
            {
                // Log to the console
                logging.AddConsole();
            })
            .Build();
 
        await webHost.StartAsync();
 
        return new Authority(webHost, url: GetBoundAddress());
 
        string GetBoundAddress()
        {
            var serverAddress = webHost.ServerFeatures.Get<IServerAddressesFeature>();
 
            Assert.NotNull(serverAddress);
 
            var authorityUrl = serverAddress.Addresses.First().Replace("127.0.0.1", "localhost");
 
            Assert.StartsWith("http://localhost", authorityUrl);
 
            return authorityUrl;
        }
    }
 
    public sealed class Authority(IWebHost webHost, string url) : IAsyncDisposable
    {
        public string Url => url;
 
        public async ValueTask DisposeAsync()
        {
            await webHost.StopAsync();
            webHost.Dispose();
        }
    }
}