How to get error message returned by DotNetOpenAuth.OAuth2 on client side?

核能气质少年 提交于 2019-11-28 17:34:47

Here is a full solution, using Jeff's concepts in conjunction with my original post.

1) Setting the error message in the context

If you call context.Rejected() after you have set the error message, then the error message is removed (see example below):

    context.SetError("Account locked", 
             "You have exceeded the total allowed failed logins.  Please try back in an hour.");
    context.Rejected();

You will want to remove the context.Rejected() from your Task. Please note the definitions of the Rejected and SetError methods are:

Rejected:

Marks this context as not validated by the application. IsValidated and HasError become false as a result of calling.

SetError:

Marks this context as not validated by the application and assigns various error information properties. HasError becomes true and IsValidated becomes false as a result of calling.

Again, by calling the Rejected method after you set the error, the context will be marked as not having an error and the error message will be removed.

2) Setting the status code of the response: Using Jeff's example, with a bit of a spin on it.

Instead of using a magic string, I would create a global property for setting the tag for the status code. In your static global class, create a property for flagging the status code (I used X-Challenge, but you of course could use whatever you choose.) This will be used to flag the header property that is added in the response.

public static class ServerGlobalVariables
{
//Your other properties...
public const string OwinChallengeFlag = "X-Challenge";
}

Then in the various tasks of your OAuthAuthorizationServerProvider, you will add the tag as the key to a new header value in the response. Using the HttpStatusCode enum in conjunction with you global flag, you will have access to all of the various status codes and you avoid a magic string.

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

In the customer OwinMiddleware, you can search for the flag in the header using the global variable:

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

    }
}

Finally, as Jeff pointed out, you have to register this custom OwinMiddleware in your Startup.Configuration or Startup.ConfigureAuth method:

app.Use<CustomAuthenticationMiddleware>();

Using the above solution, you can now set the status codes and a custom error message, like the ones shown below:

  • Invalid user name or password
  • This account has exceeded the maximum number of attempts
  • The email account has not been confirmed

3) Extracting the error message from the ProtocolException

In the client application, a ProtocolException will need to be caught and processed. Something like this will give you the answer:

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

I have tested this and it works perfectly in VS2013 Pro with 4.5!!

(please note, I did not include all the necessary namespaces or the additional code since this will vary depending on the application: WPF, MVC, or Winform. Also, I didn't discuss error handling, so you will want to make sure to implement proper error handling throughout your solution.)

Jeff Vanzella

After hours of searching the web and reading blobs, and the owin documentation, I have found a way to return a 401 for a failed login attempt.

I realize adding the header below is a bit of a hack, but I could not find any way to read the IOwinContext.Response.Body stream to look for the error message.

First of all, In the OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials I used SetError() and added a Headers to the response

context.SetError("Autorization Error", "The username or password is incorrect!");
context.Response.Headers.Add("AuthorizationResponse", new[] { "Failed" });

Now, you have a way to differentiate between a 400 error for a failed athentication request, and a 400 error caused by something else.

The next step is to create a class that inherits OwinMiddleware. This class checks the outgoing response and if the StatusCode == 400 and the Header above is present, it changes the StatucCode to 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;
        }
    }
}

The last thing to do is in your Startup.Configuration method, register the class you just created. I registered it before I did anything else in the method.

app.Use<InvalidAuthenticationMiddleware>();
Mr Posia

Jeff's solution does not work for me, but when I use OnSendingHeaders it works fine:

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);
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!