I have an ASP.NET Core Web API with a controller action that's secured using the [Authorize]
attribute. The authentication is managed through OpenIddict. When I submit a form and pass data from a CSHTML page, the [Authorize]
attribute correctly identifies the user, and everything works as expected.
However, when I send a valid bearer token from my Next.js web app, the API unexpectedly redirects me to the login page instead of authenticating and allowing access.
it should give me the relevant status code if valid 200 or if not 401 but instead of sending the status code it will redirect the request to the login page
how can I fix this issue
Here's a snippet of the controller action:
[HttpPost]
[Authorize]
public async Task<IActionResult> InviteUsers([FromBody] UserInviteViewModel inviteVM)
{
var user = User;
if (!ModelState.IsValid)
{
return BadRequest("Incorrect Arguments");
}
// ...
}
This is my startup class
public class Startup
{
public Startup(IConfiguration configuration)
=> Configuration = configuration;
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<SendGridMessageConfigurations>(Configuration.GetSection("SendGridMessage"));
services.AddTransient<IEmailSender, Services.MailService>();
services.AddControllersWithViews();
services.Configure<DataProtectionTokenProviderOptions>(o =>
o.TokenLifespan = TimeSpan.FromMinutes(5));
services.ConfigureApplicationCookie(o =>
{
o.ExpireTimeSpan = TimeSpan.FromHours(1);
o.SlidingExpiration = false;
});
services.AddDbContext<ApplicationDbContext>(options =>
{
// Configure the context to use Microsoft SQL Server.
options.UseNpgsql(
Configuration.GetConnectionString("identitydb"),
opt =>
{
opt.EnableRetryOnFailure(
maxRetryCount: 10,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorCodesToAdd: new List<string>() { });
opt.UseAdminDatabase("postgres");
});
options.UseOpenIddict();
});
// Register the Identity services.
services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
options.Lockout.MaxFailedAccessAttempts = 4;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
options.Password.RequiredLength = 8;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
options.ClaimsIdentity.EmailClaimType = Claims.Email;
});
services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
// Register the Quartz.NET service and configure it to block shutdown until jobs are complete.
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
services.Configure<CookieTempDataProviderOptions>(options =>
{
options.Cookie.IsEssential = true;
});
services.AddSession();
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
options.UseQuartz();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetDeviceEndpointUris("/connect/device")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetVerificationEndpointUris("/connect/verify")
.SetIntrospectionEndpointUris("/connect/introspect")
.SetAccessTokenLifetime(TimeSpan.FromMinutes(30))
.SetRefreshTokenLifetime(TimeSpan.FromHours(1));
options.AllowAuthorizationCodeFlow()
.AllowDeviceCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// Mark the "email", "profile", "roles" and "demo_api" scopes as supported scopes.
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, "demo_api");
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Force client applications to use Proof Key for Code Exchange (PKCE).
options.RequireProofKeyForCodeExchange();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableStatusCodePagesIntegration()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableVerificationEndpointPassthrough()
.DisableTransportSecurityRequirement(); // During development, you can disable the HTTPS requirement.
options.DisableSlidingRefreshTokenExpiration();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
options.AddAudiences("resource_server");
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
// services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.AddTransient<IJWTService, JWTService>();
services.AddCustomDaprClient();
services.AddHostedService<Worker>();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddCors();
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
services.AddAntiforgery(options =>
{
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(60);
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsProduction())
{
app.UseForwardedHeaders();
}
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseStatusCodePagesWithReExecute("/error");
app.UseRouting();
if (env.IsProduction())
{
app.Use((context, next) =>
{
context.Request.Scheme = "https";
return next();
});
}
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed(origin => true)
.AllowCredentials());
app.UseSession();
app.UseRequestLocalization(options =>
{
options.AddSupportedCultures("en-US", "fr-FR");
options.AddSupportedUICultures("en-US", "fr-FR");
options.SetDefaultCulture("en-US");
});
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<LogUserNameMiddleware>();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = PushSeriLogProperties;
});
app.UseEndpoints(options => options.MapControllerRoute(
name: "default",
pattern: "{controller=Account}/{action=Register}/{id?}"));
void PushSeriLogProperties(IDiagnosticContext diagnosticContext, HttpContext httpContext)
{
diagnosticContext.Set("UserAudit", httpContext.User.Identity.Name ?? "Anynymous");
}
}
Being redirected to a login page is the typical sign your request used cookie authentication instead of token authentication (when you're not logged in, the ASP.NET Core cookie authentication handler redirects you to the login page).
You can decorate your API action with [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
to force token authentication and return a proper 401 response when the access token is not valid.