如何在客户端获取DotNetOpenAuth.OAuth2返回的错误消息?

我正在使用ExchangeUserCredentialForToken函数从授权服务器获取令牌。 当我的用户存在于我的数据库中时,它工作正常,但是当凭据是incorect时,我想向客户端发回消息。 我正在使用以下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 _authenticationCodes = new ConcurrentDictionary(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); } } } 

这是一个完整的解决方案,将Jeff的概念与我的原始post结合使用。

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方法的定义是:

被拒绝:

将此上下文标记为未由应用程序validation。 由于调用,IsValidated和HasError变为false。

SETERROR:

将此上下文标记为未由应用程序validation,并分配各种错误信息属性。 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中,您可以使用全局变量在标头中搜索标志:

 //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.ConfigurationStartup.ConfigureAuth方法中注册此自定义OwinMiddleware:

 app.Use(); 

使用上述解决方案,您现在可以设置状态代码和自定义错误消息,如下所示:

  • 用户名或密码无效
  • 此帐户已超过最大尝试次数
  • 电子邮件帐户尚未确认

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( 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。另外,我没有讨论error handling,所以你需要确保在整个解决方案中实施适当的error handling。

经过几个小时的网络搜索和阅读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" }); 

现在,您可以区分失败的身份validation请求的400错误和其他原因导致的400错误。

下一步是创建一个inheritanceOwinMiddleware的类。 此类检查传出响应,如果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(); 

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); } }