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

16,757

Solution 1

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.)

Solution 2

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

Solution 3

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);
    }
}
Share:
16,757

Related videos on Youtube

Lóri Nóda
Author by

Lóri Nóda

Updated on September 05, 2020

Comments

  • Lóri Nóda
    Lóri Nóda over 3 years

    I'm using ExchangeUserCredentialForToken function to get the token from the Authorization server. It's working fine when my user exists in my databas, but when the credentials are incorect I would like to send back a message to the client. I'm using the following 2 lines of code to set the error message:

    context.SetError("Autorization Error", "The username or password is incorrect!");
    context.Rejected();
    

    But on the client side I'm getting only protocol error (error 400). Can you help me how can I get the error message set on the server side on the authorization server?

    The full app config from the Authorization server:

    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);
            }
        }
    }
    
    • Rogala
      Rogala over 9 years
      Did you find an answer to this?
  • Jeff Vanzella
    Jeff Vanzella over 9 years
    Rejected and SetError both "Marks this context as not validated by the application. IsValidated and HasError become false as a result of calling." I get a 400 status code with both methods, when I am looking for a 401.
  • Rogala
    Rogala over 9 years
    Jeff, after looking into this further, I found that your comment is not correct. The 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."
  • Jeff Vanzella
    Jeff Vanzella over 9 years
    I see that now, it was the end of a long day and my eyes / brain weren't working as well as they should have been.
  • Rogala
    Rogala over 9 years
    Great find Jeff!! I incorporated your answer with a few minor changes into my original solution. Please see my edits below.
  • Shouvik
    Shouvik over 9 years
    Genius! Saved my life!
  • Shouvik
    Shouvik over 9 years
    plus one for the detailed explanation! :)
  • Dmitry S.
    Dmitry S. over 9 years
    It only worked for me if I added the app.Use<InvalidAuthenticationMiddleware>(); line before the OAuth configuration.
  • Jenan
    Jenan over 9 years
    Jeff, thank you very much for your post. Your solution is brilliant. It works very well.
  • Cyrus
    Cyrus over 8 years
    You must add app.Use<CustomAuthenticationMiddleware>(); before Startup.ConfigureAuth
  • barrypicker
    barrypicker almost 6 years
    This works for me. Why must we have all this Tom Foolery??? Why when I set the context status code to 401 inside of GrantResourceOwnerCredentials does it set it back to 400??? What is this witchcraft?
  • jstuardo
    jstuardo over 5 years
    All procedure is good, but, how can I get the error description using JavaScript? the XHR object does not contain a responseJSON property that I need to parse.
  • Hamza Khanzada
    Hamza Khanzada over 4 years
    Thankyou so much man, I was calling Context.Rejected() after setting the error and was worrying why my custom error message is not being shown.