Подтвердить что ты не робот

Как получить сообщение об ошибке, возвращенное DotNetOpenAuth.OAuth2 на стороне клиента?

Я использую функцию ExchangeUserCredentialForToken для получения маркера с сервера авторизации. Он отлично работает, когда мой пользователь существует в моих базах данных, но когда учетные данные являются инстанциями, я хотел бы отправить сообщение клиенту. Я использую следующие 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<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);
        }
    }
}
4b9b3361

Ответ 1

Вот полное решение, использующее концепции Джеффа в сочетании с моим оригинальным сообщением.

1) Установка сообщения об ошибке в контексте

Если вы вызываете context.Rejected() после того, как вы установили сообщение об ошибке, сообщение об ошибке удаляется (см. пример ниже):

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

Вы хотите удалить контекст .Rejected() из своей задачи. Обратите внимание, что определения методов Отклонено и SetError:

Отклонено:

Отмечает этот контекст как не проверенный приложением. IsValidated и HasError становятся ложными в результате вызова.

SetError:

Отмечает этот контекст как не проверенный приложением и присваивает различные свойства информации об ошибке. HasError становится истинным, а IsValidated становится ложным в результате вызова.

Опять же, вызывая метод 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);
        }         

    }
}

Наконец, как указал Джефф, вы должны зарегистрировать этот пользовательский OwinMiddleware в методе Startup.Configuration или Startup.ConfigureAuth:

app.Use<CustomAuthenticationMiddleware>();

Используя приведенное выше решение, вы можете теперь установить коды состояния и настраиваемое сообщение об ошибке, как показано ниже:

  • Неверное имя пользователя или пароль
  • Эта учетная запись превысила максимальное количество попыток
  • Учетная запись электронной почты не подтверждена.

3) Извлечение сообщения об ошибке из протоколаException

В клиентском приложении необходимо будет поймать и обработать исключение 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<OAuthErrorMsg>(
                ExtractResponseString(webException));

            var errorMessage = error.error_description;
            //Now it 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. Кроме того, я не обсуждал обработку ошибок, поэтому вы захотите убедитесь, что вы выполняете правильную обработку ошибок во всем своем решении.)

Ответ 2

После нескольких часов поиска в Интернете и чтения блобов и документации 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" });

Теперь у вас есть способ различать ошибку 400 для неудавшегося запроса аутентификации и ошибку 400, вызванную чем-то другим.

Следующий шаг - создать класс, наследующий OwinMiddleware. Этот класс проверяет исходящий ответ, и если присутствует StatusCode == 400 и верхний колонтитул, он меняет 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<InvalidAuthenticationMiddleware>();

Ответ 3

Решение 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);
    }
}