DEV Community

Cover image for [Parte 10] ASP.NET Core: Refresh Tokens
Isaac Ojeda
Isaac Ojeda

Posted on

[Parte 10] ASP.NET Core: Refresh Tokens

Introducción

En esta publicación hablaremos de que son los Refresh Tokens y cómo es una forma segura de implementarlos.

La implementación de refresh tokens está un poco abierta a interpretación, aquí intentaré explicar mi interpretación, buscando siempre la seguridad del usuario y tener esa posibilidad de ofrecer una mejor experiencia al iniciar sesión y no tener que estarlo autenticado a cada momento.

Como siempre aquí puedes ver el código para este post. Siempre puedes buscarme en mi twitter en @balunatic si es que tienes dudas o sugerencias sobre temas de los que te gustaría que hablemos aquí.

Tokens

Los Tokens son pedazos de información que contienen datos para facilitar el proceso de autenticación y autorización en una aplicación. Es decir, con los tokens es fácil y usualmente seguro determinar la identidad de un usuario y así autorizar su acceso.

Nota 👀: En este blog ya hemos hablado varias veces de autenticación y autorización, te dejo aquí los artículos: Parte 5: Identity Core, Servidor de autenticación OIDC y Autenticación JWT y Identity Core.

OAuth 2.0 es ahora el framework de autorización más popular, y por su popularidad, ya todos tenemos que usar conceptos que OAuth 2.0 definió desde un inicio. En OAuth 2.0 se define lo que son los Access Tokens y Refresh Tokens.

En cambio, OpenID Connect (OIDC) es un protocolo de identidad que se centra más en la identidad del usuario, es aquí donde podemos encontrar el concepto de los ID Tokens.

ID Token

Como su nombre lo dice, es un Token para identificación del usuario. Es un token que las aplicaciones pueden usar para obtener información directa del usuario autenticado.

Los ID Tokens sirven para dar esa personalización al usuario, si inicia sesión con Facebook, mínimo ya podríamos saber su nombre, su correo y posiblemente su foto de perfil. Así, sin preguntar nada, podríamos ya tener un perfil del usuario autenticado y ofrecer una experiencia agradable al usar nuestra aplicación.

Realmente el uso de ID Tokens se emplean cuando hacemos una implementación más "formal" de OpenID Connect, pero es interesante conocer que es, ya que a veces vemos estos nombres rondando por internet.

Access Token

Cuando un usuario inicia sesión, el servidor de autorización genera un access token, el cual es un token que las aplicaciones clientes usan para realizar llamadas seguras a una API. Cuando la aplicación cliente necesita de información que solo está disponibles para usuarios autenticados, se usa el access token para realizar solicitudes en nombre de ese usuario.

Los Access Token son los que usualmente emitimos cuando nosotros mismos implementamos la autenticación.

Los access token generalmente tienen un tiempo de vida corto, esto es porque, por motivos de seguridad, debemos de limitar esa ventana de tiempo en que un atacante podría robar la identidad de un usuario en caso de que un Access Token fuera robado.

Con un Access Token, en teoría, puedes hacer lo que sea que el usuario al que le pertenece el JWT pueda hacer, por eso es importante la seguridad de estos.

Refresh Tokens

Los Refresh Tokens es por lo que estamos aquí el día de hoy.

Como se mencionó antes, por propósitos de seguridad, los access tokens solo deben de ser válidos por un periodo corto de tiempo. Una vez que este expira, las aplicaciones clientes pueden usar un refresh token para actualizar el access token y obtener uno nuevo.

Por lo tanto, un refresh token es el equivalente al saber el usuario y contraseña del usuario, ya que con el refresh token, podemos generar otro access token sin preguntar las credenciales nuevamente.

Image description

En el Diagrama: SPA = Single Page Application, AS = Authorization Server, RS = Resource Server, AT = Access Token, RT = Refresh Token

La aplicación cliente puede obtener un nuevo access token siempre que el refresh token siga siendo válido y que no haya expirado o revocado. Por consecuencia, un Refresh Token con un periodo de vida muy largo, podría dar poder infinito a quien lo tenga para generar los Access Tokens que quiera. Podría ser un usuario legítimo sin ningún problema, pero también un usuario malintencionado.

