Connecting Multifactor Authentication Multifactor in .NET Core

The article describes a method of connecting multifactor authentication for a site running on the .net core platform using built-in authorization mechanisms.


A few words why multifactor authentication is needed at all:


  1. Safety
  2. Safety again
  3. Convenience

Yes, the last point is not a mistake. The second and / or third authentication factor is not only an addition to the traditional password, but also a full replacement. Instead of sms with the code that needs to be interrupted on the site, modern methods are PUSH in the messenger with a button to confirm the action or biometric authentication using a fingerprint on the phone or laptop.


Principle of operation


  1. The site requests and verifies the username / password of the user
  2. If they are specified correctly, sends the user to the authentication page through the second factor
  3. After successful verification, the user returns to the site with an access token and logs in, that is, receives permission to view the closed part of the site.

Access token


β€” JWT (JSON Web Token). . , , JSON . : , . base64-url. ,


{
  "typ": "JWT",     // : JWT
  "alg": "HS256"    // : HMAC   SHA-256
}

, , , , . : , claims . , iss, aud, sub .


{
  "iss": "https://access.multifactor.ru",   // 
  "aud": "https://example.com",             // 
  "sub": "user@example.com",                // 
  "jti": "RxMEyo9",                         //id 
  "iat": 1571684399,                        // 
  "exp": 1571684699,                        // 
  "returnUrl": "/",                         // 
  "rememberMe": "False",                    // 
  "createdAt": "10/21/19 6:59:55 PM"        // 
}

JWT β€” , , HMAC-SHA256(message, secret), :


  • message β€” , base64-url ;
  • secret β€” , , .

JWT , , , , e-mail, .
JWT , , . , .


, ASP.NET Core Web Application. :


  1. .


appsettings.json API multifactor.ru


"Multifactor": {
    "ApiKey": "",
    "ApiSecret": "",
    "CallbackUrl": "https://localhost:44300/account/mfa"
  }

  • ApiKey ApiSecret
  • CallbackUrl β€”

API


API multifactor.ru


/// <summary>
/// Multifactor Client
/// </summary>
public class MultifactorService
{
    //    API
    private string _apiKey;
    private string _apiSecret;
    private string _callbackUrl;

    private string _apiHost = "https://api.multifactor.ru";

    public MultifactorService(string apiKey, string apiSecret, string callbackUrl)
    {
        _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
        _apiSecret = apiSecret ?? throw new ArgumentNullException(nameof(apiSecret));
        _callbackUrl = callbackUrl ?? throw new ArgumentNullException(nameof(callbackUrl));
    }

    public async Task<string> GetAccessPage(string identityName, IDictionary<string, string> claims = null)
    {
        if (string.IsNullOrEmpty(identityName)) throw new ArgumentNullException(nameof(identityName));

        var request = JsonConvert.SerializeObject(new
        {
            Identity = identityName,    //login 
            Callback = new
            {
                Action = _callbackUrl,  // 
                Target = "_self"
            },
            Claims = claims             // 
        });

        var payLoad = Encoding.UTF8.GetBytes(request);

        //basic authorization
        var authHeader = Convert.ToBase64String(Encoding.ASCII.GetBytes(_apiKey + ":" + _apiSecret));

        using var client = new WebClient();
        client.Headers.Add("Authorization", "Basic " + authHeader);
        client.Headers.Add("Content-Type", "application/json");

        var responseData = await client.UploadDataTaskAsync(_apiHost + "/access/requests", "POST", payLoad);
        var responseJson = Encoding.ASCII.GetString(responseData);

        var response = JsonConvert.DeserializeObject<MultifactorResponse<MultifactorAccessPage>>(responseJson);
        return response.Model.Url; //  
    }

    internal class MultifactorResponse<T>
    {
        public bool Success { get; set; }
        public T Model { get; set; }
    }

    internal class MultifactorAccessPage
    {
        public string Url { get; set; }
    }
}

β€” API . , appsettings.json. GetAccessPage , API .



Startup.cs, ConfigureServices API


//load Multifactor settings
var multifactorSection = Configuration.GetSection("Multifactor");
var apiKey = multifactorSection["ApiKey"];
var apiSecret = multifactorSection["ApiSecret"];
var callbackUrl = multifactorSection["CallbackUrl"];

//register Multifactor service
var multifactorService = new MultifactorService(apiKey, apiSecret, callbackUrl);
services.AddSingleton(multifactorService);

.net core . , . JWT β€” Bearer . , JWT HTTP "Authorization: Bearer", .NET Core , , .


.net JWT Bearer


services
    .AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(x =>
    {
        x.RequireHttpsMetadata = true;
        x.SaveToken = true;
        x.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(apiSecret)), //signature validation key
            ValidateIssuer = true,
            ValidIssuer = "https://access.multifactor.ru",
            ValidateAudience = false,
            NameClaimType = ClaimTypes.NameIdentifier
        };
    });

:


  • ValidateIssuerSigningKey β€”
  • IssuerSigningKey ApiSecret API
  • ValidateIssuer β€”
  • NameClaimType

Middleware


Configure , JWT Bearer


app.Use(async (context, next) =>
    {
        var token = context.Request.Cookies["jwt"];
        if (!string.IsNullOrEmpty(token))
        {
            context.Request.Headers.Add("Authorization", "Bearer " + token);
        }

        await next();
    });


//redirect to /account/login when unauthorized
    app.UseStatusCodePages(async context => {
        var response = context.HttpContext.Response;

        if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
        {
            response.Redirect("/account/login");
        }
    });

AccountController


, . , AccountController, .


API multifactor.ru


private MultifactorService _multifactorService;


[HttpPost("/account/login")]
public async Task<IActionResult> Login([Required]string login, [Required]string password)
{
    if (ModelState.IsValid)
    {
        // identity provider      
        var isValidUser = _identityService.ValidateUser(login, password, out string role);

        if (isValidUser)
        {
            var claims = new Dictionary<string, string> //         ,       
            {
                { "Role", role }
            };

            var url = await _multifactorService.GetAccessPage(login, claims);
            return RedirectPermanent(url);
        }
    }

    return View();
}

β€”


[HttpPost("/account/mfa")]
public IActionResult MultifactorCallback(string accessToken)
{
    //         
    Response.Cookies.Append("jwt", accessToken);
    return LocalRedirect("/");
}

, , Logout,


[HttpGet("/account/logout")]
public IActionResult Logout()
{
    Response.Cookies.Delete("jwt");
    return Redirect("/");
}

A working draft with the code from the article is available on GitHub . For a deeper understanding of how ClaimsIdentity-based authentication works in ASP.NET Core, check out this great article .

Source: https://habr.com/ru/post/undefined/


All Articles