c# - google - ¿Cómo obtener un mensaje de error devuelto por DotNetOpenAuth.OAuth2 en el lado del cliente?
web api authentication token example (3)
Aquí hay una solución completa, utilizando los conceptos de Jeff junto con mi publicación original.
1) Configuración del mensaje de error en el contexto
Si llama a context.Rejected () después de haber configurado el mensaje de error, se eliminará el mensaje de error (vea el ejemplo a continuación):
context.SetError("Account locked",
"You have exceeded the total allowed failed logins. Please try back in an hour.");
context.Rejected();
Querrá eliminar el contexto. Rechazado () de su tarea. Tenga en cuenta que las definiciones de los métodos Rejected y SetError son:
Rechazado:
Marca este contexto como no validado por la aplicación. IsValidated y HasError se vuelven falsos como resultado de la llamada.
SetError:
Marca este contexto como no validado por la aplicación y asigna varias propiedades de información de error. HasError se convierte en verdadero e IsValidated se convierte en falso como resultado de la llamada.
Nuevamente, al llamar al método Rechazado después de establecer el error, el contexto se marcará como que no tiene un error y el mensaje de error se eliminará.
2) Configuración del código de estado de la respuesta: usando el ejemplo de Jeff, con un poco de giro en él.
En lugar de usar una cadena mágica, crearía una propiedad global para establecer la etiqueta para el código de estado. En su clase global estática, cree una propiedad para marcar el código de estado (utilicé X-Challenge, pero por supuesto que podría usar lo que elija). Esto se usará para marcar la propiedad del encabezado que se agrega en la respuesta.
public static class ServerGlobalVariables
{
//Your other properties...
public const string OwinChallengeFlag = "X-Challenge";
}
Luego, en las diversas tareas de su OAuthAuthorizationServerProvider, agregará la etiqueta como la clave a un nuevo valor de encabezado en la respuesta. Usando la enumeración HttpStatusCode junto con su marca global, tendrá acceso a todos los diversos códigos de estado y evitará una cadena mágica.
//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() });
En el cliente OwinMiddleware, puede buscar la bandera en el encabezado usando la variable global:
//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);
}
}
}
Finalmente, como señaló Jeff, tiene que registrar este OwinMiddleware personalizado en su método de configuración de inicio o configuración de inicio:
app.Use<CustomAuthenticationMiddleware>();
Con la solución anterior, ahora puede configurar los códigos de estado y un mensaje de error personalizado, como los que se muestran a continuación:
- Usuario o contraseña invalido
- Esta cuenta ha superado el número máximo de intentos.
- La cuenta de correo electrónico no ha sido confirmada.
3) Extraer el mensaje de error de la excepción ProtocolException
En la aplicación cliente, será necesario detectar y procesar una excepción de protocolo. Algo como esto te dará la respuesta:
//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;
}
He probado esto y funciona perfectamente en VS2013 Pro con 4.5 !!
(Tenga en cuenta que no incluí todos los espacios de nombres necesarios ni el código adicional, ya que variará según la aplicación: WPF, MVC o Winform. Además, no comenté el manejo de errores, por lo que querrá asegurarse de implementar el manejo adecuado de errores en su solución.)
Estoy usando la función ExchangeUserCredentialForToken
para obtener el token del servidor de autorización. Funciona bien cuando mi usuario existe en mi base de datos, pero cuando las credenciales son incorrectas, me gustaría enviar un mensaje al cliente. Estoy usando las siguientes 2 líneas de código para configurar el mensaje de error:
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Rejected();
Pero en el lado del cliente, solo obtengo un error de protocolo (error 400). ¿Puede ayudarme? ¿Cómo puedo obtener el mensaje de error establecido en el lado del servidor en el servidor de autorización?
La configuración completa de la aplicación desde el servidor de Autorización:
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);
}
}
}
Después de horas de buscar en la web y leer blobs, y la documentación de owin, he encontrado una manera de devolver un 401 para un intento de inicio de sesión fallido.
Me doy cuenta de que agregar el encabezado a continuación es un truco, pero no pude encontrar ninguna manera de leer el flujo IOwinContext.Response.Body para buscar el mensaje de error.
En primer lugar, en el OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials
usé SetError()
y añadí un Headers
a la respuesta
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Response.Headers.Add("AuthorizationResponse", new[] { "Failed" });
Ahora, tiene una forma de diferenciar entre un error 400 para una solicitud de autenticación fallida y un error 400 causado por otra cosa.
El siguiente paso es crear una clase que herede OwinMiddleware
. Esta clase verifica la respuesta saliente y si el StatusCode == 400
y el Encabezado anterior están presentes, cambia el StatucCode a 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;
}
}
}
Lo último que debe hacer es en su método de Startup.Configuration
, registrar la clase que acaba de crear. Lo registré antes de hacer cualquier otra cosa en el método.
app.Use<InvalidAuthenticationMiddleware>();
La solución de Jeff no funciona para mí, pero cuando uso OnSendingHeaders
funciona bien:
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);
}
}