Prevent redirect to /Account/Login in asp.net core 2.2 - asp.net-core-2.2

I am trying to prevent the app to redirect to /Account/Login in asp.net core 2.2 when the user isn't logged in.
Even though i write LoginPath = new PathString("/api/contests"); any unauthorized requests are still redirected to /Account/Login
This is my Startup.cs:
using System;
using System.Reflection;
using AutoMapper;
using Contest.Models;
using Contest.Tokens;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Swashbuckle.AspNetCore.Swagger;
namespace Contest
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "clientapp/build";
});
// ===== Add our DbContext ========
string connection = Configuration.GetConnectionString("DBLocalConnection");
string migrationAssemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddDbContext<ContestContext>(options =>
options.UseSqlServer(connection,
sql => sql.MigrationsAssembly(migrationAssemblyName)));
// ===== Add Identity ========
services.AddIdentity<User, IdentityRole>(o =>
{
o.User.RequireUniqueEmail = true;
o.Tokens.EmailConfirmationTokenProvider = "EMAILCONF";
// I want to be able to resend an `Email` confirmation email
// o.SignIn.RequireConfirmedEmail = true;
}).AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ContestContext>()
.AddTokenProvider<EmailConfirmationTokenProvider<User>>("EMAILCONF")
.AddDefaultTokenProviders();
services.Configure<DataProtectionTokenProviderOptions>(o =>
o.TokenLifespan = TimeSpan.FromHours(3)
);
services.Configure<EmailConfirmationTokenProviderOptions>(o =>
o.TokenLifespan = TimeSpan.FromDays(2)
);
// ===== Add Authentication ========
services.AddAuthentication(o => o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "auth_cookie";
options.Cookie.SameSite = SameSiteMode.None;
options.LoginPath = new PathString("/api/contests");
options.AccessDeniedPath = new PathString("/api/contests");
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
},
};
});
// ===== Add Authorization ========
services.AddAuthorization(o =>
{
});
services.AddCors();
// ===== Add MVC ========
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// ===== Add Swagger ========
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Title = "Core API",
Description = "Documentation",
});
var xmlPath = $"{System.AppDomain.CurrentDomain.BaseDirectory}Contest.xml";
c.IncludeXmlComments(xmlPath);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseSpaStaticFiles(new StaticFileOptions()
{
});
app.UseCors(policy =>
{
policy.AllowAnyHeader();
policy.AllowAnyMethod();
policy.AllowAnyOrigin();
policy.AllowCredentials();
});
app.UseAuthentication();
app.UseMvc();
if (env.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Core API");
});
}
app.UseSpa(spa =>
{
spa.Options.SourcePath = "clientapp";
if (env.IsDevelopment())
{
// spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
}
});
}
}
}
I managed to bypass this by creating a controller to handle this route:
[Route("/")]
[ApiController]
public class UnauthorizedController : ControllerBase
{
public UnauthorizedController()
{
}
[HttpGet("/Account/Login")]
[AllowAnonymous]
public IActionResult Login()
{
HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
return new ObjectResult(new
{
StatusCode = StatusCodes.Status401Unauthorized,
Message = "Unauthorized",
});
}
}
What is wrong with my setup?
Thanks

Hey there and welcome to StackOverflow 👋
The behaviour you experience is linked to the fact that you use ASP.NET Identity.
When you call services.AddIdentity, behind the scenes a cookie-based authentication scheme is registered and set as the default challenge scheme, as you can see in the code here on GitHub.
Even though you registered a cookie authentication scheme yourself and set it as the default scheme, the specific default schemes — like AuthenticateScheme, ChallengeScheme, SignInScheme, etc... — take precendence. DefaultScheme is used by the authentication system only when the specific one is not set.
To answer your question, you could apply the configuration settings to the ASP.NET Identity cookie options by using the helper method services.ConfigureApplicationCookie, like so:
// ===== Add Identity ========
services.AddIdentity<User, IdentityRole>(o =>
{
o.User.RequireUniqueEmail = true;
o.Tokens.EmailConfirmationTokenProvider = "EMAILCONF";
// I want to be able to resend an `Email` confirmation email
// o.SignIn.RequireConfirmedEmail = true;
}).AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ContestContext>()
.AddTokenProvider<EmailConfirmationTokenProvider<User>>("EMAILCONF")
.AddDefaultTokenProviders();
services.Configure<DataProtectionTokenProviderOptions>(o =>
o.TokenLifespan = TimeSpan.FromHours(3)
);
services.Configure<EmailConfirmationTokenProviderOptions>(o =>
o.TokenLifespan = TimeSpan.FromDays(2)
);
// ===== Configure Identity =======
service.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "auth_cookie";
options.Cookie.SameSite = SameSiteMode.None;
options.LoginPath = new PathString("/api/contests");
options.AccessDeniedPath = new PathString("/api/contests");
// Not creating a new object since ASP.NET Identity has created
// one already and hooked to the OnValidatePrincipal event.
// See https://github.com/aspnet/AspNetCore/blob/5a64688d8e192cacffda9440e8725c1ed41a30cf/src/Identity/src/Identity/IdentityServiceCollectionExtensions.cs#L56
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
});
It also means that you can safely remove the part where you add a cookie-based authentication scheme since this is taken care of by ASP.NET Identity itself.
Let me know how you go!

