首页 > 解决方案 > 在 .net 核心应用程序上实现 JWT 会引发无效负载

问题描述

我正在尝试在我的 .net core 2.2 API 和 .net core 3.1 Web-Application 之间实现 JWT 令牌,但是在登录后取消保护令牌时遇到了问题。在调试模式(本地计算机)下,它运行良好,在将其发布到我的服务器后,我立即遇到了各种异常。

首先我得到了例外The key {...some GUID...} was not found in the keyring。所以我阅读并看到,我需要将Load User Profile我的应用程序池设置为true. 这就是我所做的。

现在我的例外说

CryptographicException: The payload was invalid.
Microsoft.AspNetCore.DataProtection.Cng.CbcAuthenticatedEncryptor.DecryptImpl(Byte* pbCiphertext, uint cbCiphertext, Byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData)
Microsoft.AspNetCore.DataProtection.Cng.Internal.CngAuthenticatedEncryptorBase.Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData)
Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.UnprotectCore(byte[] protectedData, bool allowOperationsOnRevokedKeys, out UnprotectStatus status)
Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors, out bool requiresMigration, out bool wasRevoked)
Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.Unprotect(byte[] protectedData)
KlehnPhotography.Site.TokenAuthentication.JwtTokenService.UnprotectToken(string protectedText) in JwtTokenValidator.cs
KlehnPhotography.Site.Service.ServiceBase.CreateClient() in ServiceBase.cs
KlehnPhotography.Site.Service.TagService.GetTags() in TagService.cs
KlehnPhotography.Site.Controllers.PhotosController.Index() in PhotosController.cs
lambda_method(Closure , object )
Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable+Awaiter.GetResult()
Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+TaskOfActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, object controller, object[] arguments)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
System.Runtime.CompilerServices.ValueTaskAwaiter<TResult>.GetResult()
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask<IActionResult> actionResultValueTask)
....

我不知道从哪里开始,因为它在本地工作正常 - 所以调试没有帮助。

我想问题出在网站上,而不是 API(如果我错了,请纠正我),所以这里是我的 Startup.cs 和我的 TokenAuthetication-namespace:

启动.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSession(options =>
                        {
                            options.IdleTimeout = TimeSpan.FromMinutes(30);
                            options.Cookie.HttpOnly = true;
                        });

    SecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("mysupersecret_secretkey!123"));

    services.AddScoped<IDataSerializer<AuthenticationTicket>, TicketSerializer>();

    TokenValidationParameters tokenValidationParameters =
        new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = signingKey,

            ValidateIssuer = true,
            ValidIssuer = "ExampleIssuer",

            ValidateAudience = true,
            ValidAudience = "ExampleAudience",

            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero,
        };

    var serialiser = services.BuildServiceProvider().GetService<IDataSerializer<AuthenticationTicket>>();
    var dataProtector = services.BuildServiceProvider()
                                .GetDataProtector(new[] {"IronSphere.Web.Site-Auth"});

    services.AddAuthentication(options =>
                                {
                                    options.DefaultAuthenticateScheme =
                                        CookieAuthenticationDefaults.AuthenticationScheme;
                                    options.DefaultChallengeScheme =
                                        CookieAuthenticationDefaults.AuthenticationScheme;
                                })
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, 
                options =>
                    {
                        options.Cookie.Name = "access_token";
                        options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
                        options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
                        options.ReturnUrlParameter = "returnUrl";
                        options.TicketDataFormat = new JwtTokenValidator(SecurityAlgorithms.HmacSha256,
                                            tokenValidationParameters, serialiser, dataProtector);
                    });

    services.AddMvc();

    services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

    services.AddTransient(x => x.GetDataProtector(new[] {"IronSphere.Web.Site-Auth"}));

    services.AddTransient<IAccountService, AccountService>();

    services.AddHttpContextAccessor();
    services.AddOptions<AppSettings>();
    services.AddMemoryCache();
    services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
    services.AddTransient(typeof(ISession), serviceProvider =>
    {
        var httpContextAccessor =
            serviceProvider.GetService<IHttpContextAccessor>();
        return httpContextAccessor.HttpContext.Session;
    });
    services.AddTransient(typeof(ISession), serviceProvider =>
                                            {
                                                var httpContextAccessor =
                                                    serviceProvider.GetService<IHttpContextAccessor>();
                                                return httpContextAccessor.HttpContext.Session;
                                            });
    services.AddTransient<IJwtTokenService, JwtTokenService>();

    services.AddDataProtection()
        .SetApplicationName("KlehnPhotography.Site");
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseSession();

    app.UseStaticFiles();
    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(Configuration.GetSection("AppSettings:ImagesPath").Value),
        RequestPath = "/images"
    });

    app.UseFileServer();
    app.UseRouting();

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

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

