简体   繁体   中英

C# EntityFramework Core Endopoint JWT Authentication

I'm working on an Web API written in ASP.NET Core with Entity Framework Core.

Currently i'm facing a problem which prevents me from sleep :)

I protect my endpoints using a class which is called TokenProviderMiddleWare (followed on this tutorial page: https://stormpath.com/blog/token-authentication-asp-net-core )

This is the function where I retrieve the user from the database and check if the provided password is a match with the database:

        private Task<ClaimsIdentity> GetUserIdentity(string email, string password)
        {
            var driver = context.Drivers.SingleOrDefault(d => d.Email == email);

            if (driver == null)
                return Task.FromResult<ClaimsIdentity>(null);

            if (driver.Password != password)
            {
                SetBadLoginAttempt(driver);
                context.SaveChangesAsync();
                return Task.FromResult<ClaimsIdentity>(null);
            }

            if (driver.IsLoginDisabled)
            {
                return Task.FromResult<ClaimsIdentity>(null);
            }

            ResetBadLoginAttempt(driver);
            context.SaveChangesAsync();

            return Task.FromResult(new ClaimsIdentity(
                new System.Security.Principal.GenericIdentity(email, "Token"),
                new Claim[] {
                    new Claim("fullName", driver.Name),
                }
            ));
        }`

If I run an login twice at the same moment, I got this error:

Connection id "0HL5KO6M27JFT": An unhandled exception was thrown by the application.
System.InvalidOperationException: An attempt was made to use the context 
while it is being configured. A DbContext instance cannot be used inside OnConfiguring since it is still being configured at this
point.

It's fixed by doing this on the first line in this function:

Driver driver = null;
lock(context)
{
    driver = context.Drivers.SingleOrDefault(d => d.Email == email);
}

But I think this is ugly and not scalable.

So in short, I want to check my user in the database via EntityFramework. My DbContext is injected via the constructor by .NET Core. And I think there is some kind of concurrency issue...

This class where I use this code in looks like this:

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using SmartoonAPI.Persistence;
using System.Linq;
using System.Collections.Generic;
using SmartoonDomain.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace SmartoonAPI.JWT
{
public class TokenProviderMiddleware
{
    private readonly RequestDelegate next;
    private readonly TokenProviderOptions options;
    private readonly ISmartoonContext context;

    public TokenProviderMiddleware(RequestDelegate next, IOptions<TokenProviderOptions> options, ISmartoonContext context)
    {
        this.context = context;
        this.next = next;
        this.options = options.Value;
    }

    public Task Invoke(HttpContext context)
    {
        // If the request path doesn't match, skip
        if (!context.Request.Path.Equals(options.Path, StringComparison.Ordinal))
        {
            return next(context);
        }

        // Request must be POST with Content-Type: application/x-www-form-urlencoded
        if (!context.Request.Method.Equals("POST")
           || !context.Request.HasFormContentType)
        {
            context.Response.StatusCode = 400;
            return context.Response.WriteAsync("Bad request.");
        }

        return GenerateToken(context);
    }

    private async Task GenerateToken(HttpContext context)
    {
        ClaimsIdentity identity;
        if (!string.IsNullOrEmpty(context.Request.Form["email"]) && !string.IsNullOrEmpty(context.Request.Form["password"]))
            identity = await GetUserIdentity(context.Request.Form["email"], context.Request.Form["password"]);
        else
            identity = await GetApplicationIdentity(context.Request.Form["appid"], context.Request.Form["secret"]);

        if (identity == null)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsync("Login failed!");
            return;
        }

        var now = DateTime.UtcNow;

        // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
        // You can add other claims here, if you want:
        var claims = new List<Claim>()
        {
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(), ClaimValueTypes.Integer64)
        };

        claims.AddRange(identity.Claims);

        // Create the JWT and write it to a string
        var jwt = new JwtSecurityToken(
            issuer: options.Issuer,
            audience: options.Audience,
            claims: claims,
            notBefore: now,
            expires: now.Add(options.Expiration),
            signingCredentials: options.SigningCredentials);
        var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

        var response = new
        {
            access_token = encodedJwt,
            expires_in = (int)options.Expiration.TotalSeconds
        };

        // Serialize and return the response
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented }));
    }

    private LoginAble SetBadLoginAttempt(LoginAble loginAble)
    {
        if (loginAble.LoginDisabledOn.HasValue && (DateTime.Now - loginAble.LoginDisabledOn.Value).TotalMinutes > 30)
        {
            ResetBadLoginAttempt(loginAble);
            loginAble.BadLoginAttempt++;
            return loginAble;
        }

        loginAble.BadLoginAttempt++;

        if (loginAble.BadLoginAttempt < 3)
        {
            return loginAble;
        }
        else
        {
            loginAble.IsLoginDisabled = true;
            loginAble.LoginDisabledOn = DateTime.Now;
        }
        return loginAble;
    }

    private LoginAble ResetBadLoginAttempt(LoginAble loginAble)
    {
        loginAble.BadLoginAttempt = 0;
        loginAble.IsLoginDisabled = false;
        loginAble.LoginDisabledOn = null;
        return loginAble;
    }

    private Task<ClaimsIdentity> GetUserIdentity(string email, string password)
    {
        var driver = context.Drivers.SingleOrDefault(d => d.Email == email);

        if (driver == null)
            return Task.FromResult<ClaimsIdentity>(null);

        if (driver.Password != password)
        {
            SetBadLoginAttempt(driver);
            context.SaveChangesAsync();
            return Task.FromResult<ClaimsIdentity>(null);
        }

        if (driver.IsLoginDisabled)
        {
            return Task.FromResult<ClaimsIdentity>(null);
        }

        ResetBadLoginAttempt(driver);
        context.SaveChangesAsync();

        return Task.FromResult(new ClaimsIdentity(
            new System.Security.Principal.GenericIdentity(email, "Token"),
            new Claim[] {
                new Claim("fullName", driver.Name),
            }
        ));
    }

    private Task<ClaimsIdentity> GetApplicationIdentity(string appId, string secret)
    {
        var appCredential = context.AppCredentials.SingleOrDefault(a => a.AppId == appId);

        if (appCredential == null)
            return Task.FromResult<ClaimsIdentity>(null);

        if (appCredential.Secret != secret)
        {
            SetBadLoginAttempt(appCredential);
            context.SaveChangesAsync();
            return Task.FromResult<ClaimsIdentity>(null);
        }

        ResetBadLoginAttempt(appCredential);
        context.SaveChangesAsync();

        return Task.FromResult(new ClaimsIdentity(
            new System.Security.Principal.GenericIdentity(appCredential.AppId, "Token"),
            new Claim[] {
                new Claim("description", appCredential.Description),
            }
        ));
    }
}
}

The ASP.NET Core middleware gets instantiated once per application while the database context by default gets instantiated on every request and gets disposed upon its completion. Injecting the database context directly into Invoke method should solve the issue.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM