【问题标题】:How to mock Jwt bearer token for integration tests如何模拟 Jwt 不记名令牌以进行集成测试
【发布时间】:2021-11-10 06:31:10
【问题描述】:

我使用 .Net 5 创建了一个微服务,它有一些只能使用 jwtBearertoken 调用的端点。

StartUp 类中的 ConfigureServicesConfigure 方法如下所示:

        public void ConfigureServices(IServiceCollection services)
    {
        ConfigureDatabaseServices(services);
        ConfigureMyProjectClasses(services);
        services.AddVersioning();

        services.AddControllers();
        services.AddAuthentication(_configuration);
        // Add framework services.
        var mvcBuilder = services
            .AddMvc()
            .AddControllersAsServices();
        ConfigureJsonSerializer(mvcBuilder);
    }

        public void Configure(
        IApplicationBuilder app,
        IWebHostEnvironment webEnv,
        ILoggerFactory loggerFactory,
        IHostApplicationLifetime applicationLifetime)
    {
        _logger = loggerFactory.CreateLogger("Startup");

        try
        {
            app.Use(async (context, next) =>
            {
                var correlationId = Guid.NewGuid();
                System.Diagnostics.Trace.CorrelationManager.ActivityId = correlationId;
                context.Response.Headers.Add("X-Correlation-ID", correlationId.ToString());
                await next();
            });

            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
            applicationLifetime.ApplicationStopped.Register(() =>
            {
                LogManager.Shutdown();
            });
        }
        catch (Exception e)
        {
            _logger.LogError(e.Message);
            throw;
        }
    }

身份验证扩展:

    public static class AuthenticationExtensions
    {
    public static void AddAuthentication(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options     =>
        {
            options.Authority = configuration["Authorization:Authority"];
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateAudience = false
            };
        });
    }
}

我正在使用微服务的授权服务器来验证令牌。

在控制器上方添加[Authorize] 属性后,邮递员返回401 Unauthorized,而我在添加身份验证之前创建的集成测试也按预期返回Unauthorized。 现在我试图弄清楚如何通过添加 JwtBearerToken 并模拟来自授权服务器的响应来更改我的集成测试,以便我的测试将再次通过。 我怎样才能做到这一点?

【问题讨论】:

  • 即使有可能,您也不应该模拟 Authorize 属性。(我不确定这是否可能)集成测试的目标是测试实际的请求/响应。我认为你应该为你的测试生成一个令牌。
  • @AliReza 无意模拟 Authorize 属性,我试图模拟添加 jwtbearertoken 并模拟授权服务器的响应
  • 我知道我的意思是当您在管道中获得授权时,您的应用程序的行为可能会有所不同。最好在您的请求中包含令牌。但最后,如果你真的需要模拟授权。你必须开始模拟整个 DI

标签: c# jwt


【解决方案1】:

我的答案不是 100% 集成,因为我们将添加一个额外的身份验证方案。 TL;DR:您不是在测试您的身份验证是否有效,而是在解决它。

最好使用 ACTUAL 令牌,但也许这个解决方案是一个不错的中间立场。

您可以创建另一个身份验证方案,例如 DevBearer,您可以在其中指定一个帐户,例如,如果您发送身份验证标头 DevBearer Customer-John,应用程序会将您识别为客户 John。

我在开发过程中使用这种方法,因为它很容易快速测试不同的用户。我的代码如下所示:

Startup.Auth.cs

        private void ConfigureAuthentication(IServiceCollection services)
        {
            services.AddHttpContextAccessor();

            services
                    .AddAuthentication(options =>
                    {
                        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                    })
                    .AddJwtBearer(options =>
                    {
                        options.Audience = "Audience";
                        options.Authority = "Authority";
                    });

#if DEBUG
            if (Environment.IsDevelopment())
            {
                AllowDevelopmentAuthAccounts(services);
                return;
            }
#endif

            // This is custom and you might need change it to your needs.
            services.AddAuthorization();

        }


#if DEBUG
        // If this is true, you can use the Official JWT bearer login flow AND Development Auth Account (DevBearer) flow for easier testing.
        private static void AllowDevelopmentAuthAccounts(IServiceCollection services)
        {
            services.AddAuthentication("DevBearer").AddScheme<DevelopmentAuthenticationSchemeOptions, DevelopmentAuthenticationHandler>("DevBearer", null);

            // This is custom and you might need change it to your needs.
            services.AddAuthorization();
        }
#endif

自定义策略提示

