如何在客户端获取 DotNetOpenAuth.OAuth2 返回的错误消息?
Posted
技术标签:
【中文标题】如何在客户端获取 DotNetOpenAuth.OAuth2 返回的错误消息?【英文标题】:How to get error message returned by DotNetOpenAuth.OAuth2 on client side? 【发布时间】:2014-09-21 20:10:24 【问题描述】:我正在使用ExchangeUserCredentialForToken
函数从授权服务器获取令牌。当我的用户存在于我的数据库中时,它工作正常,但是当凭据不正确时,我想向客户端发回一条消息。我正在使用以下 2 行代码来设置错误消息:
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Rejected();
但在客户端我只收到协议错误(错误 400)。您能帮我如何在授权服务器上获取服务器端设置的错误消息吗?
来自授权服务器的完整应用配置:
using Constants;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Infrastructure;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using AuthorizationServer.Entities;
using AuthorizationServer.Entities.Infrastructure.Abstract;
using AuthorizationServer.Entities.Infrastructure.Concrete;
namespace AuthorizationServer
public partial class Startup
private IEmployeeRepository Repository;
public void ConfigureAuth(IAppBuilder app)
//instanciate the repository
Repository = new EmployeeRepository();
// Enable Application Sign In Cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
AuthenticationType = "Application",
AuthenticationMode = AuthenticationMode.Passive,
LoginPath = new PathString(Paths.LoginPath),
LogoutPath = new PathString(Paths.LogoutPath),
);
// Enable External Sign In Cookie
app.SetDefaultSignInAsAuthenticationType("External");
app.UseCookieAuthentication(new CookieAuthenticationOptions
AuthenticationType = "External",
AuthenticationMode = AuthenticationMode.Passive,
CookieName = CookieAuthenticationDefaults.CookiePrefix + "External",
ExpireTimeSpan = TimeSpan.FromMinutes(5),
);
// Enable google authentication
app.UseGoogleAuthentication();
// Setup Authorization Server
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
AuthorizeEndpointPath = new PathString(Paths.AuthorizePath),
TokenEndpointPath = new PathString(Paths.TokenPath),
ApplicationCanDisplayErrors = true,
#if DEBUG
AllowInsecureHttp = true,
#endif
// Authorization server provider which controls the lifecycle of Authorization Server
Provider = new OAuthAuthorizationServerProvider
OnValidateClientRedirectUri = ValidateClientRedirectUri,
OnValidateClientAuthentication = ValidateClientAuthentication,
OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials,
OnGrantClientCredentials = GrantClientCredetails
,
// Authorization code provider which creates and receives authorization code
AuthorizationCodeProvider = new AuthenticationTokenProvider
OnCreate = CreateAuthenticationCode,
OnReceive = ReceiveAuthenticationCode,
,
// Refresh token provider which creates and receives referesh token
RefreshTokenProvider = new AuthenticationTokenProvider
OnCreate = CreateRefreshToken,
OnReceive = ReceiveRefreshToken,
);
// indicate our intent to use bearer authentication
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
AuthenticationType = "Bearer",
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active
);
private Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
if (context.ClientId == Clients.Client1.Id)
context.Validated(Clients.Client1.RedirectUrl);
else if (context.ClientId == Clients.Client2.Id)
context.Validated(Clients.Client2.RedirectUrl);
return Task.FromResult(0);
private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
string clientname;
string clientpassword;
if (context.TryGetBasicCredentials(out clientname, out clientpassword) ||
context.TryGetFormCredentials(out clientname, out clientpassword))
employee Employee = Repository.GetEmployee(clientname, clientpassword);
if (Employee != null)
context.Validated();
else
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Rejected();
return Task.FromResult(0);
private Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
var identity = new ClaimsIdentity(new GenericIdentity(context.UserName, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
context.Validated(identity);
return Task.FromResult(0);
private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context)
var identity = new ClaimsIdentity(new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
context.Validated(identity);
return Task.FromResult(0);
private readonly ConcurrentDictionary<string, string> _authenticationCodes =
new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
_authenticationCodes[context.Token] = context.SerializeTicket();
private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
string value;
if (_authenticationCodes.TryRemove(context.Token, out value))
context.DeserializeTicket(value);
private void CreateRefreshToken(AuthenticationTokenCreateContext context)
context.SetToken(context.SerializeTicket());
private void ReceiveRefreshToken(AuthenticationTokenReceiveContext context)
context.DeserializeTicket(context.Token);
【问题讨论】:
你找到答案了吗? 【参考方案1】:经过数小时的网络搜索和阅读 blob 以及 owin 文档,我找到了一种方法,可以在登录尝试失败时返回 401。
我意识到在下面添加标头有点麻烦,但我找不到任何方法来读取 IOwinContext.Response.Body 流以查找错误消息。
首先,在OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials
中我使用了SetError()
并在响应中添加了Headers
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Response.Headers.Add("AuthorizationResponse", new[] "Failed" );
现在,您可以区分身份验证请求失败的 400 错误和其他原因导致的 400 错误。
下一步是创建一个继承OwinMiddleware
的类。此类检查传出响应,如果 StatusCode == 400
和上面的 Header 存在,它将 StatucCode 更改为 401。
public class InvalidAuthenticationMiddleware : OwinMiddleware
public InvalidAuthenticationMiddleware(OwinMiddleware next)
: base(next)
public override async Task Invoke(IOwinContext context)
await Next.Invoke(context);
if (context.Response.StatusCode == 400 && context.Response.Headers.ContainsKey("AuthorizationResponse"))
context.Response.Headers.Remove("AuthorizationResponse");
context.Response.StatusCode = 401;
最后要做的是在您的Startup.Configuration
方法中,注册您刚刚创建的类。我在方法中执行任何其他操作之前注册了它。
app.Use<InvalidAuthenticationMiddleware>();
【讨论】:
伟大的杰夫!我将您的答案与一些小的改动合并到我的原始解决方案中。请在下面查看我的编辑。 天才!救了我的命! 只有当我在 OAuth 配置之前添加app.Use<InvalidAuthenticationMiddleware>();
行时它才对我有用。
杰夫,非常感谢您的帖子。你的解决方案很棒。效果很好。
这对我有用。为什么我们必须拥有所有这些汤姆愚弄???为什么当我在 GrantResourceOwnerCredentials 中将上下文状态代码设置为 401 时,它会将其设置回 400?这是什么巫术?【参考方案2】:
这是一个完整的解决方案,将 Jeff 的概念与我的原始帖子结合使用。
1) 在上下文中设置错误消息
如果您在设置错误消息后调用 context.Rejected(),则会删除错误消息(参见下面的示例):
context.SetError("Account locked",
"You have exceeded the total allowed failed logins. Please try back in an hour.");
context.Rejected();
您将希望从您的任务中删除 context.Rejected()。请注意 Rejected 和 SetError 方法的定义是:
拒绝:
将此上下文标记为未经应用程序验证。 IsValidated 和 HasError 作为调用的结果变为 false。
设置错误:
将此上下文标记为未经应用程序验证并分配各种错误信息属性。由于调用,HasError 变为 true,IsValidated 变为 false。
同样,通过在设置错误后调用 Rejected 方法,上下文将被标记为没有错误并且错误消息将被删除。
2) 设置响应的状态码: 使用 Jeff 的例子,稍微转一下。
我将创建一个全局属性来设置状态代码的标记,而不是使用魔术字符串。在您的静态全局类中,创建一个用于标记状态代码的属性(我使用了 X-Challenge,但您当然可以使用您选择的任何内容。)这将用于标记响应中添加的标头属性。
public static class ServerGlobalVariables
//Your other properties...
public const string OwinChallengeFlag = "X-Challenge";
然后在您的 OAuthAuthorizationServerProvider 的各种任务中,您将添加标签作为响应中新标头值的键。将 HttpStatusCode 枚举与全局标志结合使用,您将可以访问所有各种状态代码,并且避免使用魔术字符串。
//Set the error message
context.SetError("Account locked",
"You have exceeded the total allowed failed logins. Please try back in an hour.");
//Add your flag to the header of the response
context.Response.Headers.Add(ServerGlobalVariables.OwinChallengeFlag,
new[] ((int)HttpStatusCode.Unauthorized).ToString() );
在客户OwinMiddleware中,可以使用全局变量在header中搜索flag:
//This class handles all the OwinMiddleware responses, so the name should
//not just focus on invalid authentication
public class CustomAuthenticationMiddleware : OwinMiddleware
public CustomAuthenticationMiddleware(OwinMiddleware next)
: base(next)
public override async Task Invoke(IOwinContext context)
await Next.Invoke(context);
if (context.Response.StatusCode == 400
&& context.Response.Headers.ContainsKey(
ServerGlobalVariables.OwinChallengeFlag))
var headerValues = context.Response.Headers.GetValues
(ServerGlobalVariables.OwinChallengeFlag);
context.Response.StatusCode =
Convert.ToInt16(headerValues.FirstOrDefault());
context.Response.Headers.Remove(
ServerGlobalVariables.OwinChallengeFlag);
最后,正如 Jeff 所指出的,您必须在您的 Startup.Configuration
或 Startup.ConfigureAuth
方法中注册此自定义 OwinMiddleware:
app.Use<CustomAuthenticationMiddleware>();
使用上述解决方案,您现在可以设置状态代码和自定义错误消息,如下所示:
用户名或密码无效 此帐户已超过最大尝试次数 邮箱账户未确认3) 从 ProtocolException 中提取错误信息
在客户端应用程序中,需要捕获和处理 ProtocolException。这样的事情会给你答案:
//Need to create a class to deserialize the Json
//Create this somewhere in your application
public class OAuthErrorMsg
public string error get; set;
public string error_description get; set;
public string error_uri get; set;
//Need to make sure to include Newtonsoft.Json
using Newtonsoft.Json;
//Code for your object....
private void login()
try
var state = _webServerClient.ExchangeUserCredentialForToken(
this.emailTextBox.Text,
this.passwordBox.Password.Trim(),
scopes: new string[] "PublicProfile" );
_accessToken = state.AccessToken;
_refreshToken = state.RefreshToken;
catch (ProtocolException ex)
var webException = ex.InnerException as WebException;
OAuthErrorMsg error =
JsonConvert.DeserializeObject<OAuthErrorMsg>(
ExtractResponseString(webException));
var errorMessage = error.error_description;
//Now it's up to you how you process the errorMessage
public static string ExtractResponseString(WebException webException)
if (webException == null || webException.Response == null)
return null;
var responseStream =
webException.Response.GetResponseStream() as MemoryStream;
if (responseStream == null)
return null;
var responseBytes = responseStream.ToArray();
var responseString = Encoding.UTF8.GetString(responseBytes);
return responseString;
我已经对此进行了测试,它在 VS2013 Pro 4.5 中完美运行!!
(请注意,我没有包含所有必要的命名空间或附加代码,因为这会因应用程序而异:WPF、MVC 或 Winform。另外,我没有讨论错误处理,所以你会想要确保在整个解决方案中实施正确的错误处理。)
【讨论】:
Rejected 和 SetError 都“将此上下文标记为未由应用程序验证。IsValidated 和 HasError 由于调用而变为 false。”当我在寻找 401 时,两种方法都会得到 400 状态码。 杰夫,在进一步调查后,我发现您的评论不正确。 SetError“将此上下文标记为未由应用程序验证并分配各种错误信息属性。由于调用,HasError 变为 true,IsValidated 变为 false。” 我明白了,漫长的一天结束了,我的眼睛/大脑并没有正常工作。 加一为详解! :) 您必须在Startup.ConfigureAuth
之前添加app.Use<CustomAuthenticationMiddleware>();
【参考方案3】:
Jeff 的解决方案对我不起作用,但是当我使用 OnSendingHeaders
时它可以正常工作:
public class InvalidAuthenticationMiddleware : OwinMiddleware
public InvalidAuthenticationMiddleware(OwinMiddleware next) : base(next)
public override async Task Invoke(IOwinContext context)
context.Response.OnSendingHeaders(state =>
var response = (OwinResponse)state;
if (!response.Headers.ContainsKey("AuthorizationResponse") && response.StatusCode != 400) return;
response.Headers.Remove("AuthorizationResponse");
response.StatusCode = 401;
, context.Response);
await Next.Invoke(context);
【讨论】:
以上是关于如何在客户端获取 DotNetOpenAuth.OAuth2 返回的错误消息?的主要内容,如果未能解决你的问题,请参考以下文章