9

I have seen this link Two Factor Auth using goolgle authenticator on how to create a two factor authentication in web api, but my requirements are little different.

  1. I want to use two factor authentication for issuing an access token. (If the user has chosen to enable two factor authentication)
  2. I would like to create the OTP code using ASP.NET identity itself. (Like the way we do in MVC web application SignInManager.SendTwoFactorCodeAsync("Phone Code")

The problem with my current implementation is, when I call SignInManager.SendTwoFactorCodeAsync("Phone Code") I get the error user id not found.

To debug, I tried calling User.Identity.GetUserId(); and it returns the correct user id.

I checked the source code of Microsoft.AspNet.Identity.Owin assembly

    public virtual async Task<bool> SendTwoFactorCodeAsync(string provider)
    {
        var userId = await GetVerifiedUserIdAsync().WithCurrentCulture();
        if (userId == null)
        {
            return false;
        }

        var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider).WithCurrentCulture();
        // See IdentityConfig.cs to plug in Email/SMS services to actually send the code
        await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token).WithCurrentCulture();
        return true;
    }

    public async Task<TKey> GetVerifiedUserIdAsync()
    {
        var result = await AuthenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.TwoFactorCookie).WithCurrentCulture();
        if (result != null && result.Identity != null && !String.IsNullOrEmpty(result.Identity.GetUserId()))
        {
            return ConvertIdFromString(result.Identity.GetUserId());
        }
        return default(TKey);
    }

As seen from the above code, SendTwoFactorCodeAsync method internally calls GetVerifiedUserIdAsync which checks the two factor authentication cookie. As this is a web api project, cookie isn't there and 0 is returned, resulting in user id not found error.

My question, how to correctly implement two factor authentication in web api using asp.net identity?

Steve Vinoski
  • 18,969
  • 3
  • 26
  • 38
Anand Murali
  • 3,902
  • 1
  • 27
  • 43

1 Answers1

13

This is what i have implemented to get this working on an api. i assume you are using the default ASP.NET single user template.

1. ApplicationOAuthProvider

inside GrantResourceOwnerCredentials method you must add this code

var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);

var twoFactorEnabled = await userManager.GetTwoFactorEnabledAsync(user.Id);
if (twoFactorEnabled)
{
 var code = await userManager.GenerateTwoFactorTokenAsync(user.Id, "PhoneCode");
 IdentityResult notificationResult = await userManager.NotifyTwoFactorTokenAsync(user.Id, "PhoneCode", code);
 if(!notificationResult.Succeeded){
   //you can add your own validation here
   context.SetError(error, "Failed to send OTP"); 
 }
}

// commented for clarification
ClaimIdentity oAuthIdentity .....

// Commented for clarification
AuthenticationProperties properties = CreateProperties(user);
// Commented for clarification

Inside CreateProperties method replace the paramenter with userObject like this:

public static AuthenticationProperties CreateProperties(ApplicationUser user)
{
  IDictionary<string, string> data = new Dictionary<string, string>
  {
    { "userId", user.Id },
    { "requireOTP" , user.TwoFactorEnabled.ToString() },
  }

// commented for clarification
}

The above code with check if the user has TFA enable, if its enabled it will generate verification code and send it using SMSService of your choice.

2. Create TwoFactorAuthorize Attribute

create response class ResponseData

public class ResponseData
{
    public int Code { get; set; }
    public string Message { get; set; }
}

add TwoFactorAuthorizeAttribute

public override async Task OnAuthorizationAsync(HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken)
    {
        #region Get userManager
        var userManager = HttpContext.Current.GetOwinContext().Get<ApplicationUserManager>();
        if(userManager == null)
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized, new ResponseData
            {
                Code = 100,
                Message = "Failed to authenticate user."
            });
            return;
        }
        #endregion

        var principal = actionContext.RequestContext.Principal as ClaimsPrincipal;

        #region Get current user
        var user = await userManager.FindByNameAsync(principal?.Identity?.Name);
        if(user == null)
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized, new ResponseData
            {
                Code = 100,
                Message = "Failed to authenticate user."
            });
            return;
        }
        #endregion

        #region Validate Two-Factor Authentication
        if (user.TwoFactorEnabled)
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized, new ResponseData
            {
                Code = 101,
                Message = "User must be authenticated using Two-Factor Authentication."
            });
        }
        #endregion

        return;
    }
}

3. Use TwoFactorAuthorizeAttribute

in a controller use TwoFactorAuthorizeAttribute

[Authorize]
[TwoFactorAuthorize]
public IHttpActionResult DoMagic(){
}

4. Verify OTP In your AccountController you must add the api end-point to verify the OTP

    [Authorize]
    [HttpGet]
    [Route("VerifyPhoneOTP/{code}")]
    public async Task<IHttpActionResult> VerifyPhoneOTP(string code)
    {
        try
        {
           bool verified = await UserManager.VerifyTwoFactorTokenAsync(User.Identity.GetUserId(), "PhoneCode", code);
            if (!verified)
                return BadRequest($"{code} is not a valid OTP, please verify and try again.");


            var result = await UserManager.SetTwoFactorEnabledAsync(User.Identity.GetUserId(), false);
            if (!result.Succeeded)
            {
                foreach (string error in result.Errors)
                    errors.Add(error);

                return BadRequest(errors[0]);
            }

            return Ok("OTP verified successfully.");
        }
        catch (Exception exception)
        {
            // Log error here
        }
    }
Spharah
  • 565
  • 6
  • 16
  • Hi Spharah, thanks a lot for the detailed answer. Can you also include the logic to validate the OTP code which is entered by the user? – Anand Murali Apr 10 '17 at 14:20
  • Hi Anand, i updated the answer to include the code to validate OTP, dont forget to upvote the answer :-) – Spharah Apr 10 '17 at 14:37
  • Spharah, I've up voted. Before accepting the answer, plz can you clear by doubt. After successful OTP verification, you set IsTwoFactorEnabled to false. Lets say, the user logs in again in another machine, now he will not get the OTP (right?), since IsTwoFactorEnabled is set to false. When do you re-enable it? – Anand Murali Apr 10 '17 at 17:22
  • Anand, you are correct this was the requirement on the application i was working on. To continuously asking the user to enter the OTP everytime the user login, you have to change the TwoFactorAuthorization code. I will prepare the solution for that scenario. – Spharah Apr 11 '17 at 06:41
  • PEO, no i dont have it. – Spharah Jul 14 '17 at 05:59
  • hI @Sparah I am struggling with 2FA https://stackoverflow.com/questions/45134662/asp-net-identity-provider-signinmanager-keeps-returning-failure-for-twofactorsig can you check this what i am doing wrong if you have spare time. – PEO Jul 17 '17 at 01:51
  • if (user.TwoFactorEnabled) is not for validation !! it is to check whether this user must pass by two factors v or not. PhoneNumberConfirmed value in addition to the TwoFactorEnabled are together responsible on the validation, You should edit your code to comply that – AbuDawood Aug 13 '20 at 22:20