Reputation: 1054
I have an API written in .NET Core. I have a token middleware that I'm having some issues with.
If I send a request to the middleware from one client at a time it's fine. If I try to use the middleware by more than one client at a time I get errors.
From what I can tell I think the _userManager
doesn't have a new instance created every time a request is made and I think this causes my problem. I seem to either get
System.InvalidOperationException: An attempt was made to use the context while it is being configured
or
System.ObjectDisposedException: Cannot access a disposed object
I'm thinking maybe the UserManager<ApplicationUser>
that I'm injecting is a singleton? Maybe both requests are trying to use the same instance of _userManager
and the first one disposes it and the second request errors?
I'm sorta new to this so maybe I'm way off base. Maybe _userManager
wasn't meant to be used this way or maybe I'm injecting it incorrectly.
Here is some code from the starup.cs and token middleware. Below that are some of the error logs.
Parts of startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(Environment.GetEnvironmentVariable("DbConnString"));
});
services.AddIdentity<ApplicationUser, IdentityRole>(o=>
{
o.Password.RequireDigit = false;
o.Password.RequiredLength = 0;
o.Password.RequireLowercase = false;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequireUppercase = false;
o.SignIn.RequireConfirmedEmail = true;
o.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = tokenValidationParameters;
});
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseJWTTokenProviderMiddleware(Options.Create(jwtOptions));
app.UseAuthentication();
app.UseMvc();
...
}
TokenProviderMiddleware
public class TokenProviderMiddleware
{
private readonly RequestDelegate _next;
private TokenProviderOptions _options;
private UserManager<ApplicationUser> _userManager;
private readonly ILogger<TokenProviderMiddleware> _logger;
public TokenProviderMiddleware(RequestDelegate next, IOptions<TokenProviderOptions> options, ILogger<TokenProviderMiddleware> logger)
{
_next = next;
_options = options.Value;
_logger = logger;
}
public async Task Invoke(HttpContext context, UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
{
await _next(context);
return;
}
if (!context.Request.Method.Equals("POST") || !context.Request.HasFormContentType)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Bad Request");
return;
}
ApplicationUser user = null;
string refreshToken;
string accessToken;
if (context.Request.Form["grant_type"].Equals("refresh_token"))
{
refreshToken = context.Request.Form["refresh_token"];
if(String.IsNullOrEmpty(refreshToken))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("refresh_token not sent");
return;
}
// Check that the refresh_token is valid
// I don't like using exceptions for flow control but it's how ValidateToken works
try
{
ValidateRefreshToken(refreshToken);
}
catch(SecurityTokenInvalidSignatureException ex)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("refresh_token failed validation");
return;
}
catch(Exception ex)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("refresh_token failed validation");
return;
}
user = await GetUserAsync(context, "from_refresh_token");
accessToken = await GenerateAccessToken(user);
var response = new
{
access_token = accessToken
};
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented }));
return;
}
private async Task<ApplicationUser> GetUserAsync(HttpContext context, string getUserFrom)
{
ApplicationUser user = null;
if(getUserFrom.Equals("from_username_password"))
{
string username = context.Request.Form["username"];
string password = context.Request.Form["password"];
if(String.IsNullOrEmpty(username) || String.IsNullOrEmpty(password))
{
throw new ArgumentException("Username/password not sent");
}
user = await _userManager.FindByNameAsync(username);
var result = _userManager.CheckPasswordAsync(user, password);
if (result.Result == false)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Invalid username or password");
}
if(!await _userManager.IsEmailConfirmedAsync(user))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Email address not confirmed");
}
}
else if (getUserFrom.Equals("from_refresh_token"))
{
JwtSecurityToken decodedRefreshToken = new JwtSecurityTokenHandler().ReadJwtToken(context.Request.Form["refresh_token"]);
string uid = decodedRefreshToken.Payload.Claims.FirstOrDefault(x => x.Type == "uid").Value;
user = await _userManager.FindByIdAsync(uid);
}
return user;
}
Error logs
2017-12-02 19:45:27.171 -05:00 [Information] Request starting HTTP/1.1 POST http://0.0.0.0:5000/api/token application/x-www-form-urlencoded 204
2017-12-02 19:45:27.171 -05:00 [Information] Request starting HTTP/1.1 POST http://0.0.0.0:5000/api/token application/x-www-form-urlencoded 204
2017-12-02 19:45:27.171 -05:00 [Debug] "POST" requests are not supported
2017-12-02 19:45:27.171 -05:00 [Debug] "POST" requests are not supported
2017-12-02 19:45:27.171 -05:00 [Debug] The request has an origin header: '"http://0.0.0.0:8080"'.
2017-12-02 19:45:27.171 -05:00 [Debug] The request has an origin header: '"http://0.0.0.0:8080"'.
2017-12-02 19:45:27.171 -05:00 [Information] Policy execution successful.
2017-12-02 19:45:27.172 -05:00 [Information] Policy execution successful.
2017-12-02 19:45:27.172 -05:00 [Debug] Connection id ""0HL9PS1UOQ2DS"", Request id ""0HL9PS1UOQ2DS:000000C7"": started reading request body.
2017-12-02 19:45:27.172 -05:00 [Debug] Connection id ""0HL9PS1UOQ2E0"", Request id ""0HL9PS1UOQ2E0:0000000B"": started reading request body.
2017-12-02 19:45:27.175 -05:00 [Debug] Connection id ""0HL9PS1UOQ2DS"", Request id ""0HL9PS1UOQ2DS:000000C7"": done reading request body.
2017-12-02 19:45:27.175 -05:00 [Debug] Connection id ""0HL9PS1UOQ2E0"", Request id ""0HL9PS1UOQ2E0:0000000B"": done reading request body.
2017-12-02 19:45:27.450 -05:00 [Information] Entity Framework Core "2.0.0-rtm-26452" initialized '"ApplicationDbContext"' using provider '"Npgsql.EntityFrameworkCore.PostgreSQL"' with options: "None"
2017-12-02 19:45:27.762 -05:00 [Debug] System.InvalidOperationException occurred, checking if Entity Framework recorded this exception as resulting from a failed database operation.
2017-12-02 19:45:27.762 -05:00 [Debug] Entity Framework did not record any exceptions due to failed database operations. This means the current exception is not a failed Entity Framework database operation, or the current exception occurred from a DbContext that was not obtained from request services.
2017-12-02 19:45:27.763 -05:00 [Error] An unhandled exception has occurred while executing the request
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. This can happen if a second operation is started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.
at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
at Microsoft.EntityFrameworkCore.DbContext.Set[TEntity]()
at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.get_UsersSet()
at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.FindByIdAsync(String userId, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Identity.UserManager`1.FindByIdAsync(String userId)
at MyProject.TokenProviderMiddleware.<GetUserAsync>d__9.MoveNext() in /home/jrow/Projects/MyProject/MyProject/Middleware/TokenProviderMiddleware.cs:line 220
System.ObjectDisposedException: Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
Object name: 'AsyncDisposer'.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.AsyncDisposer.Dispose()
at Microsoft.EntityFrameworkCore.Query.Internal.AsyncLinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.<MoveNext>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at System.Linq.AsyncEnumerable.<Aggregate_>d__6`3.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.<GetClaimsAsync>d__38.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Microsoft.AspNetCore.Identity.UserManager`1.<GetClaimsAsync>d__103.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at MyProject.TokenProviderMiddleware.<CreateClaims>d__10.MoveNext() in /home/jrow/Projects/MyProject/MyProject/Middleware/TokenProviderMiddleware.cs:line 239
Upvotes: 0
Views: 4416
Reputation: 1054
Fixed it by passing the variable around instead of globally newing it every time. (got an answer from reddit).
public async Task Invoke(HttpContext context, UserManager<ApplicationUser> userManager)
{
...
try
{
user = await GetUserAsync(_userManager, context, "from_refresh_token");
}
...
}
private async Task<ApplicationUser> GetUserAsync(UserManager<ApplicationUser> _userManager, HttpContext context, string getUserFrom)
{
//Do stuff with _userManager
}
Upvotes: 1
Reputation: 27944
If your UserManager is depending on a dbcontext, it should not be a singleton. The dbcontext should be created separately for each request. Make your UserManager transient to manage the dbcontext correctly.
Upvotes: 0