【翻译】asp.net core2.0中的jwt认证

原文地址:https://developer.okta.com/blog/2018/03/23/token-authentication-aspnetcore-complete-guide

token认证在最近几年正在成为一个流行的主题,特别是随着移动应用和js应用不断的获得关注。像OAuth 2.0和OpenID Connect这样的基于令牌的标准的广泛采用,已经为令牌引入了更多的开发人员,但是最佳实践并不总是清晰的。

我(作者)在asp.net croe 1.0的时候就开始使用,并且花费了相当长的时间在其中。asp.net core2.0在使用和校验token方面提供了大量的支持,多亏有一个内建的JWT校验中间件。然而,asp.net 4中的关于token生成的代码在asp.net core 2.0中却不见踪影。在asp.net core版本的早期,完整的token认证过程是一件令人困惑的事情。

现在asp.net core 2.1(2.2都发布了)已经稳定,那些令人困惑的事情也得到解决。在这篇文章中,我会检查关于token认证的两个方面的最佳实践:token的校验和token的生成。

令牌身份验证是将令牌(有时称为访问令牌或承载令牌)附加到HTTP请求以进行身份验证的过程。它通常与服务于移动或SPA (JavaScript)客户端的API一起使用。

到达API的每一个请求都会被检查。如果一个有效的token被发现了,这个请求就是被允许的,如果没有token被发现或者这个token是无效的,那么请求会被一个401未授权响应给拒绝掉。

token认证经常会在OAutho2.0和OpenID Connect这些协议中使用。如果你想看到这些协议是如何工作的,可以查看这里

在ASP.NET CORE中校验token

在asp.net core的api中添加token认证是一件非常容易的事情,这要多亏了JwtBearerAuthentication这个中间件(在框架中,内建的)。如果你正在消费一个由OpenID Connect服务器创建的token,那么关于配置的过程是非常容易的。

在你的Startup类中的ConfigurationServices方法中的任意地方配置你的中间件,并使用来自授权服务器的值对其进行配置:

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

然后,在Configure方法中的UseMvc方法的上面,添加一句:

app.UseAuthentication();

第二步中UseAuthentication()是非常容易忘记的。如果经过身份验证的调用不能正常工作,请确保在正确的位置添加了这一行(UseMvc的上面)。

JwtBearer中间件会在到达的请求的请求头中查找token(JSON Web Tokens,JWT)。如果一个有效的token被发现的话,请求就会被授权。然后你将[Authorize]特性添加到你想要保护的controller或者路由上:

[Route("/api/protected")
[Authorize]
public string Protected()
{
    return "Only if you have a valid token!";
}

你可能会感觉很奇怪:配置的服务中只有Aurhority和Audience这两个参数被指定,JwtBearer中间件是如何校验传入的token的呢(这个token在request的Authorization header中)。

自动授权服务器元数据

当JwtBearer中间件第一次处理到达的请求,他会试着从授权服务器(也叫做authority或issuer,就是上面代码片段中配置的Authority)中检查一些元数据。这些元数据,或者在OpenID Connect的术语中叫做发现文档(discovery document)的,包含了public key和其他需要校验token的信息。(如果你好奇元数据是什么样子的,这里有一个OpenID Connect的发现文档,就是指的元数据。

如果JwtBearer中间件发现了这个元数据的文档,他(JwtBearer)会自动配置校验(通过配置TokenValidationParameters这个类型的属性),真几把棒!

如果没有发现这个元数据文档,你会得到一个错误:

System.IO.IOException: IDX10804: Unable to retrieve document from: "{yourAuthorizationServerAddress}".
System.Net.Http.HttpRequestException: Response status code does not indicate success: 404 (Not Found).

如果你的授权服务器没有发布这个元数据文档,或者你想要自己配置TokenValidationParameters,你可以在中间件的配置中手动的配置他们。

指定TokenValidationParameters

如果你想要细粒度的控制token的校验过程,可以使用JwtBearer中提供的配置:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        // Clock skew compensates for server time drift.
        // We recommend 5 minutes or less:
        ClockSkew = TimeSpan.FromMinutes(5),
        // Specify the key used to sign the token:
        IssuerSigningKey = signingKey,
        RequireSignedTokens = true,
        // Ensure the token hasn't expired:
        RequireExpirationTime = true,
        ValidateLifetime = true,
        // Ensure the token audience matches our audience value (default true):
        ValidateAudience = true,
        ValidAudience = "api://default",
        // Ensure the token was issued by a trusted authorization server (default true):
        ValidateIssuer = true,
        ValidIssuer = "https://{yourOktaDomain}/oauth2/default"
    };
});

TokenValidationParamters中最常用的选项是issuer、audience和clock skew。同时你需要提供提供token签名的key(密钥),这些key看起来会有所不同,取决于你使用的是对称加密还是非对称加密。

理解对称签名和非对称签名

授权服务器生成的token会使用对称或非对称的签名。如果你的授权服务器发布了一个元数据文档(discovery document),在文档中会包含关于密钥的信息(是一个public key),所有你通常不需要关心它是如何进行的。

然而,如果你要自己配置中间件,或者要手动的校验token,你需要理解你的token是如何被签名的。这两种(对称和非对称)的签名之间有什么不同的地方?

对称签名/对称key

一个对称key,也叫做shared key或者shared secret,是一个在API和授权服务器中同时保存着的、保密的值(例如密码)。并且授权服务器颁发token。授权服务器使用shared key对token进行签名,同时API使用同样的key对token进行校验。

如果你有一个shared key,很容易在JwaBearer中间件中使用:

// For example only! Don't store your shared keys as strings in code.
// Use environment variables or the .NET Secret Manager instead.
var sharedKey = new SymmetricSecurityKey(
    Encoding.UTF8.GetBytes("mysupers3cr3tsharedkey!"));

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        // Specify the key used to sign the token:
        IssuerSigningKey = sharedKey,
        RequireSignedTokens = true,
        // Other options...
    };
});

你要确保key被安全的保存,向上面代码中那样的保存你的key不是一个好主意。应该将它保存到环境变量或者使用.NET Secret Manager. ASP.NET Core configuration model 会很容易的将这个工作完成:

var sharedKey = new SymmetricSecurityKey(
    Encoding.UTF8.GetBytes(Configuration["SigningKey"]);

同样,不要在前端代码中存储共享密钥或将其公开给浏览器。它必须保存在服务器上。

非对称签名/非对称key

使用非对称签名的话,你不需要在你的服务器上面保存一个密钥(private key)。相反,一个public/private key(成对出现的)会被使用:授权服务器使用private key对token进行签名,并且发布一个public key,public key用来校验token。

通常情况下,public key的信息会自动的从元数据文档(discovery document)中检查和获取(就像上面那个链接展示的discovery document)。如果你要手动的指定他,你需要从授权服务器获取关键参数并创建一个SecurityKey对象:

// Manually specify a public (asymmetric) key published as a JWK:
var publicJwk = new JsonWebKey
{
    KeyId = "(some key ID)",
    Alg = "RS256",
    E = "AQAB",
    N = "(a long string)",
    Kty = "RSA",
    Use = "sig"
};

多数场景下,public key可以从授权服务器的JSON Web Key集(JWKS)中获取到。授权服务器可能会定期的旋转(rotate)这个key,所以,你需要定期的检查更新的key(从授权服务器中)。但是如果你让JwtBearer中间件自动的配置(通过discovery document),这一切都是自动进行的!

asp.net core中手动校验token

在一些情况下,你可能想要不通过JwtBearer中间件来校验token。使用这个中间件是首选,因为它可以很好的插入到asp.net core的授权系统中。

如果你真的想要手动的校验JWT,你可以使用System.IdentityModel.Tokens.Jwt 包中的JwtSecurityTokenHandler。它使用同样的TokenValidationParamters类来指定校验选项:

private static JwtSecurityToken ValidateAndDecode(string jwt, IEnumerable<SecurityKey> signingKeys)
{
    var validationParameters = new TokenValidationParameters
    {
        // Clock skew compensates for server time drift.
        // We recommend 5 minutes or less:
        ClockSkew = TimeSpan.FromMinutes(5),
        // Specify the key used to sign the token:
        IssuerSigningKeys = signingKeys,
        RequireSignedTokens = true,
        // Ensure the token hasn't expired:
        RequireExpirationTime = true,
        ValidateLifetime = true,
        // Ensure the token audience matches our audience value (default true):
        ValidateAudience = true,
        ValidAudience = "api://default",
        // Ensure the token was issued by a trusted authorization server (default true):
        ValidateIssuer = true,
        ValidIssuer = "https://{yourOktaDomain}/oauth2/default"
    };

    try
    {
        var claimsPrincipal = new JwtSecurityTokenHandler()
            .ValidateToken(jwt, validationParameters, out var rawValidatedToken);

        return (JwtSecurityToken)rawValidatedToken;
        // Or, you can return the ClaimsPrincipal
        // (which has the JWT properties automatically mapped to .NET claims)
    }
    catch (SecurityTokenValidationException stvex)
    {
        // The token failed validation!
        // TODO: Log it or display an error.
        throw new Exception($"Token failed validation: {stvex.Message}");
    }
    catch (ArgumentException argex)
    {
        // The token was not well-formed or was invalid for some other reason.
        // TODO: Log it or display an error.
        throw new Exception($"Token was invalid: {argex.Message}");
    }
}

如果你的授权服务器发布了一个元数据文档,你可以通过 Microsoft.IdentityModel.Protocols.OpenIdConnect 包中的OpenIdConnectConfigurationRetriever类来对这个文档中的内容进行检索,这个过程会自动的获取签名key:

var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
    // .well-known/oauth-authorization-server or .well-known/openid-configuration
    "{yourAuthorizationServerAddress}/.well-known/openid-configuration",
    new OpenIdConnectConfigurationRetriever(),
    new HttpDocumentRetriever());

var discoveryDocument = await configurationManager.GetConfigurationAsync();
var signingKeys = discoveryDocument.SigningKeys;

它负责令牌身份验证的验证端,但是如何生成令牌本身呢?

asp.net core中生成用于认证的token

回首asp.net 4.5的那段日子,UseOAuthAuthorizationServer中间件给你带来一个可以轻易生成token的端点。然后,asp.net core团队决定不要引入这个组件( decided not to bring it to ASP.NET Core,),这意味着你需要其他的东西来达成这个功能。也就是说你需要找到一个或者自己构建一个能够生成token的授权服务器。

通常有以下两种方式:

① 使用一个云服务像Azure AD B2C 或者 Okta

② 你自己构建或者配置一个

Hosted Authorization Server with Okta

用一个宿主授权服务器来生成token是最简单的。通过在上面注册一个账号然后根据网站的提示一步一步个进行下去之后,在你自己的应用中配置是非常简单的:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
    options.Authority = "https://{yourOktaDomain}/oauth2/default";
    options.Audience = "api://default";
});

还有其他的一些:

OpenIddict

OpenIddict is an easy-to-configure authorization server that works nicely with ASP.NET Core Identity and Entity Framework Core. It plugs right into the ASP.NET Core middleware pipeline and is easy to configure.

OpenIddict is a great choice if you’re already using ASP.NET Core Identity and want to generate tokens for your users. You can follow Mike Rousos’ in-depth tutorial on the MSDN blog to set it up and configure it in your application.

IdentityServer4

Thinktecture’s open-source IdentityServer project has been around for a long time, and it got a major update for .NET Core with IdentityServer4. Of the three packages discussed here, it’s the most powerful and flexible.

IdentityServer is a good choice when you want to roll your own full-fledged OpenID Connect authorization server that can handle complex use cases like federation and single sign-on. Depending on your use case, configuring IdentityServer4 can be a little complicated. Fortunately, the official documentation covers many common scenarios.

Token Authentication Can Be Complex!

I hope this article helps it feel a little less confusing. The ASP.NET Core team has done a great job of making it easy to add token authentication to your ASP.NET Core API, and options like OpenIddict and Okta make it easy to spin up an authorization server that generates tokens for your clients.

Here are some more resources if you want to keep learning: