如何手动解密ASP.NET Core身份验证cookie?

57

让我们考虑一个常见的 ASP.NET Core 场景。首先,我们添加中间件:

public void Configure(IApplicationBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions()
    {
        AuthenticationScheme = "MyCookie",
        CookieName = "MyCookie",
        LoginPath = new PathString("/Home/Login/"),
        AccessDeniedPath = new PathString("/Home/AccessDenied/"),
        AutomaticAuthenticate = true,
        AutomaticChallenge = true
    });
    //...
}

然后序列化一个主体:

await HttpContext.Authentication.SignInAsync("MyCookie", principal);

这两个调用之后,将在客户端存储一个加密的cookie。你可以在任何浏览器开发工具中看到cookie(在我的情况下是分块的):

chunked encrypted cookie generated by ASP.NET

从应用程序代码中处理cookie不是问题(也不是问题)。

我的问题是:如何在应用程序外解密cookie?我猜需要一个私钥,怎么获取它?

我查看了文档,只发现了普通的单词:

这将创建一个加密的cookie并将其添加到当前响应中。在配置期间指定的AuthenticationScheme在调用SignInAsync时也必须使用。

底层使用的加密是ASP.NET的数据保护系统。如果您在多台机器上托管、负载平衡或使用Web Farm,则需要配置数据保护以使用相同的密钥环和应用程序标识符。

那么,是否可能解密身份验证cookie?如果可以,如何解密?

更新#1:基于Ron C的很好的答案和评论,我最终得到了这段代码:

public class Startup
{
    //constructor is omitted...
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDataProtection().PersistKeysToFileSystem(
            new DirectoryInfo(@"C:\temp-keys\"));

        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseCookieAuthentication(new CookieAuthenticationOptions()
        {
            AuthenticationScheme = "MyCookie",
            CookieName = "MyCookie",
            LoginPath = new PathString("/Home/Index/"),
            AccessDeniedPath = new PathString("/Home/AccessDenied/"),
            AutomaticAuthenticate = true,
            AutomaticChallenge = true
        });

        app.UseStaticFiles();
        app.UseMvcWithDefaultRoute();
    }
}

public class HomeController : Controller
{
    public async Task<IActionResult> Index()
    {
        await HttpContext.Authentication.SignInAsync("MyCookie", new ClaimsPrincipal());

        return View();
    }

    public IActionResult DecryptCookie()
    {
        var provider = DataProtectionProvider.Create(new DirectoryInfo(@"C:\temp-keys\"));

        string cookieValue = HttpContext.Request.Cookies["MyCookie"];

        var dataProtector = provider.CreateProtector(
            typeof(CookieAuthenticationMiddleware).FullName, "MyCookie", "v2");

        UTF8Encoding specialUtf8Encoding = new UTF8Encoding(false, true);
        byte[] protectedBytes = Base64UrlTextEncoder.Decode(cookieValue);
        byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
        string plainText = specialUtf8Encoding.GetString(plainBytes);

        return Content(plainText);
    }
}

很不幸,这段代码在调用Unprotect方法时总是产生异常:

Microsoft.AspNetCore.DataProtection.dll中的CryptographicException异常: 附加信息:有效载荷无效。

我在几台机器上测试了这个代码的不同变化,但没有得到积极的结果。可能是我的错误,但是错在哪里呢?

更新 #2:我的错误在于DataProtectionProvider没有在UseCookieAuthentication中设置。再次感谢@RonC。


3
请问您是否能够更新您的答案并提供正确的代码? - Pratap Singh Mehra
1
被接受的答案是由@RonC提供的,不是我。他的代码是正确的。 - Ilya Chumakov
5个回答

58

无需密钥解密身份验证Cookie

值得注意的是,您不需要访问密钥即可解密身份验证Cookie。您只需要使用正确的`IDataProtector`并创建正确的目的参数和子目的参数即可。
基于`CookieAuthenticationMiddleware`源代码https://github.com/aspnet/Security/blob/rel/1.1.1/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationMiddleware.cs#L4,看起来您需要传递的目的是`typeof(CookieAuthenticationMiddleware)`。由于他们正在向`IDataProtector`传递其他参数,因此您需要匹配它们。因此,这行代码应该为您获取一个可以用于解密身份验证Cookie的`IDataProtector`:
var dataProtector = provider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, Options.AuthenticationScheme, "v2");

请注意,这里的provider是从DI容器中获取的IDataProtectionProvider,而Options.AuthenticationScheme在这种情况下只是"MyCookie",因为它是在startup.cs文件的Configure方法中设置的。
以下是解密身份验证cookie的两种不同方式的示例操作方法:
public IActionResult DecryptCookie() {

    //Get the encrypted cookie value
    string cookieValue = HttpContext.Request.Cookies["MyCookie"];

    //Get a data protector to use with either approach
    var dataProtector = provider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, "MyCookie", "v2");


