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

Lóri Nóda picture Lóri Nóda · Jul 30, 2014 · Viewed 16.1k times · Source

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

Answer

Jeff Vanzella picture Jeff Vanzella · Sep 16, 2014

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