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

3

25

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);
        }
    }
}
Apparatus answered 30/7, 2014 at 8:51 Comment(1)
Did you find an answer to this?Associate
A
29

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

Associate answered 8/9, 2014 at 20:51 Comment(7)
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.Provenance
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."Associate
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.Provenance
plus one for the detailed explanation! :)Protozoology
You must add app.Use<CustomAuthenticationMiddleware>(); before Startup.ConfigureAuthThreepence
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.Early
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.Kinshasa
P
35

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>();
Provenance answered 15/9, 2014 at 23:25 Comment(5)
Great find Jeff!! I incorporated your answer with a few minor changes into my original solution. Please see my edits below.Associate
Genius! Saved my life!Protozoology
It only worked for me if I added the app.Use<InvalidAuthenticationMiddleware>(); line before the OAuth configuration.Himation
Jeff, thank you very much for your post. Your solution is brilliant. It works very well.Subcontract
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?Connote
A
29

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

Associate answered 8/9, 2014 at 20:51 Comment(7)
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.Provenance
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."Associate
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.Provenance
plus one for the detailed explanation! :)Protozoology
You must add app.Use<CustomAuthenticationMiddleware>(); before Startup.ConfigureAuthThreepence
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.Early
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.Kinshasa
R
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);
    }
}
Rozier answered 21/4, 2016 at 16:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.