如何为由 OpenId Connect、cookie 身份验证和 NancyFx 和 Owin 托管的 WebApi 编写内存集成测试

How to write in memory integration test for a WebApi secured by OpenId Connect, cookie authentication and hosted by NancyFx and Owin

我有一个使用 Owin 和 NancyFX 的 WabApi 项目。 api 由 OpenId Connect 和 cookie 身份验证保护。

我必须使用 HttpClient 编写一些内存集成测试,只要您不尝试使用基于 OpenId Connect 和 cookie 的身份验证,这就非常容易。

有谁知道如何为 HttpClient 准备一个正确的身份验证 cookie 以使其作为经过身份验证的用户与 WebApi 连接?

目前我可以进行一些 http 调用以从 OpenId Connect 提供程序(由 IdentityServer v3 实现)获取正确的访问令牌、id 令牌等,但我不知道如何为 HttpClient 准备身份验证 cookie。

PS:我对 OpenId Connect 使用混合流程

您可以在下面找到我的一些文件。

服务器项目: WebApi 的 AppStartup:

服务器应用程序同时托管 WebApi 和 OpenId Connect 提供程序 (IdentityServer v3),因此其应用程序启动如下所示:

public class ServerAppStartup
{
    public static void Configuration(IAppBuilder app)
    {          
        app.Map("/identity", idsrvApp =>
        {
            var factory = new IdentityServerServiceFactory {...};
            idsrvApp.UseIdentityServer(new IdentityServerOptions
            {
                SiteName = "server app",
                SigningCertificate = ...,
                RequireSsl = false,
                Factory = factory,
                AuthenticationOptions = new AuthenticationOptions {
                    RememberLastUsername = true 
                },
                EnableWelcomePage = false
            });
        });

        app.SetDefaultSignInAsAuthenticationType("ClientCookie");

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationMode = AuthenticationMode.Active,
            AuthenticationType = "ClientCookie",
            CookieName = CookieAuthenticationDefaults.CookiePrefix + "ClientCookie",
            ExpireTimeSpan = TimeSpan.FromMinutes(5)
        });

        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {
            AuthenticationMode = AuthenticationMode.Active,
            AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
            SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(),

            Authority = options.BaseUrl+ "identity",

            ClientId = options.ClientId,                
            RedirectUri = options.RedirectUri,
            PostLogoutRedirectUri = options.PostLogoutRedirectUri,

            ResponseType = "code id_token",
            Scope = "openid profile offline_access",                                

            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                AuthorizationCodeReceived = async n =>
                {
                    /* stuff to get ACCESS TOKEN from CODE TOKEN */
                },

                RedirectToIdentityProvider = n =>
                {                       
                    if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
                    {
                        var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");

                        if (idTokenHint != null)
                        {
                            n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                        }
                    }

                    return Task.FromResult(0);
                }
            }
        }            

        JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();        

        app.UseNancy();
        app.UseStageMarker(PipelineStage.MapHandler);
    }

nancy 模块示例(类似于 MVC 或 WebApi 中的控制器):

using System;
using Nancy.ModelBinding;
using Nancy.Security;
namespace Server.Modules
{
    public class UsersModule : BaseModule  
    {
        public UsersModule() : base("/users")
        {
            Get["/getall"] = parameters =>
            {
                this.RequiresMSOwinAuthentication();
                ...
                return ...;
            };
        }
    }
}

集成测试项目:

测试服务器让我运行内存中的WebApi:

 public class TestServer: IDisposable
{
    private Func<IDictionary<string, object>, Task> _appFunc;
    public static CookieContainer CookieContainer;      
    public Uri BaseAddress { get; set; }
    // I uses OwinHttpMessageHandler becaouse it can handle http redirections
    public OwinHttpMessageHandler Handler { get; private set; }
    public HttpClient HttpClient => new HttpClient(Handler) { BaseAddress = BaseAddress };

    public static TestServer Create()
    {
        CookieContainer = new CookieContainer();           
        var result = new TestServer();

        var appBuilder = new AppBuilder();
        appBuilder.Properties["host.AppName"] = "WebApi server";

        /* Use configuration of server app */
        ServerAppStartup.Configuration(appBuilder);

        result._appFunc = appBuilder.Build();
        result.Handler = new OwinHttpMessageHandler(result._appFunc)
        {
            AllowAutoRedirect = true,
            AutoRedirectLimit = 1000,
            CookieContainer = CookieContainer,
            UseCookies = true          
        };          

        return result;
    }

    public void Dispose()
    {           
        Handler.Dispose();
        GC.SuppressFinalize(this);
    }
}

样本测试:

namespace ServerSpec.Specs.Users
{
    public class GetAllUsersSpec
    {   
        private TestServer _server;

        public GetAllUsersSpec(){
            server = TestServer.create();
        }

        [Fact]
        public void should_return_all_users()
        {
            /* here I will get error because http client or rather its cookie handler has no authentication cookie */
            var users = Get("/users/getall");
            ...
        }

        public TResponse Get<TResponse>(string urlFragment)
        {
            var client = server.HttpClient();

            var httpResponse = client.GetAsync(urlFragment).Result;
            httpResponse.EnsureSuccessStatusCode();
            return httpResponse.Content.ReadAsAsync<TResponse>().Result;
        }
    }       
}

查看此项目中的单元测试和集成测试:

https://github.com/IdentityModel/IdentityModel.Owin.PopAuthentication

它显示了在一个管道中使用 IdentityServer 进行内存中集成测试,并在另一个接受令牌的管道中使用(假)web api。