Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2 (Protección de una API web con cuentas individuales e inicio de sesión local en ASP.NET Web API 2.2)

por Mike Wasson

Descargar aplicación de ejemplo

En este tema se muestra cómo proteger una API web mediante OAuth2 para autenticarse en una base de datos de pertenencia.

Versiones de software usadas en el tutorial

En Visual Studio 2013, la plantilla de proyecto de Web API proporciona tres opciones para la autenticación:

  • Cuentas individuales. La aplicación usa una base de datos de pertenencia.
  • Cuentas de organización. Los usuarios inician sesión con sus credenciales de Azure Active Directory, Office 365 o Active Directory local.
  • Autenticación de Windows. Esta opción está destinada a aplicaciones de intranet y usa el módulo de IIS de autenticación de Windows.

Para obtener más detalles sobre estas opciones, consulte Creación de proyectos web de ASP.NET en Visual Studio 2013.

Las cuentas individuales ofrecen a los usuarios dos formas de iniciar sesión:

  • Inicio de sesión local. El usuario se registra en el sitio, para lo cual escribe un nombre de usuario y una contraseña. La aplicación almacena el hash de contraseña en la base de datos de pertenencia. Cuando el usuario inicia sesión, el sistema ASP.NET Identity comprueba la contraseña.
  • Inicio de sesión social. El usuario inicia sesión con un servicio externo, como Facebook, Microsoft o Google. La aplicación crea una entrada para el usuario en la base de datos de pertenencia, pero no almacena ninguna credencial. El usuario se autentica mediante el inicio de sesión en el servicio externo.

En este artículo se analiza el escenario de inicio de sesión local. En los inicios de sesión local y social, Web API usa OAuth2 para autenticar las solicitudes. Sin embargo, los flujos de credenciales son distintos para ambos inicios de sesión.

En este artículo se muestra el uso de una aplicación simple que permite al usuario iniciar sesión y enviar llamadas AJAX autenticadas a una API web. Puede descargar el código de ejemplo aquí. El archivo Léame describe cómo crear el ejemplo desde cero en Visual Studio.

Image of sample form

La aplicación de ejemplo usa Knockout.js para el enlace de datos y jQuery para enviar las solicitudes AJAX. Nos centraremos en las llamadas AJAX, por lo que no es necesario conocer Knockout.js para este artículo.

Durante el proceso, describiremos:

  • Qué hace la aplicación en el lado cliente.
  • Qué sucede en el servidor.
  • El tráfico HTTP que existe entre ambos.

En primer lugar, debemos definir algunos términos de OAuth2.

  • Recurso. Parte de los datos que se puede proteger.
  • Servidor de recursos. Servidor que hospeda el recurso.
  • Propietario del recurso. Entidad que puede conceder permiso para acceder a un recurso. (Suele ser el usuario).
  • Cliente: aplicación que quiere acceder al recurso. En este artículo, el cliente es un explorador web.
  • Token de acceso. Token que concede acceso a un recurso.
  • Token de portador. Un tipo determinado de token de acceso, con la propiedad de que cualquier usuario puede usar el token. En otras palabras, un cliente no necesita una clave criptográfica u otro secreto para usar un token de portador. Por ese motivo, los tokens de portador solo se deben usar a través de HTTPS y deben tener tiempos de expiración relativamente cortos.
  • Servidor de autorización. Un servidor que proporciona tokens de acceso.

Una aplicación puede actuar como servidor de autorización y como servidor de recursos. La plantilla de proyecto de Web API sigue este patrón.

Flujo de credenciales de inicio de sesión local

Para el inicio de sesión local, Web API usa el flujo de contraseña del propietario del recurso definido en OAuth2.

  1. El usuario escribe un nombre y una contraseña en el cliente.
  2. El cliente envía estas credenciales al servidor de autorización.
  3. El servidor de autorización autentica las credenciales y devuelve un token de acceso.
  4. Para acceder a un recurso protegido, el cliente incluye el token de acceso en el encabezado Authorization de la solicitud HTTP.

Diagram of local login credential flow

Al seleccionar Cuentas individuales en la plantilla de proyecto de Web API, el proyecto incluye un servidor de autorización que valida las credenciales de usuario y emite tokens. En el diagrama siguiente se muestra el mismo flujo de credenciales en cuanto a los componentes de Web API.

Diagram when individual accounts is selected in the Web A P I

En este escenario, los controladores de Web API actúan como servidores de recursos. Un filtro de autenticación valida los tokens de acceso y el atributo [Authorize] se usa para proteger un recurso. Cuando un controlador o una acción tienen el atributo [Authorize], deben autenticarse todas las solicitudes a ese controlador o acción. De lo contrario, se deniega la autorización y Web API devuelve un error 401 (no autorizado).

Tanto el servidor de autorización como el filtro de autenticación llaman a un componente de middleware de OWIN que controla los detalles de OAuth2. Más adelante en este tutorial describiremos el diseño en mayor detalle.

Envío de una solicitud no autorizada

Para empezar, ejecute la aplicación y haga clic en el botón Llamar a API. Al completarse la solicitud, debería aparecer un mensaje de error en el cuadro Resultado. Esto se debe a que la solicitud no contiene un token de acceso, por lo que no está autorizada.

Image of result error message

El botón Llamar a API envía una solicitud AJAX a ~/api/values, que invoca una acción del controlador de Web API. Esta es la sección del código JavaScript que envía la solicitud AJAX. En la aplicación de ejemplo, todo el código de la aplicación JavaScript se encuentra en el archivo Scripts\app.js.

// If we already have a bearer token, set the Authorization header.
var token = sessionStorage.getItem(tokenKey);
var headers = {};
if (token) {
    headers.Authorization = 'Bearer ' + token;
}

$.ajax({
    type: 'GET',
    url: 'api/values/1',
    headers: headers
}).done(function (data) {
    self.result(data);
}).fail(showError);

Hasta que el usuario inicia sesión, no hay ningún token de portador y, por lo tanto, no hay ningún encabezado Authorization en la solicitud. Esto hace que la solicitud devuelva un error 401.

Esta es la solicitud HTTP. (Hemos usado Fiddler para capturar el tráfico HTTP).

GET https://localhost:44305/api/values HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Accept-Language: en-US,en;q=0.5
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/

Respuesta HTTP:

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
WWW-Authenticate: Bearer
Date: Tue, 30 Sep 2014 21:54:43 GMT
Content-Length: 61

{"Message":"Authorization has been denied for this request."}

Observe que la respuesta incluye un encabezado Www-Authenticate establecido en Bearer. Esto indica que el servidor espera un token de portador.

Registro de un usuario

En la sección Registrar de la aplicación, escriba un correo electrónico y una contraseña y haga clic en el botón Registrar.

No es necesario usar una dirección de correo electrónico válida para este ejemplo, pero una aplicación real confirmaría la dirección. (Consulte Creación de una aplicación web de ASP.NET MVC 5 segura con inicio de sesión, confirmación por correo electrónico y restablecimiento de contraseña). Para la contraseña, use algo parecido a "Contraseña1!", con una letra mayúscula, una letra minúscula, un número y un carácter no alfanumérico. Para que la aplicación sea sencilla, no se ha incluido la validación del lado cliente, por lo que si hay algún problema con el formato de contraseña, obtendrá un error 400 (solicitud incorrecta).

Image of register a user section

El botón Registrar envía una solicitud POST a ~/api/Account/Register/. El cuerpo de la solicitud es un objeto JSON que contiene el nombre y la contraseña. Este es el código JavaScript que envía la solicitud:

var data = {
    Email: self.registerEmail(),
    Password: self.registerPassword(),
    ConfirmPassword: self.registerPassword2()
};

$.ajax({
    type: 'POST',
    url: '/api/Account/Register',
    contentType: 'application/json; charset=utf-8',
    data: JSON.stringify(data)
}).done(function (data) {
    self.result("Done!");
}).fail(showError);

Solicitud HTTP, donde $CREDENTIAL_PLACEHOLDER$ es un marcador de posición para el par clave-valor de contraseña:

POST https://localhost:44305/api/Account/Register HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 84

{"Email":"alice@example.com",$CREDENTIAL_PLACEHOLDER1$,$CREDENTIAL_PLACEHOLDER2$"}

Respuesta HTTP:

HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 00:57:58 GMT
Content-Length: 0

La clase AccountController controla esta solicitud. Internamente, AccountController usa ASP.NET Identity para administrar la base de datos de pertenencia.

Si ejecuta la aplicación de forma local desde Visual Studio, las cuentas de usuario se almacenan en LocalDB, en la tabla AspNetUsers. Para ver las tablas en Visual Studio, haga clic en el menú Ver, seleccione Explorador de servidores y, a continuación, expanda Conexiones de datos.

Image of Data Connections

Obtención de un token de acceso

Hasta ahora no hemos aplicado OAuth, pero ahora veremos el servidor de autorización de OAuth en acción, cuando se solicita un token de acceso. En el área Iniciar sesión de la aplicación de ejemplo, escriba el correo electrónico y la contraseña y haga clic en Iniciar sesión.

Image of log in section

El botón Iniciar sesión envía una solicitud al punto de conexión de token. El cuerpo de la solicitud contiene los siguientes datos con formato form-url-encoded:

  • grant_type: "contraseña"
  • username: <correo electrónico del usuario>
  • password: <contraseña>

Este es el código JavaScript que envía la solicitud AJAX:

var loginData = {
    grant_type: 'password',
    username: self.loginEmail(),
    password: self.loginPassword()
};

$.ajax({
    type: 'POST',
    url: '/Token',
    data: loginData
}).done(function (data) {
    self.user(data.userName);
    // Cache the access token in session storage.
    sessionStorage.setItem(tokenKey, data.access_token);
}).fail(showError);

Si la solicitud se realiza correctamente, el servidor de autorización devuelve un token de acceso en el cuerpo de la respuesta. Tenga en cuenta que el token se guarda en el almacenamiento de sesión a fin de usarlo más adelante al enviar solicitudes a la API. A diferencia de otras formas de autenticación (como la basada en cookies), el explorador no incluirá automáticamente el token de acceso en las solicitudes subsiguientes. La aplicación debe hacerlo de forma explícita. Este es un aspecto positivo, porque limita las vulnerabilidades de CSRF.

Solicitud HTTP:

POST https://localhost:44305/Token HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 68

grant_type=password&username=alice%40example.com&password=Password1!

Puede ver que la solicitud contiene las credenciales del usuario. Debe usar HTTPS para proporcionar seguridad de la capa de transporte.

Respuesta HTTP:

HTTP/1.1 200 OK
Content-Length: 669
Content-Type: application/json;charset=UTF-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:22:36 GMT

{
  "access_token":"imSXTs2OqSrGWzsFQhIXziFCO3rF...",
  "token_type":"bearer",
  "expires_in":1209599,
  "userName":"alice@example.com",
  ".issued":"Wed, 01 Oct 2014 01:22:33 GMT",
  ".expires":"Wed, 15 Oct 2014 01:22:33 GMT"
}

Para mejorar la legibilidad, hemos aplicado sangría al JSON y se ha truncado el token de acceso, que es bastante largo.

La especificación de OAuth2 define las propiedades access_token, token_type y expires_in. El resto de propiedades (userName, .issued y .expires) son solo para fines informativos. Puede encontrar el código que agrega esas propiedades adicionales en el método TokenEndpoint, en el archivo /Providers/ApplicationOAuthProvider.cs.

Envío de una solicitud autenticada

Ahora que tenemos un token de portador, podemos realizar una solicitud autenticada a la API. Para ello, establezca el encabezado Authorization en la solicitud. Haga clic de nuevo en el botón Llamar a API para verlo.

Image after call A P I button has been clicked

Solicitud HTTP:

GET https://localhost:44305/api/values/1 HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Authorization: Bearer imSXTs2OqSrGWzsFQhIXziFCO3rF...
X-Requested-With: XMLHttpRequest

Respuesta HTTP:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:41:29 GMT
Content-Length: 27

"Hello, alice@example.com."

Cerrar sesión

Dado que el explorador no almacena en caché las credenciales ni el token de acceso, cerrar sesión consiste simplemente en "olvidar" el token, para lo que se quita del almacenamiento de sesión:

self.logout = function () {
    sessionStorage.removeItem(tokenKey)
}

Descripción de la plantilla de proyecto Cuentas individuales

Al seleccionar Cuentas individuales en la plantilla de proyecto Aplicación web ASP.NET, el proyecto incluye:

  • Un servidor de autorización de OAuth2
  • Un punto de conexión de Web API para administrar cuentas de usuario
  • Un modelo de EF para almacenar cuentas de usuario

Estas son las clases de aplicación principales que implementan estas características:

  • AccountController. Proporciona un punto de conexión de Web API para administrar cuentas de usuario. La acción Register es la única que usamos en este tutorial. Otros métodos de la clase admiten el restablecimiento de contraseña, los inicios de sesión sociales y otras funciones.
  • ApplicationUser, definida en /Models/IdentityModels.cs. Esta clase es el modelo de EF para las cuentas de usuario en la base de datos de pertenencia.
  • ApplicationUserManager, definida en /App_Start/IdentityConfig.cs. Esta clase deriva de UserManager y realiza operaciones en las cuentas de usuario, como la creación de un usuario o la comprobación de contraseñas, entre otras. Además, conserva automáticamente los cambios en la base de datos.
  • ApplicationOAuthProvider. Este objeto se conecta al middleware de OWIN y procesa los eventos generados por el middleware. Deriva de OAuthAuthorizationServerProvider.

Image of main application classes

Configuración del servidor de autorización

En StartupAuth.cs, el código siguiente configura el servidor de autorización de OAuth2.

PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/Token"),
    Provider = new ApplicationOAuthProvider(PublicClientId),
    AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
    AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
    // Note: Remove the following line before you deploy to production:
    AllowInsecureHttp = true
};

// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);

La propiedad TokenEndpointPath es la ruta de acceso de URL al punto de conexión del servidor de autorización. Esa es la dirección URL que la aplicación usa para obtener los tokens de portador.

La propiedad Provider especifica un proveedor que se conecta al middleware de OWIN y procesa los eventos generados por el middleware.

Este es el flujo básico cuando la aplicación quiere obtener un token:

  1. Para obtener un token de acceso, la aplicación envía una solicitud a ~/Token.
  2. El middleware de OAuth llama a GrantResourceOwnerCredentials en el proveedor.
  3. El proveedor llama a ApplicationUserManager para validar las credenciales y crear una identidad de notificaciones.
  4. Si se ejecuta correctamente, el proveedor crea un vale de autenticación, que se usa para generar el token.

Diagram of authorization flow

El middleware de OAuth no sabe nada sobre las cuentas de usuario. El proveedor se comunica entre el middleware y ASP.NET Identity. Para obtener más información sobre cómo implementar el servidor de autorización, consulte Servidor de autorización de OAuth 2.0 de OWIN.

Configuración de Web API para usar tokens de portador

En el método WebApiConfig.Register, el código siguiente configura la autenticación para la canalización de Web API:

config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

La clase HostAuthenticationFilter habilita la autenticación mediante tokens de portador.

El método SuppressDefaultHostAuthentication indica a Web API que omita cualquier autenticación que se produzca antes de que la solicitud llegue a la canalización de Web API, ya sea por IIS o por el middleware de OWIN. De este modo, se puede restringir API Web para la autenticación solo con tokens de portador.

Nota:

En concreto, la parte de MVC de la aplicación puede usar la autenticación de formularios, que almacena las credenciales en una cookie. La autenticación basada en cookies requiere el uso de tokens antifalsificación para evitar ataques CSRF. Esto es un problema para las API web, ya que no hay ninguna forma conveniente de que estas envíen el token antifalsificación al cliente. (Para obtener más información sobre este problema, consulte Prevención de ataques CSRF en Web API). Al llamar a SuppressDefaultHostAuthentication se garantiza que Web API no sea vulnerable a ataques CSRF de credenciales almacenadas en cookies.

Cuando el cliente solicita un recurso protegido, esto es lo que sucede en la canalización de Web API:

  1. El filtro HostAuthentication llama al middleware de OAuth para validar el token.
  2. El middleware convierte el token en una identidad de notificaciones.
  3. En este punto, la solicitud se ha autenticado pero no se ha autorizado.
  4. El filtro de autorización examina la identidad de notificaciones. Si las notificaciones autorizan al usuario para ese recurso, se autoriza la solicitud. De forma predeterminada, el atributo [Authorize] autorizará cualquier solicitud que se autentique. Sin embargo, puede autorizar por rol o por otras notificaciones. Para obtener más información, consulte Autenticación y autorización en Web API.
  5. Si los pasos anteriores se realizan correctamente, el controlador devuelve el recurso protegido. De lo contrario, el cliente recibe un error 401 (no autorizado).

Diagram of when the client requests a protected resource

Recursos adicionales