Realmente no hay un proceso oficial en el cual se deben de implementar un Refresh Token, pero tomando en cuenta lo anterior, debemos de considerar que es algo muy delicado si no se le presta atención.

Con Refresh Tokens podríamos abrir puertas a los atacantes si no hacemos las cosas correctamente.

Manteniendo seguros los Refresh Tokens

Para un refresh token, lo normal es que tenga un tiempo de vida mucho más largo que un Access Token, así podrá ser usado por la aplicación cliente las veces que sean necesarias.

No sé si ustedes lo notan, pero por ejemplo en facebook, jamás inicio sesión (en mi computadora personal claro). Entonces, dado que tengo un password super difícil, me es práctico que el navegador me recuerde.

No es que así lo haga facebook, solo es una referencia, pero los Refresh Tokens nos podrían permitir mantener una experiencia similar. Si el Access Token expiró, sin decirle nada al usuario, podemos usar el Refresh Token para volver a generar un access token y que el usuario permanezca autenticado.

En ASP.NET Core podemos guardar información en cookies de forma segura, encriptadas fácilmente con IDataProtector. La aplicación web podría leer las cookies antes de realizar una llamada HTTP a la API protegida y revisar si el Access Token ya expiró. Es seguro, porque en el navegador jamás se tiene acceso al Refresh Token.

¿Pero qué tal un SPA? ¿Cómo guardamos de forma segura un Refresh Token que tiene un periodo de vida largo?

Usualmente los Access Token se guardan en local storage del navegador, que con XSS es factible el poder robar esa información. La forma de caer con XSS hoy en día es con ingeniería social, ¿Has visto esto en facebook al poner las developer tools?
Image description
Robar información del Local Storage es bien fácil si copias y pega código malicioso, por lo que guardar información en el navegador, siempre será inseguro.

Para una SPA, no tenemos opción, así como facebook no tiene opción. Guardar los Refresh Tokens, tendrán que ser en el mismo local storage (What?).

La forma para mitigar este problema será la siguiente:

Rotación de Refresh Tokens

Una implementación que explica Auth0 en su blog (referencia abajo) es el uso de Refresh tokens con un periodo de vida más corto y evitar el reuso de Refresh Tokens (rotar los refresh tokens).

Es decir, cada vez que un Refresh Token se usa, esté ya se invalida y se emite un nuevo Refresh Token al solicitar un nuevo Access Token.

Como ejemplo: si UsuarioA inicia sesión, se le otorga un access token y un refresh token. Digamos que el access token tendrá una vida de 1 hora y el refresh token tendrá un periodo de vida de 7 días.

Si el access token expira (será algo rápido), se solicita un nuevo access token con el refresh token obtenido desde el inicio. Se genera un nuevo access token y un nuevo refresh token. Tanto el access token y refresh token original ya no serán válidos por lo que ya no se podrán usar.

Si viene UsuarioMaloB y de alguna forma robó el refresh token original hay dos escenarios posibles aquí:

  • El UsuarioA utilizó el refresh token antes de que UsuarioMaloB lo usara, por lo que el refresh token robado ya queda invalidado. Si ese refresh token se intenta a volver a usar, el servidor de autenticación debe de invalidar todos los refresh tokens emitidos a ese usuario por seguridad, por lo que, al expirar el Access Token, tendrá que volver a iniciar sesión. De esta forma, el refresh token robado, no pudo haber sido usado por el UsuarioMaloB.
  • El UsuarioA NO utilizó el refresh token y UsuarioMaloB logró obtenerlo y usarlo. Aquí desafortunadamente, el UsuarioMaloB sí logrará robar la identidad de UsuarioA (por una hora), pero si UsuarioA utiliza el Refresh Token original (el que le robaron) este será detectado de que ya fue usado anteriormente, por lo que el servidor de autorización debe de invalidar todos los Refresh Tokens emitidos a ese usuario. Así UsuarioMaloB no podrá volver a usar un Refresh Token (por que, cada vez que se solicite un access token, se tendrá un nuevo refresh token, por eso hay que invalidar todos).

Es importante mencionar, que, si se detecta un reúso de refresh tokens, se deben de invalidar todos los refresh tokens por que se detectó un problema de seguridad. En este caso, es posible que tengas que avisar al usuario la posible brecha de seguridad.

Nota 👀: Este proceso queda abierto a interpretación, si estás preocupado por este concern, lo mejor es que uses servicios como Auth0 o Azure B2C para delegar la responsabilidad de este tema de seguridad, ya que estos servicios están dedicados a mitigar toda brecha de seguridad

Todo esto es con el supuesto, de que tienes una Single Page Application que debe de guardar los Refresh Tokens en local storage. Si tu aplicación es una Aplicación Web tradicional con backend, realmente no es necesario emplear este mecanismo. Utilizar Refresh Tokens con un periodo de vida largo (30 días por ejemplo) no es un problema si estos se guardan de forma segura (encriptados) en las cookies, aquí la rotación realmente no será necesaria.

Nota 👀: Si usas JWTs con periodo de vida muy largos, aunque estén guardados de una forma segura, no es buena idea. Imagina que das de baja ese usuario y este usuario "eliminado" tiene 30 días para hacer lo que quiera.

Si se roban las cookies, no podrán hacer algo, porque no podrán leer el Refresh Token. Si es un SPA, con XSS (o Self XSS) sí se puede caer en esta vulnerabilidad.

Lo mismo pasa con los access token, por eso SÍ o SÍ, deben de tener un periodo de vida corto. Si deseas ofrecer una buena experiencia de usuario, debes de implementar los Refresh Tokens.

Refresh Tokens en ASP.NET Core

💡 Aviso de actualización 💡: Si has estado siguiendo todas las partes de esta serie, en este post hubo cambios en la persistencia. En las partes anteriores usábamos SQLite por simplicidad, pero ahora decidí cambiarlo a SQL Server para manejar una solución "más real" para ser usado en producción. Definitivamente SQLite no será usado en este tipo de aplicaciones (se podría, pero bueno) por lo que te recomiendo siempre que veas el código y lo estudies, en lugar de seguir paso a paso estos artículos.

Para implementar Refresh Tokens en la solución que hemos estado construyendo, vamos a agregar una tabla nueva llamada RefreshTokens.

Dado que necesitamos mantener Tracking de los Refresh Tokens emitidos, saber a qué usuario pertenecen y cuando expiran, debemos de poder persistirlos en algún almacenamiento.

Por lo que agregaremos este Entity:

namespace MediatrExample.ApplicationCore.Domain;

public class RefreshToken
{
    public int RefreshTokenId { get; set; }
    public string RefreshTokenValue { get; set; }
    public bool Active { get; set; }
    public DateTime Expiration { get; set; }
    public bool Used { get; set; }
    public User User { get; set; }
    public string UserId { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Y lo agregamos al actual DbContext:

// ...código omitido
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
// código omitido...
Enter fullscreen mode Exit fullscreen mode

Los detalles de migraciones y update de la base de datos ya los estaré omitiendo, ya que no están en el scope de este post.

ApplicationCore -> Common -> Services -> AuthService

Crearemos este servicio para reutilizar la generación de JWTs, ya que ahora generaremos JWTs no solo desde el endpoint que hicimos antes, sino también ahora al usar el refresh token.

using MediatrExample.ApplicationCore.Domain;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace MediatrExample.ApplicationCore.Common.Services;
public class AuthService
{
    private readonly IConfiguration _config;
    private readonly UserManager<User> _userManager;

    public AuthService(IConfiguration config, UserManager<User> userManager)
    {
        _config = config;
        _userManager = userManager;
    }

    public async Task<string> GenerateAccessToken(User user)
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Sid, user.Id),
            new Claim(ClaimTypes.Name, user.UserName)
        };

        var roles = await _userManager.GetRolesAsync(user);

        foreach (var role in roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }

        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
        var tokenDescriptor = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: claims,
            expires: DateTime.Now.AddMinutes(60),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
    }
}

Enter fullscreen mode Exit fullscreen mode

Este es el mismo código que vimos en el post de JWT y Identity Core, pero ahora lo pasamos a una clase aparte para no copiar y pegar código.

Features -> Auth -> TokenCommand

Necesitamos actualizar el método de autenticación, porque ahora también generaremos el Refresh token (y aquí también usaremos el AuthService que acabamos de crear)

// ... código omitido

public class TokenCommandHandler : IRequestHandler<TokenCommand, TokenCommandResponse>
{
    private readonly UserManager<User> _userManager;
    private readonly AuthService _authService;
    private readonly MyAppDbContext _context;

    public TokenCommandHandler(UserManager<User> userManager, AuthService authService, MyAppDbContext context)
    {
        _userManager = userManager;
        _authService = authService;
        _context = context;
    }

    public async Task<TokenCommandResponse> Handle(TokenCommand request, CancellationToken cancellationToken)
    {
        var user = await _userManager.FindByNameAsync(request.UserName);

        if (user is null || !await _userManager.CheckPasswordAsync(user, request.Password))
        {
            throw new ForbiddenAccessException();
        }

        var jwt = await _authService.GenerateAccessToken(user);
        var newAccessToken = new RefreshToken
        {
            Active = true,
            Expiration = DateTime.UtcNow.AddDays(7),
            RefreshTokenValue = Guid.NewGuid().ToString("N"),
            Used = false,
            UserId = user.Id
        };

        _context.Add(newAccessToken);

        await _context.SaveChangesAsync();

        return new TokenCommandResponse
        {
            AccessToken = jwt,
            RefreshToken = newAccessToken.RefreshTokenValue
        };
    }
}

public class TokenCommandResponse
{
    public string AccessToken { get; set; } = default!;
    public string RefreshToken { get; set; } = default!;
}

Enter fullscreen mode Exit fullscreen mode

Ahora ya no solo regresamos el access token, sino también el refresh token. Queda guardado en la base de datos y ya, no hay más que hacer aquí.

Features -> Auth -> RefreshTokenCommand

Ahora que ya regresamos el refresh token, podemos crear el comando para generar un access token partiendo de un refresh token:

namespace MediatrExample.ApplicationCore.Features.Auth;

public class RefreshTokenCommand : IRequest<RefreshTokenCommandResponse>
{
    public string AccessToken { get; set; } = default!;
    public string RefreshToken { get; set; } = default!;
}

public class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, RefreshTokenCommandResponse>
{
    private readonly MyAppDbContext _context;
    private readonly AuthService _authService;
    private readonly ILogger<RefreshTokenCommand> _logger;

    public RefreshTokenCommandHandler(MyAppDbContext context, AuthService authService, ILogger<RefreshTokenCommand> logger)
    {
        _context = context;
        _authService = authService;
        _logger = logger;
    }

    public async Task<RefreshTokenCommandResponse> Handle(RefreshTokenCommand request, CancellationToken cancellationToken)
    {
        var refreshToken = await _context.RefreshTokens.FirstOrDefaultAsync(q => q.RefreshTokenValue == request.RefreshToken);

        // Refresh token no existe, expiró o fue revocado manualmente
        // (Pensando que el usuario puede dar click en "Cerrar Sesión en todos lados" o similar)
        if (refreshToken is null ||
            refreshToken.Active == false ||
            refreshToken.Expiration <= DateTime.UtcNow)
        {
            throw new ForbiddenAccessException();
        }

        // Se está intentando usar un Refresh Token que ya fue usado anteriormente,
        // puede significar que este refresh token fue robado.
        if (refreshToken.Used)
        {
            _logger.LogWarning("El refresh token del {UserId} ya fue usado. RT={RefreshToken}", refreshToken.UserId, refreshToken.RefreshTokenValue);

            var refreshTokens = await _context.RefreshTokens
                .Where(q => q.Active && q.Used == false && q.UserId == refreshToken.UserId)
                .ToListAsync();

            foreach (var rt in refreshTokens)
            {
                rt.Used = true;
                rt.Active = false;
            }

            await _context.SaveChangesAsync();

            throw new ForbiddenAccessException();
        }

        // TODO: Podríamos validar que el Access Token sí corresponde al mismo usuario

        refreshToken.Used = true;

        var user = await _context.Users.FindAsync(refreshToken.UserId);

        if (user is null)
        {
            throw new ForbiddenAccessException();
        }

        var jwt = await _authService.GenerateAccessToken(user);

        var newRefreshToken = new RefreshToken
        {
            Active = true,
            Expiration = DateTime.UtcNow.AddDays(7),
            RefreshTokenValue = Guid.NewGuid().ToString("N"),
            Used = false,
            UserId = user.Id
        };

        _context.Add(newRefreshToken);

        await _context.SaveChangesAsync();

        return new RefreshTokenCommandResponse
        {
            AccessToken = jwt,
            RefreshToken = newRefreshToken.RefreshTokenValue
        };
    }
}

public class RefreshTokenCommandResponse
{
    public string AccessToken { get; set; } = default!;
    public string RefreshToken { get; set; } = default!;
}
Enter fullscreen mode Exit fullscreen mode

Aquí están sucediendo un par de cosas que se ven (y son) sencillas:

  • Se revisa que el refresh token que se quiere usar exista, esté activo y que no esté expirado
  • Si el refresh token existe, es válido y no ha expirado, se revisa que no haya sido usado con anterioridad
    • Si es un refresh token que ya se usó, hay dos posibles escenarios:
    • Mala implementación de la aplicación cliente, utilizó un mismo refresh token
    • El refresh token fue robado y fue usado o no por un usuario malintencionado
    • Si el refresh token ya se usó, se desactivan todos los refresh tokens de ese usuario por seguridad
  • Si todo está bien, el refresh token se marca como usado y se vuelve a generar otro access token y otro refresh token

Para hacer pruebas, actualizamos el controller:

[HttpPost("refresh")]
public Task<RefreshTokenCommandResponse> RefreshToken([FromBody] RefreshTokenCommand command) =>
    _mediator.Send(command);
Enter fullscreen mode Exit fullscreen mode

Con la siguiente petición en Swagger:

curl -X 'POST' \
  'https://localhost:7113/api/auth' \
  -H 'accept: text/plain' \
  -H 'Content-Type: application/json' \
  -d '{
  "userName": "other_user",
  "password": "Passw0rd.1234"
}'
Enter fullscreen mode Exit fullscreen mode

Obtenemos esta respuesta:

{
  "accessToken": "eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiI4OWZiZjI2YS01MDM1LTRmYTktYTU3Ny03ZDFmYTliNTU4M2MiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoib3RoZXJfdXNlciIsImV4cCI6MTY1MDMyNzI5MywiaXNzIjoiV2ViQXBpSnd0LmNvbSIsImF1ZCI6ImxvY2FsaG9zdCJ9.NdXy7E10qNHvMjmhq6P8upl187eI3QGgGqXUquIJ1f0",
  "refreshToken": "10b105155efa4de38d796924518a7c8f"
}
Enter fullscreen mode Exit fullscreen mode

El refresh token es un token único, no guarda información como lo hace el access token. Lo importante aquí, es generar tokens aleatorios, únicos e imposibles de adivinar (un Guid funciona bien para eso).

Y si probamos el Refresh Command:

curl -X 'POST' \
  'https://localhost:7113/api/auth/refresh' \
  -H 'accept: text/plain' \
  -H 'Content-Type: application/json' \
  -d '{
  "accessToken": "eyJhbGciOiJodHRw....",
  "refreshToken": "10b105155efa4de38d796924518a7c8f"
}'
Enter fullscreen mode Exit fullscreen mode

Un nuevo access token y un nuevo refresh token.

{
  "accessToken": "eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiI4OWZiZjI2YS01MDM1LTRmYTktYTU3Ny03ZDFmYTliNTU4M2MiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoib3RoZXJfdXNlciIsImV4cCI6MTY1MDMyNzM5OCwiaXNzIjoiV2ViQXBpSnd0LmNvbSIsImF1ZCI6ImxvY2FsaG9zdCJ9.B9_HBjwdoFaaMutc5bAyzpc6gtD0sELYXaL6k-w0ZkE",
  "refreshToken": "8466d1f27ac9442788caf5c3f72d6c70"
}
Enter fullscreen mode Exit fullscreen mode

Si queremos usar nuevamente un mismo refresh token veremos la alerta (y de existir otros refresh tokens, digamos, en otros navegadores, todo se invalidará):

Image description

Pruebas de Integración

💡 Aviso de actualización 💡: Junto con los movimientos realizados de SQLite a SQL Server, se actualizó la clase TestBase para probar mejor la autenticación, revisa el código para más información.

Funcionalidad nueva que agregamos, funcionalidad que debemos de probar con las pruebas que vimos en un post anterior.

IntegrationTests -> RefreshTokenCommandTests

Para este caso, tendremos dos pruebas, una para sí generar un access token con un refresh token y otra prueba para comprobar que no se pueden usar refresh tokens que ya fueron usados.

// ...usings

namespace MediatRExample.IntegrationTests.Features.Auth;

public class RefreshTokenCommandTests : TestBase
{
    [Test]
    public async Task AccessTokenDenied_WithUsed_RefreshToken()
    {
        // Arrenge
        var (_, _, AuthInfo) = await CreateTestUser("testUser", "Pass.W0rd", Array.Empty<string>());

        // Usamos el Refresh Token
        var command = new RefreshTokenCommand
        {
            AccessToken = AuthInfo.AccessToken,
            RefreshToken = AuthInfo.RefreshToken
        };
        await SendAsync(command);
        var anonymHttpClient = Application.CreateClient();

        // Act
        var result = await anonymHttpClient.PostAsJsonAsync("api/auth/refresh", command);

        // Assert
        FluentActions.Invoking(() => result.EnsureSuccessStatusCode())
            .Should().Throw<HttpRequestException>();

        result.StatusCode.Should().Be(HttpStatusCode.Forbidden);
    }

    [Test]
    public async Task GetAccessToken_WithValid_RefreshToken()
    {
        // Arrenge
        var (_, _, AuthInfo) = await CreateTestUser("testUser", "Pass.W0rd", Array.Empty<string>());

        var command = new RefreshTokenCommand
        {
            AccessToken = AuthInfo.AccessToken,
            RefreshToken = AuthInfo.RefreshToken
        };
        var anonymHttpClient = Application.CreateClient();

        // Act
        var result = await anonymHttpClient.PostAsJsonAsync("api/auth/refresh", command);

        // Assert
        FluentActions.Invoking(() => result.EnsureSuccessStatusCode())
            .Should().NotThrow();

        var commandResponse = JsonSerializer.Deserialize<RefreshTokenCommandResponse>(result.Content.ReadAsStream(), new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        });

        commandResponse.Should().NotBeNull();
        commandResponse.AccessToken.Should().NotBeNull();
        commandResponse.RefreshToken.Should().NotBeNull();
    }
}
Enter fullscreen mode Exit fullscreen mode

Quedarían más escenarios que probar, por ejemplo, que, si existen más refresh tokens y simulamos un reúso de refresh tokens, estos deben de ser invalidados y no poder usarse.

Conclusión

La mayoría de las veces no se implementa bien la autenticación por Bearer Tokens, para mitigar esta mala experiencia de usuario se suelen usar access tokens con un periodo de vida largo, lo cual es una falla de seguridad.

Utilizar access tokens con una vida corta podría empeorar la experiencia del usuario y hacerlo ver que el sistema es difícil y molesto de usar, aunque nosotros estemos viendo principalmente por la seguridad de su información.

Encontrar el balance de esta situación con refresh tokens es lo ideal, access tokens cortos, refresh tokens que se invalidan, son prácticas que nos proveerán la seguridad necesaria para nuestras aplicaciones.

Recuerda, no siempre necesitarás refresh tokens, o si los necesitas, no siempre necesitarás rotarlos. Todo esto es gracias a la popularidad de las SPAs y lo inseguro que es un navegador.

Espero te sea de utilidad, para cualquier duda, puedes siempre buscarme en mi twitter en [@balunatic])(https://twitter.com/balunatic).

Referencias

Top comments (4)

Collapse
 
skynhero profile image
SkyNHerO

se agradece x la información brindada

Collapse
 
benomzn profile image
Jonathan Hinojos

Nice, buen post. Me imagino que para la opción de "Cerrar Sesión" desde el Front, tendrías un endpoint de /logout, donde recibes el Refresh Token y lo invalidas, cierto? También vi que para "invalidar" (por así llamarlo) a los AccesToken, se podría crear una tabla de blacklist, para los casos en que el usuario cierra sesión antes de la hora de expiración.

Saludos!

Collapse
 
koetelabs profile image
koetelabs

Primero de todos muchas gracias por los posts, son geniales.

Estoy siguiendo la serie y ahora me gustaría crear una aplicación web Razor que consuma de esa api y que el login de la propia web se realiza contra esa misma api. He visto en otros posts como hacer con un servidor OpenId pero con esta arquitectura no me queda muy claro como implementarlo.

Collapse
 
dsegura profile image
Delmirio Segura

Hola @isaacojeda, quisiera coordinar una reunión con usted. Donde te puedo escribir personal?