TokenValidator.cs

public interface IJwtTokenService
{
    string UnprotectToken(string protectedText);
}

public class JwtTokenService : IJwtTokenService
{
    private readonly IDataSerializer<AuthenticationTicket> _ticketSerializer;
    private readonly IDataProtector _dataProtector;

    public JwtTokenService(IDataSerializer<AuthenticationTicket> serializer, IDataProtector protector)
    {
        _ticketSerializer = serializer;
        _dataProtector = protector;
    }

    public string UnprotectToken(string protectedText)
    {
        SecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("mysupersecret_secretkey!123"));

        TokenValidationParameters tokenValidationParameters =
            new TokenValidationParameters
            {
                ValidateIssuerSigningKey =
                    true,
                IssuerSigningKey = signingKey,

                ValidateIssuer = true,
                ValidIssuer = "ExampleIssuer",

                ValidateAudience = true,
                ValidAudience = "ExampleAudience",

                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero,
            };

        JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();

        AuthenticationTicket authTicket =
            _ticketSerializer.Deserialize(_dataProtector.Unprotect(Base64UrlTextEncoder.Decode(protectedText)));

        if (authTicket.Properties == null || !authTicket.Properties.Items.Any())
            return null;

        if (!authTicket.Properties.Items.TryGetValue("jwt", out string embeddedJwt))
            throw new ArgumentException("No JWT was found in the Authentication Ticket");

        handler.ValidateToken(embeddedJwt, tokenValidationParameters, out SecurityToken validToken);

        JwtSecurityToken validJwt;

        if ((validJwt = validToken as JwtSecurityToken) == null)
            throw new ArgumentException("Invalid JWT");

        if (!validJwt.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.Ordinal))
            throw new ArgumentException($"Algorithm must be '{SecurityAlgorithms.HmacSha256}'");


        return embeddedJwt;
    }
}

public class JwtTokenValidator : ISecureDataFormat<AuthenticationTicket>
{
    private readonly string _algorithm;
    private readonly TokenValidationParameters _validationParameters;
    private readonly IDataSerializer<AuthenticationTicket> _ticketSerializer;
    private readonly IDataProtector _dataProtector;

    public JwtTokenValidator(string algorithm, TokenValidationParameters validationParameters,
        IDataSerializer<AuthenticationTicket> ticketSerializer, IDataProtector dataProtector)
    {
        _algorithm = algorithm;
        _validationParameters = validationParameters;
        _ticketSerializer = ticketSerializer;
        _dataProtector = dataProtector;
    }

    public AuthenticationTicket Unprotect(string protectedText) => Unprotect(protectedText, null);

    public AuthenticationTicket Unprotect(string protectedText, string purpose)
    {
        JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
        AuthenticationTicket authTicket;

        try
        {
            authTicket =
                _ticketSerializer.Deserialize(_dataProtector.Unprotect(Base64UrlTextEncoder.Decode(protectedText)));

            if (authTicket.Properties == null || !authTicket.Properties.Items.Any())
                return null;

            if (!authTicket.Properties.Items.TryGetValue("jwt", out string embeddedJwt))
                throw new ArgumentException("No JWT was found in the Authentication Ticket");

            IdentityModelEventSource.ShowPII = true;
            handler.ValidateToken(embeddedJwt, this._validationParameters, out SecurityToken validToken);

            JwtSecurityToken validJwt;

            if ((validJwt = validToken as JwtSecurityToken) == null)
                throw new ArgumentException("Invalid JWT");

            if (!validJwt.Header.Alg.Equals(_algorithm, StringComparison.Ordinal))
                throw new ArgumentException($"Algorithm must be '{_algorithm}'");
        }
        catch (Exception exception)
        {
            return null;
        }

        return authTicket;
    }