If you are not using ASP.NET Identity, you can follow the same pattern that kuldeep chopra mentioned in another answer, but instead inside the AddCookie method:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie((o) =>
{
o.Cookie.HttpOnly = true;
o.LoginPath = string.Empty;
o.AccessDeniedPath = string.Empty;
o.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
});
}
It is not enough to just set the paths to empty/null.

services.ConfigureApplicationCookie(options => {
options.AccessDeniedPath = "/Account/Login";
options.LoginPath = "/Account/Denied";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.Events.OnRedirectToLogin = context => {
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
});

I faced this issue and made a workaround , I just create a controller "Account" and write the Redirection inside it:
public class AccountController : Controller
{
public IActionResult Login()
{
return RedirectToAction("Login", "Home");
}
}

A bit late to this but maybe others will have the same thing as I.
So what code did I have?
services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddScheme<CerveraProxyAuthenticationOptions, CerveraProxyAuthenticationSchemeHandler>(ProxyAuthentication.AuthenticationScheme, options => { })
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Events.OnRedirectToLogin = (o) =>
{
o.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = (o) =>
{
o.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return Task.CompletedTask;
};
if (cookieSettings.LocalizationCookieExpiration.TryConvertToTimeSpan(out TimeSpan expiration))
{
options.ExpireTimeSpan = expiration;
options.Cookie.MaxAge = expiration;
}
options.Cookie.SecurePolicy = cookieSettings.SecureCookies
? CookieSecurePolicy.Always
: CookieSecurePolicy.SameAsRequest;
options.Cookie.Path = "/";
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
SameSiteMode sameSiteMode = Enum.Parse<SameSiteMode>(cookieSettings.SameSite);
options.Cookie.SameSite = sameSiteMode;
options.Cookie.Name = cookieSettings.AuthenticationCookieName;
options.Events = new CookieAuthenticationEvents
{
OnSigningIn = async context =>
{
context.Options.Cookie.Domain = context.HttpContext.Request.Host.Host;
}
};
});
The issue was that I had this code at the bottom of the scope!
options.Events = new CookieAuthenticationEvents
{
OnSigningIn = async context =>
{
context.Options.Cookie.Domain = context.HttpContext.Request.Host.Host;
}
};
Once I moved it to the top, it started work! Weird but works!

Related

.NET 6.0 JWT token is not recognized from localhost, but it IS recognized through SwaggerUI-- what's up?

Problem
I have a Target Framework: .NET 6.0 API backend with an exposed API with Identity Framework implemented in it.
I can successfully obtain data 0 issue on unauthorized API Endpoints with this above them on both SwaggerUI AND my localhost frontend. [AllowAnonymous]
When it comes to authorized API Endpoints however it is a totally different story.
SwaggerUI has NO PROBLEM AT ALL when I put my generated "JWT" into the "Authorize" padlock on the side of the screen.
Localhost JWT are simply not accepted or seen or understood or something I totally don't understand.
Request seems fine
Screenshot of the API request I am sending over has the authorization with the token, I also took the token to JWT io and it can parse out the name without problem.
I've also tried removing or adding "bearer" to the authorization header sent over and that makes no difference.
Screencap of request:
https://i.imgur.com/CY7F32o.png
My entire Startup.cs
(I know showing anything less would make it harder for you all to help me so >_< )
namespace API
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DbContext>(opt =>
{
opt.UseLazyLoadingProxies();
opt.UseMySql(Configuration.GetConnectionString("DefaultConnection"), ServerVersion.AutoDetect(Configuration.GetConnectionString("DefaultConnection")), opt => opt.EnableRetryOnFailure());
});
services.AddIdentity<UserModel, IdentityRole>(options =>
{
//options.Password.RequiredLength = 5;
//options.Password.RequireLowercase
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<DbContext>()
.AddDefaultTokenProviders();
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.Always;
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "auth_cookie";
options.Cookie.SameSite = SameSiteMode.None;
options.LoginPath = new PathString("/api/contests");
options.AccessDeniedPath = new PathString("/api/contests");
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
});
services.Configure<SmtpSettings>(Configuration.GetSection("SMTP"));
services.AddSingleton<IEmailManager, EmailManager>();
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddControllers(opt =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
opt.Filters.Add(new AuthorizeFilter(policy));
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "API", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please insert JWT with Bearer into field",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});
//Allow localhost to actually contact the server.
services.AddCors(opt =>
{
opt.AddPolicy("CorsPolicy", policy =>
{
policy.AllowAnyHeader()
.AllowAnyMethod()
.WithExposedHeaders("WWW-Authenticate")
.WithOrigins("http://localhost:3000", "http://localhost:5000", "https://localhost:5000")
.AllowCredentials();
});
});
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["TokenKey"]));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
//"Normal" API Auth.
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateAudience = false,
ValidateIssuer = false,
ValidateLifetime = true, //~5 minute leeway?
ClockSkew = TimeSpan.Zero //force time.
};
//SignalR auth.
opt.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chat"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
}).AddDiscord(options =>
{
options.CorrelationCookie.SameSite = SameSiteMode.Lax;
options.ClientId = Configuration["Discord:ClientId"];
options.ClientSecret = Configuration["Discord:ClientSecret"];
options.Scope.Add("email");
//options.CallbackPath = "/";
});
services.AddScoped<IJwtGenerator, JWTGenerator>();
services.AddScoped<IUserAccessor, UserAccessor>();
services.AddScoped<IImageAccessor, ImageAccessor>();
services.Configure<Infrastructure.Images.CloudinarySettings>(Configuration.GetSection("Cloudinary"));
services.AddRazorPages().AddRazorRuntimeCompilation();
services.AddServiceLayer();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseMiddleware<ErrorHandlingMiddleware>();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
c.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
});
app.UseCors(c => c.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToController("Index", "Fallback");
});
}
}
}

AuthenticateResult is returing false after successful SAML response

I am using SAML2 core middleware with ASP.Net for authentication. I am able to get a successful SAML response with statuscode of 200 that I could see in the browser but my code to authenticateResult is returning false. Once the authentication is success, I am trying to get the claims and pass the user information to the application. Could someone please help?
Here is my code to AccountController.cs :
public async Task<IActionResult> Callback(string returnUrl)
{
var authenticateResult1 = await HttpContext.AuthenticateAsync();
if (authenticateResult1.Succeeded)
{
var claimsIdentity = (ClaimsIdentity)authenticateResult1.Principal.Identity;
if (!claimsIdentity.HasClaim(c => c.Type == "ClaimTypes.Email"))
{
claimsIdentity.AddClaim(new Claim(" ClaimTypes.Email", "Test Test"));
await HttpContext.SignInAsync(authenticateResult1.Principal, authenticateResult1.Properties);
}
}
Here is what I have in startup.cs :
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddSamlCore(options =>
{
options.SignOutPath = "/signedout";
options.ServiceProvider.EntityId = "XXXXXXXX";
options.MetadataAddress = "https://xxxxx.xxxxxxx.com/simplesaml/saml2/idp/metadata.php";
options.ForceAuthn = true;
options.WantAssertionsSigned = true;
options.RequireMessageSigned = false;
options.UseTokenLifetime = false;
// Service Provider Properties (optional) - These set the appropriate tags in the metadata.xml file
options.ServiceProvider.ServiceName = "XXXXX";
options.ServiceProvider.Language = "en-US";
options.ServiceProvider.OrganizationDisplayName = "XXXXXXX";
options.ServiceProvider.OrganizationName = "xxxxxxxxx";
options.ServiceProvider.OrganizationURL = "https://xxxxxxx.com";
options.ServiceProvider.ContactPerson = new ContactType()
{
Company = "xxxxxxxxxxx",
GivenName = "Guy Fawks",
EmailAddress = new[] { "someemailhere#xxxxxxxx.com" },
// contactType = ContactTypeType.technical,
TelephoneNumber = new[] { "+1 234 5678" }
};
//Events - Modify events below if you want to log errors, add custom claims, etc.
options.Events.OnRemoteFailure = context =>
{
return Task.FromResult(0);
};
options.Events.OnTicketReceived = context =>
{
var identity = context.Principal.Identity as ClaimsIdentity;
var claims = context.Principal.Claims;
if (claims.Any(c => c.Type == ClaimTypes.Email))
{
var userId = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email).Value;
var name = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name);
identity.TryRemoveClaim(name);
identity.AddClaim(new Claim(ClaimTypes.Name, userId));
}
return Task.FromResult(0);
};
})
.AddCookie();
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
IdentityModelEventSource.ShowPII = true; //To show detail of error and see the problem
}
else
{
app.UseExceptionHandler("/Home/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.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication(); // AT modified on 4/17
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
```

Cannot access Authorized content in .NET Core using JWT authentication

I developed a sample app where I use JWT authentication to access the private content.
After getting a token in Postman, I am able to pass it to the private controller action also via Postman and it works all well.
But when I try to use SignInManager to access, it won't work. User.Identity.Name and User.Identity.IsAuthenticated are always nulls
Here's my startup code
public class Startup
{
private AppModule appModule;
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
appModule = new AppModule();
AppModule.Configuration = Configuration; //setting up of static variable
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
string connectionSring = null;
if (true) //(env.IsDevelopment())
connectionSring = appModule.GetConnectionString(EuConstants.LOCAL_CONNECTION_NAME);
else
connectionSring = appModule.GetConnectionString(EuConstants.DEFAULT_CONNECTION_NAME);
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(connectionSring);
});
/* tell the framework to use our cusom user and role classes */
services.AddIdentity<Gn_User, Gn_Role>(options =>
{
options.Stores.MaxLengthForKeys = 128;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(o => {
o.LoginPath = "/portal/login";
});
services.AddAuthorization();
services.AddAuthentication(option => {
option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
option.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
options.SaveToken = true;
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["Jwt:Site"],
ValidIssuer = Configuration["Jwt:Site"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SigningKey"]))
};
});
services.AddScoped<AppModule>(sp => appModule
);
services.AddMvc(
options =>
{
// make sure that all attributes by default required authentication
// options.Filters.Add(new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()));
}
).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
// tell the application to use the authentication
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Portal}/{action=Index}/{id?}");
});
}
}
and here's the authentication method to generate the token
public async Task<ActionResult> Login([FromBody] Gn_User model)
{
string password = "yaser#1234";
var user = await _userManager.FindByNameAsync(model.UserName);
if (user != null && await _userManager.CheckPasswordAsync(user, password))
{
var claim = new[] {
new Claim(JwtRegisteredClaimNames.Sub, user.UserName)
};
var signinKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(AppModule.Configuration["Jwt:SigningKey"]));
int expiryInMinutes = Convert.ToInt32(AppModule.Configuration["Jwt:ExpiryInMinutes"]);
var token = new JwtSecurityToken(
issuer: AppModule.Configuration["Jwt:Site"],
audience: AppModule.Configuration["Jwt:Site"],
expires: DateTime.UtcNow.AddMinutes(expiryInMinutes),
signingCredentials: new SigningCredentials(signinKey, SecurityAlgorithms.HmacSha256)
);
return Ok(
new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
You forget to add your claims to your JWT.
public async Task<ActionResult> Login([FromBody] Gn_User model)
{
string password = "yaser#1234";
var user = await _userManager.FindByNameAsync(model.UserName);
if (user != null && await _userManager.CheckPasswordAsync(user, password))
{
var claim = new[] {
new Claim(JwtRegisteredClaimNames.Sub, user.UserName)
};
var signinKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(AppModule.Configuration["Jwt:SigningKey"]));
int expiryInMinutes = Convert.ToInt32(AppModule.Configuration["Jwt:ExpiryInMinutes"]);
var token = new JwtSecurityToken(
issuer: AppModule.Configuration["Jwt:Site"],
audience: AppModule.Configuration["Jwt:Site"],
/////////////////////////////////
claims : claim,// Dont Forget To Add Claims To Your JWT
/////////////////////////////////
expires: DateTime.UtcNow.AddMinutes(expiryInMinutes),
signingCredentials: new SigningCredentials(signinKey, SecurityAlgorithms.HmacSha256)
);
return Ok(
new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
I worked for me by adding the following code to register the identity.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
instead of passing the parameters as options
services.AddAuthentication(op => {
op.defaultSchema...... = JwtBearerDefaults.AuthenticationScheme
})

ASP.NET Core 2.2 MVC problem redirecting after signing in

I have recently been experiencing an issue when attempting to login on the web app that I am working on, I did not modify the Startup.cs file and it used to work last time when I used it, but now when I am trying to log in it redirects me back to the Login page, although signing in succeeds
var result = await _signInManager.PasswordSignInAsync(user.UserName,
model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
// return LocalRedirect(returnUrl);
return RedirectToAction(nameof(HomeController.Index), "Home");
}
it hits the RetirectToAction, but I am sent back to Login Page, also I see two statuses in the network console, 200 and 302 which should be ok
Just a little update, after trying different things it looks like the signInManager does not sign me in at all
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<AORContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<AORContext>();
services.Configure<IdentityOptions>(options =>
{
// Default User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+";
options.User.RequireUniqueEmail = true;
});
services.Configure<IdentityOptions>(options =>
{
// Default Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 0;
});
services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = "/Account/AccessDenied";
//options.Cookie.Name = "YourAppCookieName";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
// ReturnUrlParameter requires
//using Microsoft.AspNetCore.Authentication.Cookies;
//options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
//options.SlidingExpiration = true;
});
services.AddMvc(config =>
{
// using Microsoft.AspNetCore.Mvc.Authorization;
// using Microsoft.AspNetCore.Authorization;
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/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.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
I had the same problem, and this feels more like a hack than anything but I got it work by redirecting the action back to itself with a flag. And if the flag is set, redirect to the actual target page:
// GET: LoginWithToken
[AllowAnonymous]
[HttpGet("LoginWithToken")]
public async Task<ActionResult> LoginWithToken(string token = null, bool tokenLoginSuccess = false)
{
// redirect
if (tokenLoginSuccess)
return RedirectToAction(nameof(Index));
// set cookie
await schoolLoginService.SignInWithToken(this.HttpContext, token);
return RedirectToAction(nameof(LoginWithToken), new { tokenLoginSuccess = true });
}

How to remove the redirect from an ASP.NET Core webapi and return HTTP 401?

Following the answer on this question, I have added authorization on everything by default, using the following code:
public void ConfigureServices(IServiceCollection aServices)
{
aServices.AddMvc(options =>
{
var lBuilder = new AuthorizationPolicyBuilder().RequireAuthenticatedUser();
var lFilter = new AuthorizeFilter(lBuilder.Build());
options.Filters.Add(lFilter);
});
aServices.AddMvc();
}
public void Configure(IApplicationBuilder aApp, IHostingEnvironment aEnv, ILoggerFactory aLoggerFactory)
{
aApp.UseCookieAuthentication(options =>
{
options.AuthenticationScheme = "Cookies";
options.AutomaticAuthentication = true;
});
}
However when someone tries to access something unauthorized, it returns a (what seems a default) redirect URL (http://foo.bar/Account/Login?ReturnUrl=%2Fapi%2Ffoobar%2F).
I want it to return a HTTP 401 only, instead of a redirect.
How can I do this in ASP.NET 5 for a WebAPI?
I had with this problem in an Angular2 + ASP.NET Core application. I managed to fix it in the following way:
services.AddIdentity<ApplicationUser, IdentityRole>(config => {
// ...
config.Cookies.ApplicationCookie.AutomaticChallenge = false;
// ...
});
If this is not working for you, you can try with the following method instead:
services.AddIdentity<ApplicationUser, IdentityRole>(config => {
// ...
config.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api"))
{
ctx.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
// added for .NET Core 1.0.1 and above (thanks to #Sean for the update)
ctx.Response.WriteAsync("{\"error\": " + ctx.Response.StatusCode + "}");
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
return Task.FromResult(0);
}
};
// ...
}
Update for Asp.Net Core 2.0
Cookie options are now configured in the following way:
services.ConfigureApplicationCookie(config =>
{
config.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = ctx => {
if (ctx.Request.Path.StartsWithSegments("/api"))
{
ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else {
ctx.Response.Redirect(ctx.RedirectUri);
}
return Task.FromResult(0);
}
};
});
By the url you get redirected to I assume you're using cookie authentication.
You should get the desired results by setting the LoginPath property of the CookieAuthenticationOptions to null or empty as described by one of the users.
app.UseCookieAuthentication(options =>
{
options.LoginPath = "";
});
It was probably working back then but it's not working anymore (because of this change).
I've submitted a bug on GitHub for this.
I'll update the answer once it gets fixed.
I had a similar problem.
I solved this adding by manually the services.
ConfigureServices method:
services.AddTransient<IUserStore<User>, UserStore<User, IdentityRole, ApplicationDbContext>>();
services.AddTransient<IPasswordHasher<User>, PasswordHasher<User>>();
services.AddTransient<IUserValidator<User>, UserValidator<User>>();
services.AddTransient<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.AddTransient<IPasswordValidator<User>, PasswordValidator<User>>();
services.AddTransient<IdentityErrorDescriber, IdentityErrorDescriber>();
services.AddTransient<ILogger<UserManager<User>>, Logger<UserManager<User>>>();
services.AddTransient<UserManager<User>>();
services.AddMvcCore()
.AddJsonFormatters()
.AddAuthorization();
services.AddCors(options=> {
options.AddPolicy("AllowAllHeaders", (builder) => {
builder.WithOrigins("*").AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("WWW-Authenticate"); ;
});
});
services.AddAuthentication(options=> {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
options.ApiSecret = "secret";
});
Configure method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("AllowAllHeaders");
app.UseAuthentication();
app.UseMvc();
}
I am using aspnet core 2.0, IdentityServer 4 and aspnet identity.
Setting LoginPath = "" or null no longer works on Version 1.1.0.0. So here's what I did:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
ExpireTimeSpan = TimeSpan.FromDays(150),
AuthenticationScheme = options.Cookies.ApplicationCookie.AuthenticationScheme,
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync,
OnRedirectToLogin = async (context) => context.Response.StatusCode = 401,
OnRedirectToAccessDenied = async (context) => context.Response.StatusCode = 403
},
AutomaticAuthenticate = true,
AutomaticChallenge = true,
});
Be aware, you should not use the CookieAuthentication only if you want to use your own Authentication Mechanism for example bypassing the Identity provider which not the case for most of us.
The default Identity provider use the CookieAuthenticationOptions behind the scene, you can configure it like the below.
services.AddIdentity<ApplicationUser, IdentityRole>(o =>
{
o.Password.RequireDigit = false;
o.Password.RequireUppercase = false;
o.Password.RequireLowercase = false;
o.Password.RequireNonAlphanumeric = false;
o.User.RequireUniqueEmail = true;
o.Cookies.ApplicationCookie.LoginPath = null; // <-----
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
Tested in version 1.0.0
in case it helps, below is my answer - with dotnet 1.0.1
its based on Darkseal's answer except I had to add the line ctx.Response.WriteAsync() to stop the redirect to the default 401 URL (Account/Login)
// Adds identity to the serviceCollection, so the applicationBuilder can UseIdentity
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
//note: this has no effect - 401 still redirects to /Account/Login!
//options.Cookies.ApplicationCookie.LoginPath = null;
options.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = ctx =>
{
//for WebApi: prevent aspnet core redirecting to 'Account/Login' on a 401:
if (ctx.Request.Path.StartsWithSegments("/api"))
{
ctx.RedirectUri = null;
ctx.Response.WriteAsync("{\"error\": " + ctx.Response.StatusCode + "}");
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
return Task.FromResult(0);
}
};
})
.AddDefaultTokenProviders();
}
Use this code in Startup :
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = $"/Account/Login";
options.LogoutPath = $"/Account/Logout";
options.AccessDeniedPath = $"/Account/AccessDenied";
options.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = (ctx) =>
{
if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
ctx.Response.StatusCode = 401;
return Task.CompletedTask;
},
OnRedirectToAccessDenied = (ctx) =>
{
if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
ctx.Response.StatusCode = 403;
return Task.CompletedTask;
}
};
});

Resources