Issue with AuthenticateAsync
If anyone can give me a clue as to where I am going wrong. I have slowly been upgrading my code base from .NET 6 to .NET 8. With my authorization controller, in the authorize method with .NET 6 this line of code
var results = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme)
```would return null or not succeed. I would then hit the Login Razor Page and Sign In, the same result would then be an actual `AuthenticateResult`.
With the upgrade to .NET 8, it always returns null even after I sign in. I have looked with the Dev Tools open and I can see that the sign in does generate a cookie. I am testing this from Postman using the authorization tab in there and using all the correct parameters.
Here is what is not changed from .NET 6 to .NET 8:
Authorize method:
```aspx-csharp
[HttpGet(Name = nameof(Authorize))]
[HttpPost(Name = nameof(Authorize))]
[IgnoreAntiforgeryToken]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the user principal stored in the authentication cookie.
// If it can't be extracted, redirect the user to the login page.
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (result == null || !result.Succeeded)
{
if (request.HasPrompt(Prompts.None))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
}
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
if (request.HasPrompt(Prompts.Login))
{
var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
var parameters = Request.HasFormContentType ?
Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();
parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
});
}
if (request.MaxAge != null && result.Properties.IssuedUtc != null &&
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))
{
if (request.HasPrompt(Prompts.None))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
}
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
var user = await _userManager.GetUserAsync(result.Principal) ??
throw new InvalidOperationException("The user details cannot be retrieved");
var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found");
var permissionsArray = await _applicationManager.GetPermissionsAsync(application);
var scopes = permissionsArray.Where(a => a.StartsWith("scp:")).Select(a => a[4..]).ToList().ToImmutableArray();
var authorizations = await _authorizationManager.FindAsync(
subject: await _userManager.GetUserIdAsync(user),
client: await _applicationManager.GetIdAsync(application),
status: Statuses.Valid,
type: AuthorizationTypes.Permanent,
scopes: scopes).ToListAsync();
switch (await _applicationManager.GetConsentTypeAsync(application))
{
case ConsentTypes.External when !authorizations.Any():
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client."
}));
case ConsentTypes.Implicit:
case ConsentTypes.External when authorizations.Any():
case ConsentTypes.Explicit when authorizations.Any() && !request.HasPrompt(Prompts.Consent):
var principal = await _signInManager.CreateUserPrincipalAsync(user);
principal.SetScopes(scopes);
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
var authorization = authorizations.LastOrDefault();
if (authorization == null)
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
subject: await _userManager.GetUserIdAsync(user),
client: await _applicationManager.GetIdAsync(application),
type: AuthorizationTypes.Permanent,
scopes: principal.GetScopes());
}
principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
case ConsentTypes.Explicit when request.HasPrompt(Prompts.None):
case ConsentTypes.Systematic when request.HasPrompt(Prompts.None):
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"Interactive user consent is required."
}));
default:
return View(new AuthorizeViewModel
{
ApplicationName = await _applicationManager.GetDisplayNameAsync(application),
Scope = string.Join(" ", scopes)
});
}
}
Login page:
[AllowAnonymous]
public class LoginModel : PageModel
{
private readonly ApplicationUserManager _userManager;
private readonly SignInManager<ApplicationUsers> _signInManager;
private readonly ILogger<LoginModel> _logger;
public LoginModel(SignInManager<ApplicationUsers> signInManager,
ILogger<LoginModel> logger,
ApplicationUserManager userManager)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
}
[BindProperty]
public InputModel Input { get; set; }
public IList<AuthenticationScheme> ExternalLogins { get; set; }
public string ReturnUrl { get; set; }
[TempData]
public string ErrorMessage { get; set; }
public class InputModel
{
//Snipped fro brevity
}
public async Task OnGetAsync(string returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
{
ModelState.AddModelError(string.Empty, ErrorMessage);
}
returnUrl ??= Url.Content("~/");
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
if (ModelState.IsValid)
{
var user = await _userManager.FindByNameAsync(Input.Email);
if (user != null)
{
if (!await _signInManager.CanSignInAsync(user) || (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user)))
{
_logger.LogInformation("Login was unsuccessful through controller");
}
if (!await _userManager.CheckPasswordAsync(user, Input.Password))
{
_logger.LogInformation("Login was unsuccessful through controller");
await _userManager.AccessFailedAsync(user);
}
if (_userManager.SupportsUserLockout)
{
await _userManager.ResetAccessFailedCountAsync(user);
}
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, 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.");
return Page();
}
}
return Page();
}
}
Here is what I have changed in the .NET 6 to .NET 8 basically a move from startup.cs
to program.cs
:
.NET 6 startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("Policy", builder =>
{
builder.WithOrigins(Configuration.GetSection("Cors:Origins").GetChildren().Select(c => c.Value).ToArray())
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
services.AddControllers();
services.AddRazorPages();
services.AddDbContext<IdentDbContext>(options =>
{
options.UseSqlServer(
Configuration.GetConnectionString("IdentityDB"));
options.UseOpenIddict();
});
services.AddDataProtection()
.PersistKeysToDbContext<IdentDbContext>()
.SetDefaultKeyLifetime(TimeSpan.FromDays(7))
.SetApplicationName("Applications");
services.AddIdentity<ApplicationUsers, ApplicationRoles>()
.AddEntityFrameworkStores<IdentDbContext>()
.AddUserStore<ApplicationUserStore>()
.AddRoleStore<ApplicationRoleStore>()
.AddRoleManager<ApplicationRoleManager>()
.AddUserManager<ApplicationUserManager>()
.AddErrorDescriber<ApplicationIdentityErrorDescriber>()
.AddDefaultTokenProviders()
.AddDefaultUI();
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
// Configure the options for the Identity Account
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedAccount = true;
options.User.RequireUniqueEmail = true;
options.Lockout.MaxFailedAccessAttempts = 3;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
});
services.Configure<SecurityStampValidatorOptions>(options =>
{
options.ValidationInterval = TimeSpan.FromSeconds(1);
});
services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<IdentDbContext>();
options.UseQuartz();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
options.SetAuthorizationEndpointUris("/api/Authorization/Authorize")
.SetTokenEndpointUris("/Token")
.SetIntrospectionEndpointUris("/Introspect")
.SetUserinfoEndpointUris("/api/Userinfo/Userinfo")
.SetVerificationEndpointUris("/Verify");
options.RegisterScopes(Scopes.OpenId, Scopes.Email, Scopes.Profile, Scopes.Roles);
options.UseReferenceAccessTokens()
.UseReferenceRefreshTokens()
.UseDataProtection();
options.AllowClientCredentialsFlow()
.AllowAuthorizationCodeFlow()
.RequireProofKeyForCodeExchange()
.AllowRefreshTokenFlow();
if (_env.IsDevelopment())
{
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
}
else if (_env.IsProduction() || _env.IsStaging())
{
options.AddSigningCertificate(Configuration.GetSection("CertifcateThumbprints:SigningCertificate").Value)
.AddEncryptionCertificate(Configuration.GetSection("CertifcateThumbprints:EncryptionCertificate").Value);
}
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableStatusCodePagesIntegration()
.EnableUserinfoEndpointPassthrough()
.EnableVerificationEndpointPassthrough();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
options.UseLocalServer();
options.UseDataProtection();
options.UseSystemNetHttp();
options.UseAspNetCore();
});
services.AddSwaggerGen(swagger =>
{
swagger.OperationFilter<SwaggerDefaultValues>();
swagger.OperationFilter<AuthenticationRequirementOperationFilter>();
swagger.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
swagger.IncludeXmlComments(xmlPath);
});
services.AddApiVersioning();
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVVV";
options.DefaultApiVersion = ApiVersion.Parse("0.10.alpha");
options.AssumeDefaultVersionWhenUnspecified = true;
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
if (_env.IsDevelopment())
{
services.AddHostedService<TestData>();
}
else if (_env.IsStaging() || _env.IsProduction())
{
services.AddHostedService<ProdStageSeed>();
}
}
public void Configure(IApplicationBuilder app, IApiVersionDescriptionProvider provider)
{
if (_env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseStatusCodePagesWithReExecute("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseCors("Policy");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.DisplayOperationId();
var versionDescription = provider.ApiVersionDescriptions;
foreach (var description in provider.ApiVersionDescriptions.OrderByDescending(_ => _.ApiVersion))
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"API {description.GroupName}");
}
});
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
});
}
.NET 8 program.cs
:
builder.Services.AddCors(options =>
{
options.AddPolicy("Policy", opt =>
{
opt.WithOrigins(builder.Configuration.GetSection("Cors:Origins").GetChildren().Select(c => c.Value).ToArray()!)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
builder.Services.AddControllersWithViews()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new EntityIdJsonConverterFactory());
});
builder.Services.AddRazorPages();
builder.Services.AddDbContext<IdentDbContext>(options =>
{
options.UseSqlServer(
builder.Configuration.GetConnectionString("IdentityDB"));
options.UseOpenIddict();
});
builder.Services.AddDataProtection()
.PersistKeysToDbContext<IdentDbContext>()
.SetDefaultKeyLifetime(TimeSpan.FromDays(7))
.SetApplicationName("Applications");
builder.Services.AddIdentity<ApplicationUsers, ApplicationRoles>()
.AddEntityFrameworkStores<IdentDbContext>()
.AddUserStore<ApplicationUserStore>()
.AddRoleStore<ApplicationRoleStore>()
.AddRoleManager<ApplicationRoleManager>()
.AddUserManager<ApplicationUserManager>()
.AddErrorDescriber<ApplicationIdentityErrorDescriber>()
.AddDefaultTokenProviders()
.AddDefaultUI();
builder.Services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedAccount = true;
options.User.RequireUniqueEmail = true;
options.Lockout.MaxFailedAccessAttempts = 3;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
});
builder.Services.Configure<SecurityStampValidatorOptions>(options =>
{
options.ValidationInterval = TimeSpan.FromSeconds(1);
});
builder.Services.AddQuartz(options =>
{
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
builder.Services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<IdentDbContext>();
options.UseQuartz();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
options.SetAuthorizationEndpointUris("api/Authorization/Authorize")
.SetTokenEndpointUris("Token")
.SetIntrospectionEndpointUris("Introspect")
.SetUserinfoEndpointUris("api/Userinfo/Userinfo")
.SetVerificationEndpointUris("Verify");
options.RegisterScopes(Scopes.OpenId, Scopes.Email, Scopes.Profile, Scopes.Roles);
options.UseReferenceAccessTokens()
.UseReferenceRefreshTokens()
.UseDataProtection();
options.AllowClientCredentialsFlow()
.AllowAuthorizationCodeFlow()
.RequireProofKeyForCodeExchange()
.AllowRefreshTokenFlow();
if (builder.Environment.IsDevelopment())
{
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
}
else if (builder.Environment.IsProduction() || builder.Environment.IsStaging())
{
options.AddSigningCertificate(builder.Configuration.GetSection("CertifcateThumbprints:SigningCertificate").Value!)
.AddEncryptionCertificate(builder.Configuration.GetSection("CertifcateThumbprints:EncryptionCertificate").Value!);
}
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableStatusCodePagesIntegration()
.EnableUserinfoEndpointPassthrough()
.EnableVerificationEndpointPassthrough();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
options.UseLocalServer();
options.UseDataProtection();
options.UseSystemNetHttp();
options.UseAspNetCore();
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(swagger =>
{
swagger.OperationFilter<SwaggerDefaultValues>();
swagger.OperationFilter<AuthenticationRequirementOperationFilter>();
swagger.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
swagger.IncludeXmlComments(xmlPath);
});
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(0, 10, "alpha");
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
}).AddMvc()
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVVV";
options.SubstituteApiVersionInUrl = false;
});
builder.Services.AddApplication()
.AddInfrastructure(builder.Configuration)
.AddDataLibrary();
builder.Services.AddHttpClient("Login", sp => { sp.BaseAddress = new Uri(builder.Configuration.GetSection("BaseAddresses:Api").Value!); });
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
if (builder.Environment.IsDevelopment())
{
builder.Services.AddHostedService<TestData>();
builder.Services.AddHostedService<ProdStageSeed>();
}
else if (builder.Environment.IsStaging() || builder.Environment.IsProduction())
{
builder.Services.AddHostedService<ProdStageSeed>();
}
builder.Host.UseWolverine(opts =>
{
opts.PersistMessagesWithSqlServer(builder.Configuration.GetConnectionString("IdentityDB")!);
opts.UseEntityFrameworkCoreTransactions();
opts.Policies.AutoApplyTransactions();
opts.Policies.UseDurableLocalQueues();
opts.Discovery.IncludeAssembly(Assembly.Load("IdentityApplication"));
opts.UseFluentValidation();
opts.Services.AddResourceSetupOnStartup();
opts.OnException<SqlException>().MoveToErrorQueue();
});
builder.Services.AddTransient<ExceptionMiddleware>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment() || app.Environment.IsStaging())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
app.UseSwaggerUI(c =>
{
c.DisplayOperationId();
var versionDescription = provider.ApiVersionDescriptions;
foreach (var description in provider.ApiVersionDescriptions.OrderByDescending(_ => _.ApiVersion))
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"API {description.GroupName}");
}
});
}
else
{
app.UseStatusCodePagesWithReExecute("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseCors("Policy");
app.UseSerilogRequestLogging();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<ExceptionMiddleware>();
app.MapRazorPages();
app.MapControllers();
app.Run();
So with the minimal change I made I cannot figure out why the result is always returning null. A cookie is generated by the Login Page and does show up in program and Postman. I understand that the AuthenticateAsync changed in .NET 7 but I cannot find any good documentation as to what might be affected in my code. Not sure what the cause is, but I have been stuck on this for over a coupe of weeks.