// Because my Policies/Auth situation is different than yours, I will only post a hint that you might want to use.
// I want to allow calls from the REAL flow AND DevBearer flow during development so I can easily call my API using the DevBearer flow, or still connect it to the real IDentityServer and front-end for REAL calls.

                var policyBuilder = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser();

                // The #IF adds an extra "security" check so we don't accidentally activate the development auth flow on production
#if DEBUG
                if (_allowDevelopmentAuthAccountCalls)
                {
                    policyBuilder.AddAuthenticationSchemes("DevBearer").RequireAuthenticatedUser();
                }
#endif

                return policyBuilder;

身份验证处理程序

#if DEBUG
using System;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace NAMESPACE
{
    public class DevelopmentAuthenticationHandler : AuthenticationHandler<DevelopmentAuthenticationSchemeOptions>
    {
        public DevelopmentAuthenticationHandler(
            IOptionsMonitor<DevelopmentAuthenticationSchemeOptions> options,
            ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
            : base(options, logger, encoder, clock)
        {
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Context.Request.Headers.TryGetValue("Authorization", out var authorizationHeader))
            {
                return AuthenticateResult.Fail("Unauthorized");
            }

            var auth = AuthenticationHeaderValue.Parse(authorizationHeader);

            if (auth.Scheme == "Bearer")
            {
                // If Bearer is used, it means the user wants to use the REAL authentication method and not the development accounts. 
                return AuthenticateResult.Fail("Bearer requests should use the real JWT validation scheme");
            }

            // Dumb workaround for NSwag/Swagger: I can't find a way to make it automatically pass "DevBearer" in the auth header.
            // Having to type DevBearer everytime is annoying. So if it is missing, we just pretend it's there.
            // This means you can either pass "ACCOUNT_NAME" in the Authorization header OR "DevBearer ACCOUNT_NAME".
            if (auth.Parameter == null)
            {
                auth = new AuthenticationHeaderValue("DevBearer", auth.Scheme);
            }

            IEnumerable<Claim> claims;
            try
            {
                var user = auth.Parameter;
                claims = GetClaimsForUser(user);
            }
            catch (ArgumentException e)
            {
                return AuthenticateResult.Fail(e);
            }

            var identity = new ClaimsIdentity(claims, "DevBearer");
            var principal = new ClaimsPrincipal(identity);

            // Add extra claims if you want to
            await Options.OnTokenValidated(Context, principal);

            var ticket = new AuthenticationTicket(principal, "DevBearer");

            return AuthenticateResult.Success(ticket);
        }

        private static IEnumerable<Claim> GetClaimsForUser(string? user)
        {
            switch (user?.ToLowerInvariant())
            {
                // These all depend on your needs.
                case "Customer-John":
                    {
                        yield return new("ID_CLAIM_NAME", Guid.Parse("JOHN_GUID_THAT_EXISTS_IN_YOUR_DATABASE").ToString(), ClaimValueTypes.String);
                        yield return new("ROLE_CLAIM_NAME", "Customer", ClaimValueTypes.String);
                        break;
                    }
                default:
                    {
                        throw new ArgumentException("Can't set specific account for local development because the user is not recognized", nameof(user));
                    }
            }
        }
    }

    public class DevelopmentAuthenticationSchemeOptions : AuthenticationSchemeOptions
    {
        public Func<HttpContext, ClaimsPrincipal, Task> OnTokenValidated { get; set; } = (context, principal) => { return Task.CompletedTask; };
    }
}
#endif

使用类似的方法,您可以使用DevBearer Customer-John 之类的授权标头进行 API 调用,并将 ID 和角色声明添加到上下文中,从而使身份验证成功:)

【讨论】:

  • 你说得对,我不想测试身份验证是否有效并且想要解决它。那么这样我就可以将不记名令牌添加到请求的Authorization 标头中?
  • 喜欢这个? Client.DefaultRequestHeaders.Add("Authorization", "DevBearer Customer-John)?
  • 是的,你可以这样称呼它:)。如果您对这种方法有任何问题,请告诉我。如果它有效并且您对这种方法感到满意,请接受它作为答案。如果它确实有效,我仍然会问你为什么要跳过集成测试中的 auth 部分。集成测试应该测试系统的多个部分协同工作。为什么不希望包含您的身份验证系统?
  • 我不一定要跳过身份验证部分,我希望能够模拟它并测试它通过身份验证的情况。我有一个特殊的TestServer 具有身份验证功能,并为此特定目的设置了一个客户端。
猜你喜欢
  • 2019-06-09
  • 1970-01-01
  • 2020-05-06
  • 2017-01-23
  • 2015-08-13
  • 2019-05-21
  • 1970-01-01
  • 2021-07-12
  • 2017-02-23
相关资源
最近更新 更多