ASP Core 2.0 JwtBearerAuthentication not working - authentication

I am trying to use JwtBearerAuthentication in Asp core 2.0 and I've encountered two main issues.
The Configure method of startup is like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
{
HotModuleReplacement = true
});
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseTestSensitiveConfiguration(null);
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
}
and the ConfigureServices like bellow:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver())
.AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
services.AddDbContext<FGWAContext>(options => options.UseSqlServer(connection));
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<FGWAContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = false;
options.Password.RequiredLength = 4;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
// User settings
options.User.RequireUniqueEmail = true;
});
//// If you want to tweak Identity cookies, they're no longer part of IdentityOptions.
//services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");
//services.AddAuthentication();
//// If you don't want the cookie to be automatically authenticated and assigned to HttpContext.User,
//// remove the CookieAuthenticationDefaults.AuthenticationScheme parameter passed to AddAuthentication.
//services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
// .AddCookie(options =>
// {
// options.LoginPath = "/Account/LogIn";
// options.LogoutPath = "/Account/LogOff";
// });
//services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
// .AddJwtBearer(jwtBearerOptions =>
// {
// //jwtBearerOptions.Events.OnChallenge = context =>
// //{
// // context.Response.Headers["Location"] = context.Request.Path.Value;
// // context.Response.StatusCode = 401;
// // return Task.CompletedTask;
// //};
// jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
// {
// ValidateIssuerSigningKey = true,
// IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your secret goes here")),
// ValidateIssuer = true,
// ValidIssuer = "The name of the issuer",
// ValidateAudience = true,
// ValidAudience = "The name of the audience",
// ValidateLifetime = true, //validate the expiration and not before values in the token
// ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
// };
// });
// Enable Dual Authentication
services.AddAuthentication()
.AddCookie(cfg => cfg.SlidingExpiration = true)
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
};
});
services.AddTransient<IModelsService, ModelsService>();
services.AddTransient<IRestaurantService, RestaurantService>();
}
and the two main issues:
1- It does not work! I call the method to generate token http://localhost:59699/api/accountapi/login the the answer is something like this:
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb29kdGVzdGdldHVzckBnbWFpbC5jb20iLCJqdGkiOiIyZGQ0MDhkNy02NDE4LTQ2MGItYTUxYi1hNTYzN2Q0YWYyYzgiLCJpYXQiOiIxMC8xMi8yMDE3IDM6NDA6MDYgQU0iLCJuYmYiOjE1MDc3Nzk2MDYsImV4cCI6MTUwNzc3OTkwNiwiaXNzIjoiRXhhbXBsZUlzc3VlciIsImF1ZCI6IkV4YW1wbGVBdWRpZW5jZSJ9.of-kTEIG8bOoPfyCQjuP7s6Zm32yFFPlW_T61OT8Hqs","expires_in":300}
then I call the protected resource this way:
but the protected resource is not accessible.
2- After failure to authenticate, it redirects the request to login page. How can I disable this auto challenge behavior?
before you start answering I have to tell that, I have tried https://wildermuth.com/2017/08/19/Two-AuthorizationSchemes-in-ASP-NET-Core-2 to craete the authentication and this https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x too; and also this ASP.NET Core 2.0 disable automatic challenge to disable auto challenge, but non of them worked.
Update
The desired config is to use jwtbearerauthentication on webapi calls and for the others use cookie. Then while using the former I want to return not authorized (401) response on a not authorized request while for the later one I want a redirection.

I see at least one error. Your Authorization header should be "Bearer TOKEN", not "bearer". So capitalize Bearer.
Then for the redirection of the api call. Put the Authorize attribute with the JwtBearer schema on the api actions:
[Route("api/method")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet]
public IActionResult MyApiCall()
{
.....
}

Related

Authentication in .NET Core 3.1: Default options provide too little control and don't work as expected

