Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Identity Phone Number Token lifespan and SMS limit

I'm building 2 factor registration API using ASP.NET Identity 2.0.
I'd like to give users ability to confirm their phone numer on demand, so even if they didn't confirm they're phone number when registering they always can request new token (making request to my API) that will be send via SMS and enter it on page (also making request to my API).
In method that is responsible for sending token I'm generating token and sending it as shown below:

var token = await UserManager.GeneratePhoneConfirmationTokenAsync(user.Id);
var message = new SmsMessage
{
    Id = token,
    Recipient = user.PhoneNumber,
    Body = string.Format("Your token: {0}", token)
};
await UserManager.SmsService.SendAsync(message);

and inside UserManager:

public virtual async Task<string> GeneratePhoneConfirmationTokenAsync(TKey userId)
{
    var number = await GetPhoneNumberAsync(userId);
    return await GenerateChangePhoneNumberTokenAsync(userId, number);
}

Each time I call my method I get SMS message that contains token, problem is user can call that metod unlimited number of times and easily can generate costs - each SMS = cost.

I'd like to limit number of requests user can do to that method to one every X minutes.

Also I noticed that when I do multiple requests I get same token, I've tested my method and it looks that this token is valid for 3 minutes, so if I do request in that minutes time window I'll get same token.

Ideally I'd like to have single parameter that would allow me to specify time interval between requests and phone confirmation token lifespan.

I've tried setting token lifespan inside UserManager class using:

appUserManager.UserTokenProvider = new DataProtectorTokenProvider<User,int>(dataProtectionProvider.Create("ASP.NET Identity"))
{
    TokenLifespan = new TimeSpan(0,2,0)//2 minutes 
};

but this only affects tokens in email confirmation links.

Do I need to add extra field to my user table that will hold token validity date and check it every time I want to generate and send new token or is there easier way?

How can I specify time interval in which ASP.NET Identity will generate same phone number confirmation token?

like image 967
Misiu Avatar asked Oct 05 '16 10:10

Misiu


1 Answers

I'm no expert but i had the same question and found these two threads with a little help from google.

https://forums.asp.net/t/2001843.aspx?Identity+2+0+Two+factor+authentication+using+both+email+and+sms+timeout

https://github.com/aspnet/Identity/issues/465

I'm going to assume you are correct that the default time limit is 3minutes based on the AspNet Identity github discussion.

Hopefully the linked discussions contain the answers you need to configure a new time limit.

Regarding the rate limiting i'm using the following code which is loosely based on this discussions How do I implement rate limiting in an ASP.NET MVC site?

class RateLimitCacheEntry
{
    public int RequestsLeft;

    public DateTime ExpirationDate;
}

/// <summary>
/// Partially based on
/// https://stackoverflow.com/questions/3082084/how-do-i-implement-rate-limiting-in-an-asp-net-mvc-site
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class RateLimitAttribute : ActionFilterAttribute
{
    private static Logger Log = LogManager.GetCurrentClassLogger();

    /// <summary>
    /// Window to monitor <see cref="RequestCount"/>
    /// </summary>
    public int Seconds { get; set; }

    /// <summary>
    /// Maximum amount of requests to allow within the given window of <see cref="Seconds"/>
    /// </summary>
    public int RequestCount { get; set; }

    /// <summary>
    /// ctor
    /// </summary>
    public RateLimitAttribute(int s, int r)
    {
        Seconds = s;
        RequestCount = r;
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        try
        {
            var clientIP = RequestHelper.GetClientIp(actionContext.Request);

            // Using the IP Address here as part of the key but you could modify
            // and use the username if you are going to limit only authenticated users
            // filterContext.HttpContext.User.Identity.Name
            var key = string.Format("{0}-{1}-{2}",
                actionContext.ActionDescriptor.ControllerDescriptor.ControllerName,
                actionContext.ActionDescriptor.ActionName,
                clientIP
            );

            var allowExecute = false;

            var cacheEntry = (RateLimitCacheEntry)HttpRuntime.Cache[key];

            if (cacheEntry == null)
            {
                var expirationDate = DateTime.Now.AddSeconds(Seconds);

                HttpRuntime.Cache.Add(key,
                    new RateLimitCacheEntry
                    {
                        ExpirationDate = expirationDate,
                        RequestsLeft = RequestCount,
                    },
                    null,
                    expirationDate,
                    Cache.NoSlidingExpiration,
                    CacheItemPriority.Low,
                    null);

                allowExecute = true;
            }
            else
            {
                // Allow and decrement
                if (cacheEntry.RequestsLeft > 0)
                {
                    HttpRuntime.Cache.Insert(key,
                        new RateLimitCacheEntry
                        {
                            ExpirationDate = cacheEntry.ExpirationDate,
                            RequestsLeft = cacheEntry.RequestsLeft - 1,
                        },
                        null,
                        cacheEntry.ExpirationDate,
                        Cache.NoSlidingExpiration,
                        CacheItemPriority.Low,
                        null);

                    allowExecute = true;
                }
            }

            if (!allowExecute)
            {
                Log.Error("RateLimited request from " + clientIP + " to " + actionContext.Request.RequestUri);

                actionContext.Response
                    = actionContext.Request.CreateResponse(
                        (HttpStatusCode)429,
                        string.Format("You can call this {0} time[s] every {1} seconds", RequestCount, Seconds)
                    );
            }
        }
        catch(Exception ex)
        {
            Log.Error(ex, "Error in filter attribute");

            throw;
        }
    }
}

public static class RequestHelper
{
    /// <summary>
    /// Retrieves the client ip address from request
    /// </summary>
    public static string GetClientIp(HttpRequestMessage request)
    {
        if (request.Properties.ContainsKey("MS_HttpContext"))
        {
            return ((HttpContextWrapper)request.Properties["MS_HttpContext"]).Request.UserHostAddress;
        }

        if (request.Properties.ContainsKey(RemoteEndpointMessageProperty.Name))
        {
            RemoteEndpointMessageProperty prop;
            prop = (RemoteEndpointMessageProperty)request.Properties[RemoteEndpointMessageProperty.Name];
            return prop.Address;
        }

        return null;
    }
}

I've also seen this library recommended a few times: https://github.com/stefanprodan/WebApiThrottle

like image 82
Gunnar Már Óttarsson Avatar answered Oct 20 '22 21:10

Gunnar Már Óttarsson