    //Get the decrypted cookie as plain text
    UTF8Encoding specialUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
    byte[] protectedBytes = Base64UrlTextEncoder.Decode(cookieValue);
    byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
    string plainText = specialUtf8Encoding.GetString(plainBytes);


    //Get the decrypted cookie as a Authentication Ticket
    TicketDataFormat ticketDataFormat = new TicketDataFormat(dataProtector);
    AuthenticationTicket ticket = ticketDataFormat.Unprotect(cookieValue);

    return View();
}

这种方法使用一个名为providerIDataProtectionProvider,它是通过构造函数注入的。


将身份验证Cookie解密并将密钥持久化到目录中


如果您想在应用程序之间共享Cookie,则可以决定将数据保护密钥持久化到目录中。这可以通过在startup.cs文件的`ConfigureServices`方法中添加以下内容来完成:
services.AddDataProtection().PersistKeysToFileSystem(
        new DirectoryInfo(@"C:\temp-keys\")); 

要小心,因为密钥没有加密,所以你需要自己保护它们!如果绝对必要,只有在持久化密钥到目录中时才这样做(或者你只是想了解系统的工作原理)。你还需要指定一个使用这些密钥的cookie DataProtectionProvider。可以通过在startup.cs类的Configure方法中使用UseCookieAuthentication配置来实现:

app.UseCookieAuthentication(new CookieAuthenticationOptions() {
        DataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo(@"C:\temp-keys\")),
        AuthenticationScheme = "MyCookie",
        CookieName = "MyCookie",
        LoginPath = new PathString("/Home/Login"),
        AccessDeniedPath = new PathString("/Home/AccessDenied"),
        AutomaticAuthenticate = true,
        AutomaticChallenge = true
    });

完成了这个配置后,您现在可以使用以下代码解密身份验证 cookie:

 public IActionResult DecryptCookie() {
        ViewData["Message"] = "This is the decrypt page";
        var user = HttpContext.User;        //User will be set to the ClaimsPrincipal

        //Get the encrypted cookie value
        string cookieValue = HttpContext.Request.Cookies["MyCookie"];
        

        var provider = DataProtectionProvider.Create(new DirectoryInfo(@"C:\temp-keys\"));

        //Get a data protector to use with either approach
        var dataProtector = provider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, "MyCookie", "v2");


        //Get the decrypted cookie as plain text
        UTF8Encoding specialUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
        byte[] protectedBytes = Base64UrlTextEncoder.Decode(cookieValue);
        byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
        string plainText = specialUtf8Encoding.GetString(plainBytes);


        //Get teh decrypted cookies as a Authentication Ticket
        TicketDataFormat ticketDataFormat = new TicketDataFormat(dataProtector);
        AuthenticationTicket ticket = ticketDataFormat.Unprotect(cookieValue);

        return View();
    }

您可以在此处了解更多关于后一种情况的信息:https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/compatibility/cookie-sharing


9
在Core 2.0中,CookieAuthenticationMiddleware类已被替换,但仍使用“Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware”的字符串值。https://github.com/aspnet/Security/blob/a7bf561b1c76aaee3d68c7448d780bc925ee283e/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs#L38 - Cirem
1
我在使用.NET Core 2.0和分块cookie方面仍然存在问题。有什么想法吗?我将cookie值连接在一起,并在DataProtectionProvider中使用cookie名称。谢谢。 - Cirem
1
如果在添加数据保护服务时指定了应用程序名称services.AddDataProtection().PersistKeysToFileSystem(new System.IO.DirectoryInfo(@"C:\temp-keys\")).SetApplicationName("MyApplicationName");,请确保在 DataProtectionProvider.Create 调用中指定相同的名称:var dataProtection = DataProtectionProvider.Create(new DirectoryInfo(@"C:\temp-keys\"), cfg => cfg.SetApplicationName("MyApplicationName")); - James H
2
@RonC 感谢您的回复。我可能读错了,但是这个页面可能表明这是可能的吗?https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-3.1#share-authentication-cookies-between-aspnet-4x-and-aspnet-core-apps - Aaron Hudon
1
请问您能否更新第一个代码片段,展示一下如何声明/初始化“provider”? - David Klempfner
显示剩余20条评论

22

在 ASP.NET Core 应用程序内,您可以直接使用 CookieAuthenticationOptions.TicketDataFormat.Unprotect(cookieValue)

这里是我编写的一个简单的静态方法:

public static AuthenticationTicket DecryptAuthCookie(HttpContext httpContext)
{
    // ONE - grab the CookieAuthenticationOptions instance
    var opt = httpContext.RequestServices
        .GetRequiredService<IOptionsMonitor<CookieAuthenticationOptions>>()
        .Get(CookieAuthenticationDefaults.AuthenticationScheme); //or use .Get("Cookies")

    // TWO - Get the encrypted cookie value
    var cookie = opt.CookieManager.GetRequestCookie(httpContext, opt.Cookie.Name);

    // THREE - decrypt it
    return opt.TicketDataFormat.Unprotect(cookie);
}

在 .NET 5 和 .NET 6 中正常工作。

我添加这个答案作为参考,因为如果你搜索如何手动解密ASP.NET身份验证cookie,这个问题会出现在每个搜索引擎上。


3
对于ASP.NET Core Identity,必须使用.Get("Identity.Application")而不是.Get(CookieAuthenticationDefaults.AuthenticationScheme) - Sazug
1
@alex 我在 OAuth2 地狱的第九层,你的解决方案起了作用。非常感谢你。 - caliche2000
1
这对我来说在前两步中是有效的。第三步后,我得到了NULL,这让我感到意外。我愿意听取建议。 - Don Rolling
@AlexfromJitbit 我相当确定我已经解决了这个问题,但自那以后我换了工作,无法查看代码并确定是什么解决了它。不过还是感谢你的跟进! - Don Rolling
你能否更新一下如何针对 Identity.ExternalC1Identity.ExternalC2 cookies 进行操作的答案? - David Klempfner
显示剩余4条评论

12

以下是用于 .NET Core 2 的一个助手方法,用于从 cookie 中获取声明:

private IEnumerable<Claim> GetClaimFromCookie(HttpContext httpContext, string cookieName, string cookieSchema)
{
    // Get the encrypted cookie value
    var opt = httpContext.RequestServices.GetRequiredService<IOptionsMonitor<CookieAuthenticationOptions>>();
    var cookie = opt.CurrentValue.CookieManager.GetRequestCookie(httpContext, cookieName);

    // Decrypt if found
    if (!string.IsNullOrEmpty(cookie))
    {
        var dataProtector = opt.CurrentValue.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", cookieSchema, "v2");
        
        var ticketDataFormat = new TicketDataFormat(dataProtector);
        var ticket = ticketDataFormat.Unprotect(cookie);
        return ticket.Principal.Claims;
    }
    return null;
}

正如 @Cirem 所指出的那样,创建保护程序的不可靠方式恰恰是 Microsoft 的做法(请参阅 此处的代码)。因此,在未来的版本中可能会发生变化。


2
它对我非常有效,似乎比被接受的答案更容易使用。非常感谢。 - AFract
1
这显然是由于 cookie 模式引起的。那么什么是 cookie 模式呢? - liang
1
@liang,这很可能是由于使用了更新版本的.NET Core。微软在v2.2中更改了我链接中提到的代码,并在v3中进行了完全的重新设计。 - Alex Klaus

10

ASP.NET Core 2.2的另一种变体:

var cookieManager = new ChunkingCookieManager();
var cookie = cookieManager.GetRequestCookie(HttpContext, ".AspNetCore.Identity.Application");

var dataProtector = dataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", "Identity.Application", "v2");

//Get the decrypted cookie as plain text
UTF8Encoding specialUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
byte[] protectedBytes = Base64UrlTextEncoder.Decode(cookie);
byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
string plainText = specialUtf8Encoding.GetString(plainBytes);


//Get teh decrypted cookies as a Authentication Ticket
TicketDataFormat ticketDataFormat = new TicketDataFormat(dataProtector);
AuthenticationTicket ticket = ticketDataFormat.Unprotect(cookie);

1
dataProtectionProvider 是从哪里来的? - Pancake
2
@Pancake可以在构造函数中使用DI进行注入,需要使用IDataProtectionProvider dataProtectionProvider。您还需要将其添加到services中:https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-3.1#persistkeystofilesystem - Adiqq

3

我刚刚在Classic ASP.net (4.6.1)中使这个工作了。请注意以下必需的安装:

  • Microsoft.Owin.Security.Interop(将与一堆依赖项一起提供 - 请注意,由于一个异常,我使用了版本3.0.1,但可能没有必要)。
  • Microsfot.AspNetCore.DataProtection(将与一堆依赖项一起提供)
  • 4.6.1框架的标准Web内容

框架定义了以下常量:

  • PROVIDER_NAME = "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware"
  • SCHEME_NAME = "Identity.Application"
  • COOKIE_NAME = ".AspNetCore.Identity.Application"(可以自定义)

以下常量是特定于配置的,但必须在应用程序之间相同

  • APP_NAME = "Auth.Test.App"
  • SHARED_KEY_DIR = "C:\\app-keyring"

步骤:

本篇文章在设置双方的过程中非常有帮助,但尤其是在正确配置.Net Core方面。因此,我们将把这留给读者作为练习。

一旦您设置了这些内容,在4.6.1解密端中,以下代码将在(例如).Net Core 3.0中生成ClaimsIdentity

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Owin.Security.Interop;
using System.IO;
using System.Security.Claims;
using System.Web;

public static ClaimsIdentity GetClaimsIdentity(HttpContext context)
{
    //Get the encrypted cookie value
    var cookie = context.Request.Cookies[Constants.COOKIE_NAME];
    if (cookie == null) {
        return null;
    }
    var cookieValue = cookie.Value;

    //Get a data protector to use with either approach
    var keysDir = new DirectoryInfo(Constants.SHARED_KEY_DIR);
    if (!keysDir.Exists) { keysDir.Create(); }

    var provider = DataProtectionProvider.Create(keysDir,
        options => options.SetApplicationName(Constants.APP_NAME));
    var dataProtector = provider.CreateProtector(Constants.PROVIDER_NAME, Constants.SCHEME_NAME, "v2");

    //Get the decrypted cookie as a Authentication Ticket
    var shim = new DataProtectorShim(dataProtector);
    var ticketDataFormat = new AspNetTicketDataFormat(shim);
    var ticket = ticketDataFormat.Unprotect(cookieValue);

    return ticket.Identity;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接