Как получить сообщение об ошибке, возвращенное 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);
}
}
}
Ответы
Ответ 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);
}
}