    public string Protect(AuthenticationTicket data) => Protect(data, null);

    public string Protect(AuthenticationTicket data, string purpose)
    {
        byte[] array = _ticketSerializer.Serialize(data);
        IDataProtector dataProtector = _dataProtector;

        if (!string.IsNullOrEmpty(purpose))
            dataProtector = dataProtector.CreateProtector(purpose);

        return Base64UrlTextEncoder.Encode(dataProtector.Protect(array));
    }
}

知道有什么问题吗?我还可以分享一个 GitHub 存储库。

更新:

API 和网站都在同一台服务器上运行。现在我添加到 Startup for API 中:

services
            .AddDataProtection(options => options.ApplicationDiscriminator = "SebastianKlehn")
            .PersistKeysToFileSystem(new DirectoryInfo(Configuration.GetSection("AppSettings:KeyStore").Value))
            .SetApplicationName("KlehnPhotography.API");

在网站启动中:

services                
            .AddDataProtection(options => options.ApplicationDiscriminator = "SebastianKlehn")
            .PersistKeysToFileSystem(new DirectoryInfo(Configuration.GetSection("AppSettings:KeyStore").Value))
            .SetApplicationName("KlehnPhotography.Site");

当我没有设置options.ApplicationDiscriminator时,没有生成文件

当我启动应用程序时,我在 KeyStore 的目录中得到一个文件。每次删除此文件并重新加载网站时,都会生成一个具有不同 GUID 的新文件。删除 cookie 并登录后,我得到了这个异常:

CryptographicException:在密钥环中找不到密钥 {45030352-111b-460c-8419-94b48817f170}。

所以我猜,网站总是搜索这个(并且总是相同的)GUID:45030352-111b-460c-8419-94b48817f170即使在 iisreset 之后它也永远不会改变 - 该文件在 KeyStore 文件夹中不存在。

在 API 的日志文件中有:

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[58]
      Creating key {59dc92b2-1932-43d7-bb99-99c2a16e12fb} with creation date 2019-12-25 17:33:17Z, activation date 2019-12-25 17:33:17Z, and expiration date 2020-03-24 17:33:17Z.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {59dc92b2-1932-43d7-bb99-99c2a16e12fb} may be persisted to storage in unencrypted form.
info: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[39]
      Writing data to file 'E:\wwwroot\Keys\key-59dc92b2-1932-43d7-bb99-99c2a16e12fb.xml'.

在网站的日志文件中,没有任何关于 DataProtection 或错误或任何内容的信息。只是正常的事情,比如Now listening on...Application started

标签: c#asp.net-corejwtasp.net-core-webapi

解决方案


CbcAuthenticatedEncryptor在两种情况下引发此特定异常:您的输入参数乱码或您的 HMAC 验证失败。

您的设置不太可能未能通过第一次检查(据我所知,这只是一个健全性检查),因此它必须是您的有效负载确实已更改(我假设您已经排除)或者验证密码密钥是错误的。

调查KeyRingBasedDataProtector.UnprotectCore显示您的有效负载是{ (int)magicHeader || (Guid)keyId || (byte[])encryptorSpecificProtectedPayload }.

在解析之后,keyId控制IAuthenticatedEncryptor我们从哪个实例KeyRingProvider- 在这个阶段必须已经用正确的密钥材料和 IV 初始化。这就是我怀疑您的问题所在 - 因为您Unprotect在不同的机器上运行 - 您无法访问相同的密钥库并KeyRingProvider让您成为新的IAuthenticatedEncryptor(即使您的密钥派生密码相同,我们仍然有IV 可能是随机的——我不会声称知道密钥是如何派生的)。在同一台机器上运行这两个应用程序可能会起作用,因为默认情况下密钥自动发现的工作方式(您似乎依赖它) - 它来自 Windows 注册表或公共文件系统位置(取决于您的操作系统)。

因此,我建议您在服务器之间配置共享密钥存储,如下所示:

services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"));
    .SetApplicationName("KlehnPhotography.Site");

看看它是否有帮助。


推荐阅读