问题
如何使用用户名和密码流将持有者令牌与 ASP.NET 5 一起使用?对于我们的方案,我们希望让用户使用 AJAX 调用注册和登录,而无需使用外部登录。
为此,我们需要有一个授权服务器端点。在以前的 ASP.NET 版本中,我们将执行以下操作,然后登录到ourdomain.com/Token
URL。
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14)
};
但是,在当前版本的 ASP.NET 中,上述内容不起作用。我们一直在努力找出新的方法。例如,GitHub上的aspnet/identity示例配置了Facebook,Google和Twitter身份验证,但似乎没有配置非外部OAuth授权服务器端点,除非这是AddDefaultTokenProviders()
所做的,在这种情况下,我们想知道提供程序的URL是什么。
研究
我们从阅读此处的源代码中了解到,我们可以通过在 Startup
类中调用 IAppBuilder.UseOAuthBearerAuthentication
将"持有者身份验证中间件"添加到 HTTP 管道中。这是一个良好的开端,尽管我们仍然不确定如何设置其令牌终结点。这不起作用:
public void Configure(IApplicationBuilder app)
{
app.UseOAuthBearerAuthentication(options =>
{
options.MetadataAddress = "meta";
});
// if this isn't here, we just get a 404
app.Run(async context =>
{
await context.Response.WriteAsync("Hello World.");
});
}
在去ourdomain.com/meta
我们只是收到我们的hello world页面。
进一步的研究表明,我们也可以使用IAppBuilder.UseOAuthAuthentication
扩展方法,并且它需要一个OAuthAuthenticationOptions
参数。该参数具有 TokenEndpoint
属性。因此,尽管我们不确定我们在做什么,但我们尝试了这个,当然没有奏效。
public void Configure(IApplicationBuilder app)
{
app.UseOAuthAuthentication("What is this?", options =>
{
options.TokenEndpoint = "/token";
options.AuthorizationEndpoint = "/oauth";
options.ClientId = "What is this?";
options.ClientSecret = "What is this?";
options.SignInScheme = "What is this?";
options.AutomaticAuthentication = true;
});
// if this isn't here, we just get a 404
app.Run(async context =>
{
await context.Response.WriteAsync("Hello World.");
});
}
换句话说,在去ourdomain.com/token
,没有错误,只是再次出现我们的hello world页面。
编辑(01/28/2021):AspNet.Security.OpenIdConnect.Server已作为3.0更新的一部分合并到OpenIddict中。要开始使用OpenIddict,请访问 documentation.openiddict.com。
<小时 />好的,让我们回顾一下OWIN/Katana3提供的不同OAuth2中间件(及其各自的IAppBuilder
扩展)以及将移植到 ASP.NET 核心的中间件:
-
app.UseOAuthBearerAuthentication
/OAuthBearerAuthenticationMiddleware
:它的名字不是很明显,但它曾经(现在仍然是,因为它已被移植到 ASP.NET Core)负责验证 OAuth2 服务器中间件颁发的访问令牌。它基本上是 cookie 中间件的令牌对应项,用于保护您的 API。在 ASP.NET Core中,它已经丰富了可选的OpenID Connect功能(它现在能够自动从颁发令牌的OpenID Connect服务器检索签名证书)。
注意:从 ASP.NET Core beta8开始,现在命名为app.UseJwtBearerAuthentication
/JwtBearerAuthenticationMiddleware
。
-
app.UseOAuthAuthorizationServer
/OAuthAuthorizationServerMiddleware
:顾名思义,OAuthAuthorizationServerMiddleware
是一个 OAuth2 授权服务器中间件,用于创建和颁发访问令牌。此中间件不会移植到 ASP.NET 核心:ASP.NET 核心中的 OAuth 授权服务。 -
app.UseOAuthBearerTokens
:这个扩展并不真正对应于中间件,只是app.UseOAuthAuthorizationServer
和app.UseOAuthBearerAuthentication
的包装器。它是 ASP.NET 标识包的一部分,只是在单个调用中配置 OAuth2 授权服务器和用于验证访问令牌的 OAuth2 持有者中间件的便捷方法。它不会移植到 ASP.NET 核心。
ASP.NET Core将提供一个全新的中间件(我很自豪地说我设计了它):
-
app.UseOAuthAuthentication
/OAuthAuthenticationMiddleware
:这个新的中间件是一个通用的OAuth2交互式客户端,其行为与app.UseFacebookAuthentication
或app.UseGoogleAuthentication
完全相同,但几乎支持任何标准的OAuth2提供程序,包括您的提供程序。Google,Facebook和Microsoft提供商都已更新为继承了这个新的基础中间件。
因此,您实际寻找的中间件是 OAuth2 授权服务器中间件,又名 OAuthAuthorizationServerMiddleware
。
虽然它被社区的大部分人视为必不可少的组成部分,但它不会被移植到 ASP.NET Core。
幸运的是,已经有一个直接的替代品:AspNet.Security.OpenIdConnect.Server(https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server)。
该中间件是 OAuth2 授权服务器中间件的高级分支,它随 Katana3 一起提供,但面向 OpenID Connect(它本身基于 OAuth2)。它使用相同的低级方法,提供细粒度控制(通过各种通知),并允许您使用自己的框架(Nancy,ASP.NET Core MVC)来提供授权页面,就像使用 OAuth2 服务器中间件一样。配置它很简单:
ASP.NET 核心 1.x:
// Add a new middleware validating access tokens issued by the server.
app.UseOAuthValidation();
// Add a new middleware issuing tokens.
app.UseOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/connect/token";
// Create your own `OpenIdConnectServerProvider` and override
// ValidateTokenRequest/HandleTokenRequest to support the resource
// owner password flow exactly like you did with the OAuth2 middleware.
options.Provider = new AuthorizationProvider();
});
ASP.NET 酷睿2.x:
// Add a new middleware validating access tokens issued by the server.
services.AddAuthentication()
.AddOAuthValidation()
// Add a new middleware issuing tokens.
.AddOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/connect/token";
// Create your own `OpenIdConnectServerProvider` and override
// ValidateTokenRequest/HandleTokenRequest to support the resource
// owner password flow exactly like you did with the OAuth2 middleware.
options.Provider = new AuthorizationProvider();
});
有一个OWIN/Katana3版本,以及一个同时支持.NET Desktop和.NET Core的 ASP.NET Core版本。
不要犹豫,尝试给邮递员样本,了解它是如何工作的。我建议阅读相关的博客文章,其中解释了如何实现资源所有者密码流。
如果您仍然需要帮助,请随时 ping 我。祝你好运!
在@Pinpoint的帮助下,我们已经将答案的基本要素连接在一起。它显示了组件如何连接在一起,而不是一个完整的解决方案。
小提琴手演示
通过我们基本的项目设置,我们能够在 Fiddler 中提出以下请求和响应。
请求
POST http://localhost:50000/connect/token HTTP/1.1
User-Agent: Fiddler
Host: localhost:50000
Content-Length: 61
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=my_username&password=my_password
响应
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 1687
Content-Type: application/json;charset=UTF-8
Expires: -1
X-Powered-By: ASP.NET
Date: Tue, 16 Jun 2015 01:24:42 GMT
{
"access_token" : "eyJ0eXAiOi ... 5UVACg",
"expires_in" : 3600,
"token_type" : "bearer"
}
响应提供了一个持有者令牌,我们可以使用该令牌来访问应用程序的安全部分。
项目结构
这是我们在Visual Studio中的项目结构。我们必须将其Properties
> Debug
> Port
设置为 50000
,以便它充当我们配置的身份服务器。以下是相关文件:
ResourceOwnerPasswordFlow
Providers
AuthorizationProvider.cs
project.json
Startup.cs
启动.cs
为了便于阅读,我将Startup
类分为两个部分。
Startup.ConfigureServices
对于最基本的,我们只需要 AddAuthentication()
.
public partial class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication();
}
}
启动.配置
public partial class Startup
{
public void Configure(IApplicationBuilder app)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
// Add a new middleware validating access tokens issued by the server.
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Audience = "resource_server_1",
Authority = "http://localhost:50000/",
RequireHttpsMetadata = false
});
// Add a new middleware issuing tokens.
app.UseOpenIdConnectServer(options =>
{
// Disable the HTTPS requirement.
options.AllowInsecureHttp = true;
// Enable the token endpoint.
options.TokenEndpointPath = "/connect/token";
options.Provider = new AuthorizationProvider();
// Force the OpenID Connect server middleware to use JWT
// instead of the default opaque/encrypted format.
options.AccessTokenHandler = new JwtSecurityTokenHandler
{
InboundClaimTypeMap = new Dictionary<string, string>(),
OutboundClaimTypeMap = new Dictionary<string, string>()
};
// Register an ephemeral signing key, used to protect the JWT tokens.
// On production, you'd likely prefer using a signing certificate.
options.SigningCredentials.AddEphemeralKey();
});
app.UseMvc();
app.Run(async context =>
{
await context.Response.WriteAsync("Hello World!");
});
}
}
授权提供程序.cs
public sealed class AuthorizationProvider : OpenIdConnectServerProvider
{
public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
{
// Reject the token requests that don't use
// grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() &&
!context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.FromResult(0);
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private), call Skip()
// to inform the server that the request should be accepted without
// enforcing client authentication.
context.Skip();
return Task.FromResult(0);
}
public override Task HandleTokenRequest(HandleTokenRequestContext context)
{
// Only handle grant_type=password token requests and let the
// OpenID Connect server middleware handle the other grant types.
if (context.Request.IsPasswordGrantType())
{
// Validate the credentials here (e.g using ASP.NET Core Identity).
// You can call Reject() with an error code/description to reject
// the request and return a message to the caller.
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique identifier]");
// By default, claims are not serialized in the access and identity tokens.
// Use the overload taking a "destinations" parameter to make sure
// your claims are correctly serialized in the appropriate tokens.
identity.AddClaim("urn:customclaim", "value",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Call SetResources with the list of resource servers
// the access token should be issued for.
ticket.SetResources("resource_server_1");
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes("profile", "offline_access");
context.Validate(ticket);
}
return Task.FromResult(0);
}
}
项目.json
{
"dependencies": {
"AspNet.Security.OpenIdConnect.Server": "1.0.0",
"Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.0",
}
// other code omitted
}