I am working with Identity and JWT auth in .NET Core 3.1. I will be adding Open Id in the future. The problem I am facing now is that I have too little control over the authentication pipeline. The pipeline makes some decisions for you, such as when to re-direct to the login. For example, for Identity authentication, when you emit a 401 status it will attempt to re-direct to /login - even if you specified a different page in the cookie configuration. Surprisingly, JWT auth has the same problem - even when the only policy supplied is the Bearer policy and the controller is tagged with the ApiController attribute.
[Authorize(AuthenticationSchemes = Schemes.Bearer)]
I want full control over the pipeline and the response. What is the best way to achieve this by utilizing as much of the built in tools as possible? I do not wish to write my own JWT validating code, rather use the validation as I configured it.
This is how I configure JWT. OnAuthenticationFailed. The callback to set the headers manually never runs.
//piranha
services.AddPiranha(options =>
{
options.UseFileStorage(naming: Piranha.Local.FileStorageNaming.UniqueFolderNames);
options.UseImageSharp();
options.UseManager();
options.UseTinyMCE();
options.UseMemoryCache();
options.UseEF<SQLiteDb>(db =>
db.UseSqlite("Filename=./Data/emurse-piranha.db"));
options.UseIdentityWithSeed<IdentitySQLiteDb>(db =>
db.UseSqlite("Filename=./Data/emurse-piranha.db"),
identityOptions =>
{
// Password settings
identityOptions.Password.RequireDigit = false;
identityOptions.Password.RequiredLength = 6;
identityOptions.Password.RequireNonAlphanumeric = false;
identityOptions.Password.RequireUppercase = false;
identityOptions.Password.RequireLowercase = false;
identityOptions.Password.RequiredUniqueChars = 1;
// Lockout settings
identityOptions.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
identityOptions.Lockout.MaxFailedAccessAttempts = 10;
identityOptions.Lockout.AllowedForNewUsers = true;
// User settings
identityOptions.User.RequireUniqueEmail = true;
},
cookieOptions =>
{
cookieOptions.Cookie.HttpOnly = true;
cookieOptions.ExpireTimeSpan = TimeSpan.FromMinutes(30);
cookieOptions.LoginPath = "/manager/login";
cookieOptions.AccessDeniedPath = "/manager/login";
cookieOptions.SlidingExpiration = true;
var defaultAction = cookieOptions.Events.OnRedirectToLogin;
cookieOptions.Events.OnRedirectToLogin = (context) =>
{
if (context.Request.Path.Value.StartsWith("/api"))
{
response.StatusCode = 401;
response.BodyWriter.WriteAsync(new ReadOnlyMemory<byte>
(Encoding.ASCII.GetBytes("unauthorized.")));
return Task.CompletedTask;
}
else
{
return defaultAction(context);
}
};
});
//turn off for prod
options.AddRazorRuntimeCompilation=true;
});
//JWT
services.AddAuthentication(Schemes.Bearer)
.AddApplicationJwt(Configuration);
public static AuthenticationBuilder AddApplicationJwt(this AuthenticationBuilder builder, IConfiguration config)
{
builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
var issuer = config["Jwt:Issuer"];
var key = config["Jwt:Key"];
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = issuer,
ValidAudience = issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))
};
//Never executes
options.Events= new JwtBearerEvents()
{
OnAuthenticationFailed = ((context) => {
response.StatusCode = 401;
response.BodyWriter.WriteAsync(new ReadOnlyMemory<byte>
(Encoding.ASCII.GetBytes("unauthorized.")));
return Task.CompletedTask;
}),
};
});
return;
}
The app is then configured using mostly default options:
app.UseAuthorization();
app.UsePiranha(options =>
{
options.UseTinyMCE();
//default Auth middleware
options.UseIdentity();
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed(origin => true) //TODO: remove for production
.AllowCredentials());
//use Piranha manager
options.UseManager();
});
Thanks to #Xerillio, I've figured out the root cause of my problem. After digging into the source code, I had a better understanding of what the extension methods are doing under the hood.
UseAuthentication adds the Authentication middleware. But UseIdentity also does this, and UseIdentity is obsolete. Infact, ALL UseIdenitity does is call UseAuthentication.
app.UseStaticFiles();
app.UsePiranha();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UsePiranhaIdentity();
//TODO: remove for production
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed(origin => true)
.AllowCredentials());
app.UsePiranhaManager();
app.UsePiranhaTinyMCE();

ASP.NET Identity Core - too many role related queries

My project uses role-based authorization and it has over 100 roles. I've noticed that before every action, server queries each user role and it's claims separately. That's over 200 queries before each action. Even an empty controller does this, so I assume this is ASP.NET Identity Core functionality. Is there any way to optimize this?
Thanks in advance.
ASP.NET Core web server output (one out of many role queries):
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[#__role_Id_0='390'], CommandType='Text', CommandTimeout='30']
SELECT [rc].[ClaimType], [rc].[ClaimValue]
FROM [AspNetRoleClaims] AS [rc]
WHERE [rc].[RoleId] = #__role_Id_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[#__normalizedName_0='100' (Size = 256)], CommandType='Text', CommandTimeout='30']
SELECT TOP(1) [r].[Id], [r].[ConcurrencyStamp], [r].[Name], [r].[NormalizedName]
FROM [AspNetRoles] AS [r]
WHERE [r].[NormalizedName] = #__normalizedName_0
My Startup.cs class:
public class Startup
{
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)
{
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.AddRouting(options => options.LowercaseUrls = true);
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromDays(1);
options.Cookie.IsEssential = true;
});
services.AddDbContext<AppDbContext>(options =>
options
.EnableSensitiveDataLogging()
.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), x =>
{
x.UseRowNumberForPaging();
x.UseNetTopologySuite();
}));
services.Configure<WebEncoderOptions>(options =>
{
options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All);
});
services.Configure<AppConfiguration>(
Configuration.GetSection("AppConfiguration"));
services.AddIdentity<User, UserRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 6;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
});
services.Configure<SecurityStampValidatorOptions>(options =>
{
// enables immediate logout, after updating the users stat.
options.ValidationInterval = TimeSpan.Zero;
});
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.Cookie.Expiration = TimeSpan.FromDays(150);
// If the LoginPath isn't set, ASP.NET Core defaults
// the path to /Account/Login.
options.LoginPath = "/Account/Login";
// If the AccessDeniedPath isn't set, ASP.NET Core defaults
// the path to /Account/AccessDenied.
options.AccessDeniedPath = "/Account/AccessDenied";
options.SlidingExpiration = true;
});
// Add application services.
services.AddScoped<IEmailSenderService, EmailSenderService>();
services.AddScoped<IUploaderService, UploaderService>();
services.AddScoped<IPdfService, PdfService>();
services.AddScoped<ICurrencyRateService, CurrencyRateService>();
services.AddScoped<IViewRenderService, ViewRenderService>();
services.AddScoped<IUserCultureInfoService, UserCultureInfoService>();
services.AddScoped<IUserService, UserService>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
services
.AddMvc(options =>
{
options.EnableEndpointRouting = false;
options
.RegisterDateTimeProvider(services)
.ModelMetadataDetailsProviders
.Add(new BindingSourceMetadataProvider(typeof(ListFilterViewModel), BindingSource.ModelBinding));
})
.AddSessionStateTempDataProvider()
.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();
app.UseDatabaseErrorPage();
// app.UseMiddleware<StackifyMiddleware.RequestTracerMiddleware>();
}
else
{
#if DEBUG
app.UseDeveloperExceptionPage();
#else
app.UseExceptionHandler("/Default/Error");
#endif
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapAreaRoute(
name: "Hubs",
areaName:"Hubs",
template: "Hubs/{controller=CompanyAddresses}/{action=Index}/{id?}");
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Default}/{action=Index}/{id?}"
);
routes.MapRoute(
name: "default",
template: "{controller=Default}/{action=Index}/{id?}");
});
}
}
I have found out what causes this weird behavior. It's this code segment in my Startup.cs class:
services.Configure<SecurityStampValidatorOptions>(options =>
{
// enables immediate logout, after updating the users stat.
options.ValidationInterval = TimeSpan.Zero;
});
Removing it solved my problem. I've been using this to force logout users by updating their security stamp as described here: How to sign out other user in ASP.NET Core Identity
It seems I will have to look for other solution for force logout, but I'm happy that requests are now not generating hundreds of SQL queries.

With JWT token and policy set, I get unauthorized 401

I follow the tutorial link below.
https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login
I am trying to understand how it works and I want to use role-based authentication using this token. so I made another policy in the Startup.cs file as below.
And I tried to use it like [Authorize(Policy = "admin")] the controller but every time I try I get unauthenticated using postman.
What am I missing? how to make roles-based authentication based on the tutorial?
Configure
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.WithOrigins("http://localhost:4200", "http://localhost:44318")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
// Configure JwtIssuerOptions
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
ValidateAudience = true,
ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(configureOptions =>
{
configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
configureOptions.TokenValidationParameters = tokenValidationParameters;
configureOptions.SaveToken = true;
});
// api user claim policy
services.AddAuthorization(options =>
{
options.AddPolicy("ApiUser", policy => policy.RequireClaim(Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess));
});
services.AddAuthorization(options =>
options.AddPolicy("admin", policy => policy.RequireRole("admin"))
);
var builder = services.AddIdentityCore<AppUser>(o =>
{
// configure identity options
o.Password.RequireDigit = false;
o.Password.RequireLowercase = false;
o.Password.RequireUppercase = false;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequiredLength = 6;
});
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
builder.AddEntityFrameworkStores<CDSPORTALContext>().AddDefaultTokenProviders().AddRoles<IdentityRole>();
//.AddRoles<IdentityRole>()
services.AddControllers();
services.AddAutoMapper(typeof(Startup));
services.AddSingleton<IJwtFactory, JwtFactory>();
services.TryAddTransient<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IRegionRepository, RegionRepository>();
services.AddScoped<IRegionService, RegionService>();
services.AddScoped<IEmailHelper, EmailHelper>();
}
// 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();
}
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.UseCors("CorsPolicy");
app.UseExceptionHandler(
builder =>
{
builder.Run(
async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
context.Response.AddApplicationError(error.Error.Message);
await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false);
}
});
});
app.UseRouting();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "Client";
spa.UseAngularCliServer(npmScript: "start");
});
}
}
Auth Controller
// POST api/auth/login
[HttpPost("login")]
public async Task<IActionResult> Post([FromBody]CredentialsViewModel credentials)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
if (identity == null)
{
//return null;
return BadRequest(Error.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
}
var id = identity.Claims.Single(c => c.Type == "id").Value;
var user = await _userManager.FindByIdAsync(id);
IList<string> role = await _userManager.GetRolesAsync(user);
var jwt = await Tokens.GenerateJwt(identity, role[0], _jwtFactory, credentials.UserName, _jwtOptions, new JsonSerializerSettings { Formatting = Formatting.Indented });
return new OkObjectResult(jwt);
}
I tried with all method and none of them working
[Authorize(Policy = "ApiUser")]
[HttpGet("getPolicy")]
public string GetPolicy()
{
return "policyWorking";
}
[Authorize(Roles = "admin")]
[HttpGet("getAdmin")]
public string GetAdmin()
{
return "adminWorking";
}
[Authorize ]
[HttpGet("getAuthorize")]
public string GetAuthorize()
{
return "normal authorize Working";
}
Decoded Token
"jti": "840d507d-b2d0-454b-bd1f-007890d3e669",
"iat": 1587699300,
"rol": "api_access",
"id": "1ac370e2-f5e9-4404-b017-8a3c087e2196",
"nbf": 1587699299,
"exp": 1587706499
I forgot to add this to appsetting.
"JwtIssuerOptions": {
"Issuer": "webApi",
"Audience": "http://localhost:4200/"
}

Server cannot authorize it's own tokens

Here is the setup, I have an auth server that issues tokens to an angular website. I have a controller inside the AuthServer that needs to use the [Authorize] system to only allow JWT tokens that are valid. When I check the User variable in my controller it is always null but when I check the HttpRequestHeaders on the controller I see the token being sent.
I also have an Api server that I implemented using the JWT tokens and [Authorize] system very easily.
Another layer, I am running both Api and Auth servers in docker containers.
My entire Startup.cs file from AuthServer:
var connectionString = Configuration.GetConnectionString("Default");
if (_env.IsDevelopment())
{
try
{
using (AppIdentityDbContext identityDb =
new AppIdentityDbContextFactory(connectionString).Create())
{
int Pendings = identityDb.Database.GetPendingMigrations().Count();
identityDb.Database.Migrate();
}
using (PersistedGrantDbContext persistGrantDb =
new PersistedGrantDbContextFactory(connectionString).Create())
{
int Pendings = persistGrantDb.Database.GetPendingMigrations().Count();
persistGrantDb.Database.Migrate();
}
}
catch (Exception)
{
}
}
services.AddControllersWithViews();
services.AddDbContextPool<AppIdentityDbContext>(options => options.UseSqlServer(connectionString));
services
.AddIdentity<AppUser, IdentityRole>(config=> {
config.User.RequireUniqueEmail = true;
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer().AddDeveloperSigningCredential()
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("Default"));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = (int)TimeSpan.FromDays(1).TotalSeconds; // interval in seconds
})
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<AppUser>()
.AddProfileService<AppUserProfileService>()
.AddJwtBearerClientAuthentication();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme,
jwtOptions =>
{
// jwt bearer options
jwtOptions.Authority = _env.IsDevelopment() ? "https://localhost:5001" : "";
jwtOptions.RequireHttpsMetadata = _env.IsDevelopment() ? false : true;
jwtOptions.Audience = "resourceapi";
jwtOptions.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateAudience = false,
ValidateIssuer = _env.IsDevelopment() ? false : true,
ValidateActor = false,
ValidateIssuerSigningKey = false
};
},
referenceOptions =>
{
// oauth2 introspection options
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()));
services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));
services.AddSingleton<IEmailSender, SmtpSender>();
Configure Section:
public void Configure(IApplicationBuilder app, IWebHostEnvironment 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();
var forwardOptions = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
RequireHeaderSymmetry = false
};
forwardOptions.KnownNetworks.Clear();
forwardOptions.KnownProxies.Clear();
// ref: https://github.com/aspnet/Docs/issues/2384
app.UseForwardedHeaders(forwardOptions);
}
app.UseCors("AllowAll");
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Check for user inside of AccountController: Controller
var u = User;
var _user = await _userManager.GetUserAsync(u);
var e = this._httpContextAccessor;

How to sign in ASP.NET Core Identity User, using OpenIdConnect Authentication?

I'm using ASP.NET Identity to authenticate my users and I want to be able to do this via Azure AD as well. All users will be in the DB beforehand, so all I need to do is sign them in and set their cookies, if the AzureAD login was successful. The problem is that when I implement the new external authentication and validate that they exist in my DB, they are not signed in.
So after successful remote login, if in my controller I check for User.Identity.IsAuthenticated it returns true, but _signInManager.IsSignedIn(User), it returns false. I have tried to follow the MS guidelines and documentation, but I assume there is something wrong with my config.
Here's the startup:
services.AddMvc(options => options.EnableEndpointRouting = false)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddRouting(options =>
{
options.LowercaseQueryStrings = true;
options.LowercaseUrls = true;
});
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("<my_db_connection_string_here>")));
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddUserManager<UserManager<ApplicationUser>>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
Configuration.GetSection("OpenIdConnect").Bind(options);
options.TokenValidationParameters.ValidateIssuer = false;
options.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async ctx =>
{
var request = ctx.HttpContext.Request;
var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);
var distributedCache = ctx.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
string userId = ctx.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var authContext = new AuthenticationContext(ctx.Options.Authority);
var result = await authContext.AcquireTokenByAuthorizationCodeAsync(
ctx.ProtocolMessage.Code, new Uri(currentUri), credential, ctx.Options.Resource);
ctx.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
});
var builder = services.AddIdentityCore<ApplicationUser>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddLogging(options =>
{
options.AddConfiguration(Configuration.GetSection("Logging"))
.AddConsole();
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
IdentityModelEventSource.ShowPII = true;
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
And in my controller:
[AllowAnonymous]
public IActionResult AzureLogin()
{
if (User.Identity.IsAuthenticated)
{
return RedirectToAction(nameof(HandleLogin)):
}
return Challenge(new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(HandleLogin))
});
}
[Authorize]
public async Task<IActionResult> HandleLogin()
{
var isAuth = User.Identity.IsAuthenticated; // true
var isSigned = _signInmanager.IsSignedIn(User); // false
return ....
}
You could try to set AutomaticAuthenticate cookie to true:
services.Configure<IdentityOptions>(options => {
// other configs
options.Cookies.ApplicationCookie.AutomaticAuthenticate = true;
});
Here's how I managed to do it:
Since I'm authorizing the user via ASP.NET Identity, I changed the default authentication method in the authentication options to options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme; and in the OpenIdConnectOptions OnAuthorizationCodeRecieved event, I validate and sign in the Identity User via the SignInManager.SignInAsync() method