Forum Discussion

MikeRM2's avatar
MikeRM2
Copper Contributor
Oct 13, 2024

Issue with Authentication

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:

 

[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.

No RepliesBe the first to reply

Resources