Seguridad OWIN: cómo implementar tokens de actualización OAuth2

80

Estoy usando la plantilla Web Api 2 que viene con Visual Studio 2013 y tiene algún middleware OWIN para realizar la autenticación de usuario y similares.

En el OAuthAuthorizationServerOptionsnoté que el servidor OAuth2 está configurado para distribuir tokens que caducan en 14 días

 OAuthOptions = new OAuthAuthorizationServerOptions
 {
      TokenEndpointPath = new PathString("/api/token"),
      Provider = new ApplicationOAuthProvider(PublicClientId,UserManagerFactory) ,
      AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
      AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
      AllowInsecureHttp = true
 };

Esto no es adecuado para mi último proyecto. Me gustaría entregar bearer_tokens de corta duración que se pueden actualizar usando unrefresh_token

He hecho muchas búsquedas en Google y no encuentro nada útil.

Así que esto es lo lejos que he logrado llegar. Ahora he llegado al punto de "WTF do I now".

He escrito un RefreshTokenProviderque se implementa IAuthenticationTokenProvidersegún la RefreshTokenProviderpropiedad en la OAuthAuthorizationServerOptionsclase:

    public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
    {
       private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();

        public async Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            var guid = Guid.NewGuid().ToString();


            _refreshTokens.TryAdd(guid, context.Ticket);

            // hash??
            context.SetToken(guid);
        }

        public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        {
            AuthenticationTicket ticket;

            if (_refreshTokens.TryRemove(context.Token, out ticket))
            {
                context.SetTicket(ticket);
            }
        }

        public void Create(AuthenticationTokenCreateContext context)
        {
            throw new NotImplementedException();
        }

        public void Receive(AuthenticationTokenReceiveContext context)
        {
            throw new NotImplementedException();
        }
    }

    // Now in my Startup.Auth.cs
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/api/token"),
        Provider = new ApplicationOAuthProvider(PublicClientId,UserManagerFactory) ,
        AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
        AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(2),
        AllowInsecureHttp = true,
        RefreshTokenProvider = new RefreshTokenProvider() // This is my test
    };

Entonces, cuando alguien solicita un bearer_token, ahora estoy enviando un refresh_token, lo cual es genial.

Entonces, ¿cómo uso este refresh_token para obtener un nuevo bearer_token, presumiblemente necesito enviar una solicitud a mi punto final de token con algunos encabezados HTTP específicos configurados?

Solo pienso en voz alta mientras escribo ... ¿Debo manejar la expiración de refresh_token en mi SimpleRefreshTokenProvider? ¿Cómo obtendría un cliente uno nuevo refresh_token?

Realmente me vendría bien algún material de lectura / documentación porque no quiero equivocarme y me gustaría seguir algún tipo de estándar.

SimonGates
fuente
7
Hay un gran tutorial sobre cómo implementar tokens de actualización usando Owin y OAuth: bitoftech.net/2014/07/16/…
Philip Bergström

Respuestas:

76

Acabo de implementar mi servicio OWIN con Bearer (llamado access_token a continuación) y Refresh Tokens. Mi idea de esto es que puede utilizar diferentes flujos. Por lo tanto, depende del flujo que desee usar cómo configure sus tiempos de vencimiento de access_token y refresh_token.

Describiré dos flujos A y B a continuación (sugiero que lo que desea tener es el flujo B):

A) el tiempo de vencimiento de access_token y refresh_token es el mismo que por defecto 1200 segundos o 20 minutos. Este flujo necesita que su cliente primero envíe client_id y client_secret con datos de inicio de sesión para obtener un access_token, refresh_token y expiration_time. Con refresh_token ahora es posible obtener un nuevo access_token durante 20 minutos (o lo que sea que establezca AccessTokenExpireTimeSpan en OAuthAuthorizationServerOptions). Debido a que el tiempo de vencimiento de access_token y refresh_token son los mismos, su cliente es responsable de obtener un nuevo access_token antes del tiempo de vencimiento. Por ejemplo, su cliente podría enviar una llamada POST de actualización a su punto final de token con el cuerpo (observación: debe usar https en producción)

grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xxxxx

para obtener un nuevo token después de, por ejemplo, 19 minutos para evitar que caduquen.

B) en este flujo, desea tener un vencimiento a corto plazo para su access_token y un vencimiento a largo plazo para su refresh_token. Supongamos, con fines de prueba, que configuró el access_token para que expire en 10 segundos ( AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10)) y el refresh_token en 5 minutos. Ahora se trata de la parte interesante que establece el tiempo de vencimiento de refresh_token: haz esto en tu función createAsync en la clase SimpleRefreshTokenProvider de esta manera:

var guid = Guid.NewGuid().ToString();


        //copy properties and set the desired lifetime of refresh token
        var refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary)
        {
            IssuedUtc = context.Ticket.Properties.IssuedUtc,
            ExpiresUtc = DateTime.UtcNow.AddMinutes(5) //SET DATETIME to 5 Minutes
            //ExpiresUtc = DateTime.UtcNow.AddMonths(3) 
        };
        /*CREATE A NEW TICKET WITH EXPIRATION TIME OF 5 MINUTES 
         *INCLUDING THE VALUES OF THE CONTEXT TICKET: SO ALL WE 
         *DO HERE IS TO ADD THE PROPERTIES IssuedUtc and 
         *ExpiredUtc to the TICKET*/
        var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties);

        //saving the new refreshTokenTicket to a local var of Type ConcurrentDictionary<string,AuthenticationTicket>
        // consider storing only the hash of the handle
        RefreshTokens.TryAdd(guid, refreshTokenTicket);            
        context.SetToken(guid);

Ahora su cliente puede enviar una llamada POST con un refresh_token a su punto final de token cuando access_tokenexpire. La parte del cuerpo de la llamada puede verse así:grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xx

Una cosa importante es que es posible que desee utilizar este código no solo en su función CreateAsync sino también en su función Create. Por lo tanto, debería considerar utilizar su propia función (por ejemplo, llamada CreateTokenInternal) para el código anterior. Aquí puede encontrar implementaciones de diferentes flujos, incluido el flujo de refresh_token (pero sin establecer el tiempo de vencimiento del refresh_token)

Aquí hay una implementación de muestra de IAuthenticationTokenProvider en github (con la configuración del tiempo de vencimiento del refresh_token)

Lamento no poder ayudar con más material que las especificaciones de OAuth y la documentación de la API de Microsoft. Publicaría los enlaces aquí, pero mi reputación no me permite publicar más de 2 enlaces ...

Espero que esto pueda ayudar a otros a ahorrar tiempo al intentar implementar OAuth2.0 con un tiempo de expiración de refresh_token diferente al tiempo de expiración de access_token. No pude encontrar una implementación de ejemplo en la web (excepto la de thinktecture vinculada anteriormente) y me tomó algunas horas de investigación hasta que funcionó para mí.

Nueva información: En mi caso tengo dos posibilidades diferentes para recibir tokens. Uno es recibir un access_token válido. Allí tengo que enviar una llamada POST con un cuerpo de cadena en formato application / x-www-form-urlencoded con los siguientes datos

client_id=YOURCLIENTID&grant_type=password&username=YOURUSERNAME&password=YOURPASSWORD

En segundo lugar, si el access_token ya no es válido, podemos probar el refresh_token enviando una llamada POST con un cuerpo String en formato application/x-www-form-urlencodedcon los siguientes datosgrant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID

Freddy
fuente
1
uno de sus comentarios dice "considere almacenar solo el hash del identificador", ¿no debería ese comentario aplicarse a la línea anterior? El ticket contiene la guía original, pero solo almacenamos el hash de la guía RefreshTokens, por lo que si RefreshTokensse filtra, ¿¡un atacante no puede usar esa información !?
esskar
parece gustarle; preguntó el OA: github.com/thinktecture/Thinktecture.IdentityModel/commit/…
esskar
1
Como se describe en el flujo B, puede establecer el tiempo de vencimiento para access_token usando AccessTokenExpireTimeSpan = TimeSpan.FromMinutes (60) durante una hora o FromWHATEVER para el tiempo que desea que expire el access_token. Pero tenga en cuenta que si está usando refresh_token en su flujo, el tiempo de vencimiento de su refresh_token debería ser mayor que el de su access_token. Entonces, por ejemplo, 24 horas para access_token y 2 meses para refresh_token. Puede establecer el tiempo de vencimiento de access_token en la configuración de OAuth.
Freddy
12
No uses Guids para tus tokens ni hashes de ellos, no es seguro. Utilice el espacio de nombres System.Cryptography para generar una matriz de bytes aleatoria y convertirla en una cadena. De lo contrario, sus tokens de actualización pueden adivinarse mediante ataques de fuerza bruta.
Bon
1
@Bon ¿Vas a adivinar por fuerza bruta a un Guid? Su limitador de velocidad debería activarse antes de que el atacante pueda publicar incluso un puñado de solicitudes. Y si no, sigue siendo un Guid.
lonix
46

Necesita implementar RefreshTokenProvider . Primero cree la clase para RefreshTokenProvider, es decir.

public class ApplicationRefreshTokenProvider : AuthenticationTokenProvider
{
    public override void Create(AuthenticationTokenCreateContext context)
    {
        // Expiration time in seconds
        int expire = 5*60;
        context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(DateTime.Now.AddSeconds(expire));
        context.SetToken(context.SerializeTicket());
    }

    public override void Receive(AuthenticationTokenReceiveContext context)
    {
        context.DeserializeTicket(context.Token);
    }
}

Luego, agregue la instancia a OAuthOptions .

OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/authenticate"),
    Provider = new ApplicationOAuthProvider(),
    AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(expire),
    RefreshTokenProvider = new ApplicationRefreshTokenProvider()
};
Mauricio
fuente
Esto creará y devolverá un nuevo token de actualización cada vez, incluso aunque es posible que solo esté interesado en devolver un nuevo token de acceso y no un nuevo token de actualización también. Por ejemplo, cuando solicitamos un token de acceso, pero con un token de actualización y no con credenciales (nombre de usuario / contraseña). ¿Hay alguna manera de evitar esto?
Mattias
Puedes, pero no es bonito. El context.OwinContext.Environmentcontiene una Microsoft.Owin.Form#collectionclave que le da una FormCollectiondonde se puede encontrar el tipo de subvención y añadir una ficha en consecuencia. Se está filtrando la implementación, puede fallar en cualquier momento con actualizaciones futuras y no estoy seguro de si es portátil entre hosts OWIN.
hvidgaard
3
puede evitar emitir un nuevo token de actualización cada vez leyendo el valor "grant_type" del objeto OwinRequest, así: var form = await context.Request.ReadFormAsync(); var grantType = form.GetValue("grant_type"); luego emita el token de actualización si el tipo de concesión no es "refresh_token"
Duy
1
@mattias Todavía querrás devolver un nuevo token de actualización en ese escenario. De lo contrario, el cliente se tambalea después de actualizar por primera vez, porque el segundo token de acceso expira y no tiene forma de actualizar sin solicitar las credenciales nuevamente.
Eric Eskildsen
9

No creo que debas usar una matriz para mantener tokens. Tampoco necesitas una guía como token.

Puede usar context.SerializeTicket () fácilmente.

Vea mi código a continuación.

public class RefreshTokenProvider : IAuthenticationTokenProvider
{
    public async Task CreateAsync(AuthenticationTokenCreateContext context)
    {
        Create(context);
    }

    public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
    {
        Receive(context);
    }

    public void Create(AuthenticationTokenCreateContext context)
    {
        object inputs;
        context.OwinContext.Environment.TryGetValue("Microsoft.Owin.Form#collection", out inputs);

        var grantType = ((FormCollection)inputs)?.GetValues("grant_type");

        var grant = grantType.FirstOrDefault();

        if (grant == null || grant.Equals("refresh_token")) return;

        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(Constants.RefreshTokenExpiryInDays);

        context.SetToken(context.SerializeTicket());
    }

    public void Receive(AuthenticationTokenReceiveContext context)
    {
        context.DeserializeTicket(context.Token);

        if (context.Ticket == null)
        {
            context.Response.StatusCode = 400;
            context.Response.ContentType = "application/json";
            context.Response.ReasonPhrase = "invalid token";
            return;
        }

        if (context.Ticket.Properties.ExpiresUtc <= DateTime.UtcNow)
        {
            context.Response.StatusCode = 401;
            context.Response.ContentType = "application/json";
            context.Response.ReasonPhrase = "unauthorized";
            return;
        }

        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(Constants.RefreshTokenExpiryInDays);
        context.SetTicket(context.Ticket);
    }
}
peeyush rahariya
fuente
2

La respuesta de Freddy me ayudó mucho a que esto funcionara. En aras de la integridad, así es como puede implementar el hash del token:

private string ComputeHash(Guid input)
{
    byte[] source = input.ToByteArray();

    var encoder = new SHA256Managed();
    byte[] encoded = encoder.ComputeHash(source);

    return Convert.ToBase64String(encoded);
}

En CreateAsync:

var guid = Guid.NewGuid();
...
_refreshTokens.TryAdd(ComputeHash(guid), refreshTokenTicket);
context.SetToken(guid.ToString());

ReceiveAsync:

public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
    Guid token;

    if (Guid.TryParse(context.Token, out token))
    {
        AuthenticationTicket ticket;

        if (_refreshTokens.TryRemove(ComputeHash(token), out ticket))
        {
            context.SetTicket(ticket);
        }
    }
}
Knelis
fuente
¿Cómo ayuda el hash en este caso?
Ajaxe
3
@Ajaxe: la solución original almacenó el Guid. Con el hash no mantenemos el token de texto sin formato, sino su hash. Si almacena los tokens en una base de datos, por ejemplo, es mejor almacenar el hash. Si la base de datos está comprometida, los tokens no se pueden utilizar siempre que estén cifrados.
Knelis
No solo para protegerse contra amenazas externas, sino también para evitar que los empleados (que tienen acceso a la base de datos) roben los tokens.
lonix