Dnyati
Dnyati

Reputation: 179

Custom authorization filter in ASP.NET Core Web API

I have an ASP.NET Core 6.0 Web API which receives request from Angular web UI application. Currently I have implemented JWT token authorization as per this : [https://www.c-sharpcorner.com/article/jwt-token-authentication-and-authorizations-in-net-core-6-0-web-api/][1].

Objective I want to achieve is: user should be logged in single device only. If same user tries to login from 2nd device it should logout from first device then login to 2nd device. All API request coming from first device should be unauthorized.

To achieve this:

  1. I need to implement Access Token & Refresh token which is implemented.
  2. I need to invalidate token of 1st device when user tries to login from 2nd device.

As per my understanding point 2 can be implanted using custom authorization filter in Web API. Can anyone suggest if process is correct and would be great if some sample with this kind of implementation.

Upvotes: 0

Views: 554

Answers (1)

Md Farid Uddin Kiron
Md Farid Uddin Kiron

Reputation: 22409

Can anyone suggest if process is correct and would be great if some sample with this kind of implementation

The process is bit argumentative because it can be achieved in numerous way.

I am not quite sure hows your authentication looks like. But I am trying to share how It can be implement using session and asp.net core Identity.

As I already said, the process is bit argumentative as someone may say, they can do that using user claims or using Db interactions.

Even there might a argument how would I define the device uniqueness, so I have used a class where I am considering User browser info, MAC address, UserId or IP.

Using either of the above two I think we can define the device Identity. Let's have a look how we can implement that:

How I have planned the algorithm:

First I am sending the user email and password for login, where I am grabbing the user browser info, user MAC and IP address.

Let's have a look:

private async Task<DeviceInfo> GetCurrentDeviceIdentifier(HttpContext httpContext)
  {
      
      var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
    
      using (var client = new HttpClient())
      {
          var response = await client.GetStringAsync("https://api64.ipify.org?format=json");

         
          var ipAddress = JObject.Parse(response)["ip"].ToString();
         
          var combinedIdentifier = $"{ipAddress}_{userAgent}";
          //var hashedIdentifier = ComputeHash(combinedIdentifier);

          var _deviceInfo = new DeviceInfo();

          _deviceInfo.BrowserInfo = userAgent;
          _deviceInfo.MAC = GetMacAddress();
          return _deviceInfo;
      }
  }

Note: For better security we can make the device info in Hash. But I am skipping for demo.

Check User login:

var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
string? userId = _userManager.GetUserId(User);
DeviceInfo deviceIdentifier = await GetCurrentDeviceIdentifier(HttpContext);
deviceIdentifier.UserId = userId!;
var signInStatus = await GetDeviceIdInSession(deviceIdentifier);
if (signInStatus == "Authorized")
{

    if (result.Succeeded)
    {
        _logger.LogInformation("User logged in.");
        return LocalRedirect(returnUrl);
    }
    if (result.RequiresTwoFactor)
    {
        return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
    }
    if (result.IsLockedOut)
    {
        _logger.LogWarning("User account locked out.");
        return RedirectToPage("./Lockout");
    }
    else
    {
        ModelState.AddModelError(string.Empty, "Invalid login attempt.");
        return Page();
    }
}
else
{
    ModelState.AddModelError(string.Empty, $"Invalid login attempt. User already login in device {deviceIdentifier.MAC} browser info: {deviceIdentifier.BrowserInfo} ");
    return Page();
}

Preserve device info in session:

private async Task<bool> SetDeviceInfoInSession(DeviceInfo deviceInfo)

{
    var userDevice = HttpContext.Session.GetComplexObjectSession<DeviceInfo>("DeviceInfo");
    if (userDevice == null)
    {
        HttpContext.Session.SetComplexObjectSession("DeviceInfo", deviceInfo);
        return true;
    }
    else
    {
        return false;
    }    

}
private async Task<string> GetDeviceIdInSession(DeviceInfo deviceInfo)
{
    var loginUserDevice = HttpContext.Session.GetComplexObjectSession<DeviceInfo>("DeviceInfo");
    string authStatus = "";
    if (loginUserDevice == null)
    {
       await SetDeviceInfoInSession(deviceInfo);
       authStatus = "Authorized";
    }
    else
    {
        if (deviceInfo.MAC == loginUserDevice!.MAC || deviceInfo.BrowserInfo == loginUserDevice!.BrowserInfo)
        {
            authStatus = "Unauthorized";
        }
        else
        {
            authStatus = "Authorized";
        }
    }

    return authStatus;

}

Class I have used:

public class DeviceInfo
  {
      public string UserId { get; set; }
      public string? MAC { get; set; }
      public string? BrowserInfo { get; set; }
  }

Note: If need any other info you can modify the class.

Session Handler Method:

public static class SessionExtension
{
    //setting session
    public static void SetComplexObjectSession(this ISession session, string key, object value)
    {
        session.SetString(key, JsonConvert.SerializeObject(value));
    }

    //getting session
    public static T? GetComplexObjectSession<T>(this ISession session, string key)
    {
        var value = session.GetString(key);
        return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value);
    }
}

Program.cs:

builder.Services.AddSession(options =>
{
    options.Cookie.Name = "DeviceInfo";
    options.IdleTimeout = TimeSpan.FromMinutes(3);
    options.Cookie.IsEssential = true;
});

app.UseRouting();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();

Output:

enter image description here

Note: You also can do that using user claim or policy based authentication. Another thing is, it's a bit longer discussion, so within one question hard to explain everything without relevant code snippet. I would recommend you to post new question along with your specific question with relevant code snippet so that it can be answered well.

Upvotes: 0

Related Questions