I'm got a site setup using vnext/core 1.0 that uses Identity 3 for authentication. I can create users, I can change passwords, I can login fine. The issue is, it appears to be ignoring the ExpireTimespan property as I'm randomly kicked out of the app after a certain amount of time and I'm struggling to get to the bottom of it.
I have my own userstore and usermanager
public IServiceProvider ConfigureServices(IServiceCollection services)
{
...
services.AddIdentity<Domain.Models.User, Domain.Models.UserRole>()
.AddUserStore<UserStore>()
.AddRoleStore<RoleStore>()
.AddUserManager<MyUserManager>()
.AddDefaultTokenProviders();
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
app.UseMyIdentity();
...
}
public static IApplicationBuilder UseMyIdentity(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
var marker = app.ApplicationServices.GetService<IdentityMarkerService>();
if (marker == null)
{
throw new InvalidOperationException("MustCallAddIdentity");
}
var options = app.ApplicationServices.GetRequiredService<IOptions<IdentityOptions>>().Value;
app.UseCookieAuthentication(options.Cookies.ExternalCookie);
app.UseCookieAuthentication(options.Cookies.TwoFactorRememberMeCookie);
app.UseCookieAuthentication(options.Cookies.TwoFactorUserIdCookie);
CookieAuthenticationOptions appCookie = options.Cookies.ApplicationCookie;
appCookie.LoginPath = new Microsoft.AspNet.Http.PathString("/Login");
appCookie.SlidingExpiration = true;
appCookie.ExpireTimeSpan = TimeSpan.FromHours(8);
appCookie.CookieName = "MyWebApp";
app.UseCookieAuthentication(appCookie);
return app;
}
Login controller
var user = await userManager.FindByNameAsync(model.Username);
if (user != null)
{
SignInResult result = await signInManager.PasswordSignInAsync(user, model.Password, false, false);
if (result.Succeeded)
{
RedirectToActionPermanent("Index", "Home");
}
}
See my problem here:
ASP.NET Core 1.0 - MVC 6 - Cookie Expiration
I ran into the same problem and spent hours going through the OS-code of aspnet Identity on github :-)
Your custom UserManager has to implement Get/UpdateSecurityStampAsync
public class MyUserManager:UserManager<MinervaUser>
{
...
public override bool SupportsUserSecurityStamp
{
get
{
return true;
}
}
public override async Task<string> GetSecurityStampAsync(MinervaUser user)
{
// Todo: Implement something useful here!
return "Token";
}
public override async Task<IdentityResult> UpdateSecurityStampAsync(MinervaUser user)
{
// Todo: Implement something useful here!
return IdentityResult.Success;
}
Related
I am working on a project with micro-services's architecture. I have one API Rest that works as an API Gateway where I want to get the username from a JSON Web Token. It may be important to note that the authentication and authorization of the system are being dealt with on another part of the system (I just need to get the username).
So far I was able to get the username from the JWT using this extension of HttpContext:
public static class HttpContextExtensions
{
private static JwtSecurityToken GetJwt(this HttpContext httpContext)
{
bool existeToken = httpContext.Request.Headers.TryGetValue("Authorization", out StringValues authorizationValue);
if (!existeToken)
{
return null;
}
string encodedToken = authorizationValue.FirstOrDefault().Split(" ", 2)[1];
return new JwtSecurityTokenHandler().ReadJwtToken(encodedToken);
}
public static string GetUsername(this HttpContext httpContext)
{
JwtSecurityToken jwt = httpContext.GetJwt();
Claim usernameClaim = jwt.Claims.FirstOrDefault(c => c.Type == "preferred_username");
return usernameClaim?.Value;
}
}
And then I can get the username on the controller:
string username = HttpContext.GetUsername();
I was wondering if there is a more elegant way to do this on .Net Core 3.1. I tried to configure my project so I can get all the Claims loaded into User.Identity in my Controller's Action, but I failed miserably. All the documentation I found was about previous versions of .Net Core.
I think I may be doing something wrong on the Startup.cs. If anyone can point me to the right direction I would really appreciate it.
I did an approach by filling the User on HttpContext in a middleware:
public class AuthMiddleware
{
private readonly RequestDelegate _next;
public AuthMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context)
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null)
{
var jwtEncodedString = authHeader.Substring(7);
var token = new JwtSecurityToken(jwtEncodedString: jwtEncodedString);
var identity = new ClaimsIdentity(token.Claims, "basic");
context.User = new ClaimsPrincipal(identity);
}
return _next(context);
}
}
Then on app setup;
public static IApplicationBuilder UseApiConfiguration(this IApplicationBuilder app, IWebHostEnvironment env)
{
// add middleware reference
app.UseMiddleware<AuthMiddleware>();
return app;
}
Can we redirect the user trying to browse hangfire url to some unauthorized page.
I am using ASP.net mvc 5.
I have the following page in my startup.cs file.
public void Configuration(IAppBuilder app)
{
String conn = System.Configuration.ConfigurationManager.
ConnectionStrings["conn"].ConnectionString;
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
GlobalConfiguration.Configuration
.UseSqlServerStorage(conn,
new SqlServerStorageOptions { QueuePollInterval = TimeSpan.FromSeconds(1) });
//BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-forget!"));
//app.UseHangfireDashboard();
app.UseHangfireDashboard("/Admin/hangfire", new DashboardOptions
{
Authorization =new[] { new DashboardAuthorizationFilter() }
});
//app.MapHangfireDashboard("/hangfire", new[] { new AuthorizationFilter() });
app.UseHangfireServer();
//start hangfire recurring jobs
HangFireServices service = new HangFireServices();
//service.StartArchive();
service.StartDelete();
}
The HangFireServices has the jobs:
public void StartDelete()
{
List<KeyValuePair<string, int>> c = _service.GetServiceRetention();
foreach (var obj in c)
{
RecurringJob.AddOrUpdate(DELETE_SERVICE + obj.Key, () =>
Delete(obj.Key), //this is my function that does the actual process
Cron.DayInterval(Convert.ToInt32(obj.Value)));
}
}
The authorization code is :
public class DashboardAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
//TODO:Implement
return false;
}
}
The default page is the Home page on which a different authorization class is set up. The user fails the authorization rules as per db and is redirected to UnAuthorizedController index page. If the user manually changes the url to point to /hangfire,as the authorization returned is false, it sees a blank page, but I want to redirect to UnAuthorizedController index page.
If you want to redirect to a particular page on controller, then may be this would help.
I created a account controller with login method as follows:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl = "jobs")
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await UserManager.FindByNameAsync(model.UserName);
await SignInManager.SignInAsync(user, false, false);
var virtualDirectory = Request.ApplicationPath.Equals("/") ? "/" : Request.ApplicationPath + "/";
return Redirect(virtualDirectory + returnUrl);
}
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
In my Startup.Auth.cs, I did following changes:
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>
(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>
(ApplicationSignInManager.Create);
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieName = "TestService",
LoginPath = new PathString("/Account/Login"),
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(20000),
Provider = new CookieAuthenticationProvider()
});
}
And finally, in the startup.cs class:
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
LogProvider.SetCurrentLogProvider(new HangfireLogProvider());
GlobalConfiguration.Configuration.UseSqlServerStorage("HangfirePersistence");
app.UseHangfireDashboard("/jobs", new DashboardOptions
{
Authorization = new[] { new HangfireAuthFilter() }
});
app.UseHangfireServer();
ConfigureAuth(app);
}
}
public class HangfireAuthFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var user = HttpContext.Current.User;
return user != null && user.IsInRole("Admin") && user.Identity.IsAuthenticated;
}
}
When ever you are not authenticated, you will be redirected to the login action on account controller.
If the error page represents static content - it's simple:
public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize([NotNull] DashboardContext context)
{
var path = context.GetHttpContext().Request.Path;
var isResource = path != null && (path.Value.StartsWith("/css") || path.Value.StartsWith("/js") || path.Value.StartsWith("/font"));
if (!context.GetHttpContext().User.Identity.IsAuthenticated && !isResource)
{
context.Response.ContentType = "text/html";
context.Response.WriteAsync(new AccessDeniedPage().ToString(context.GetHttpContext()));
return false;
}
return true;
}
}
Please make a note this AccessDeniedPage is not inherited from Hangfire.Dashboard.RazorPage. The reason is that you won't be able to assign current context to such page (Hangfire has made some its functionality internal). But if you need to inherit from Microsoft.AspNetCore.Mvc.Razor.RazorPage for instance, it will looks something like:
context.Response.WriteAsync(new AccessDeniedPage().ToString(context.GetHttpContext()));
where ToString is:
public abstract class PageBase : RazorPage
{
public UrlHelper Url { get; set; }
public override async Task ExecuteAsync()
{
await Task.CompletedTask;
}
public abstract void Execute();
public string ToString(HttpContext httpContext)
{
using var output = new StringWriter();
ViewContext = new Microsoft.AspNetCore.Mvc.Rendering.ViewContext()
{
HttpContext = httpContext,
Writer = output
};
HtmlEncoder = HtmlEncoder.Default;
Url = new UrlHelper(new ActionContext
{
HttpContext = httpContext,
ActionDescriptor = new ActionDescriptor(),
RouteData = new RouteData()
});
Execute();
return output.ToString();
}
I am using the ASP.NET Core default website template and have the authentication selected as "Individual User Accounts". How can I create roles and assign it to users so that I can use the roles in a controller to filter access?
My comment was deleted because I provided a link to a similar question I answered here. Ergo, I'll answer it more descriptively this time. Here goes.
You could do this easily by creating a CreateRoles method in your startup class. This helps check if the roles are created, and creates the roles if they aren't; on application startup. Like so.
private async Task CreateRoles(IServiceProvider serviceProvider)
{
//initializing custom roles
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var UserManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
string[] roleNames = { "Admin", "Manager", "Member" };
IdentityResult roleResult;
foreach (var roleName in roleNames)
{
var roleExist = await RoleManager.RoleExistsAsync(roleName);
if (!roleExist)
{
//create the roles and seed them to the database: Question 1
roleResult = await RoleManager.CreateAsync(new IdentityRole(roleName));
}
}
//Here you could create a super user who will maintain the web app
var poweruser = new ApplicationUser
{
UserName = Configuration["AppSettings:UserName"],
Email = Configuration["AppSettings:UserEmail"],
};
//Ensure you have these values in your appsettings.json file
string userPWD = Configuration["AppSettings:UserPassword"];
var _user = await UserManager.FindByEmailAsync(Configuration["AppSettings:AdminUserEmail"]);
if(_user == null)
{
var createPowerUser = await UserManager.CreateAsync(poweruser, userPWD);
if (createPowerUser.Succeeded)
{
//here we tie the new user to the role
await UserManager.AddToRoleAsync(poweruser, "Admin");
}
}
}
and then you could call the CreateRoles(serviceProvider).Wait(); method from the Configure method in the Startup class.
ensure you have IServiceProvider as a parameter in the Configure class.
Using role-based authorization in a controller to filter user access: Question 2
You can do this easily, like so.
[Authorize(Roles="Manager")]
public class ManageController : Controller
{
//....
}
You can also use role-based authorization in the action method like so. Assign multiple roles, if you will
[Authorize(Roles="Admin, Manager")]
public IActionResult Index()
{
/*
.....
*/
}
While this works fine, for a much better practice, you might want to read about using policy based role checks. You can find it on the ASP.NET core documentation here, or this article I wrote about it here
I have created an action in the Accounts controller that calls a function to create the roles and assign the Admin role to the default user. (You should probably remove the default user in production):
private async Task CreateRolesandUsers()
{
bool x = await _roleManager.RoleExistsAsync("Admin");
if (!x)
{
// first we create Admin rool
var role = new IdentityRole();
role.Name = "Admin";
await _roleManager.CreateAsync(role);
//Here we create a Admin super user who will maintain the website
var user = new ApplicationUser();
user.UserName = "default";
user.Email = "default#default.com";
string userPWD = "somepassword";
IdentityResult chkUser = await _userManager.CreateAsync(user, userPWD);
//Add default User to Role Admin
if (chkUser.Succeeded)
{
var result1 = await _userManager.AddToRoleAsync(user, "Admin");
}
}
// creating Creating Manager role
x = await _roleManager.RoleExistsAsync("Manager");
if (!x)
{
var role = new IdentityRole();
role.Name = "Manager";
await _roleManager.CreateAsync(role);
}
// creating Creating Employee role
x = await _roleManager.RoleExistsAsync("Employee");
if (!x)
{
var role = new IdentityRole();
role.Name = "Employee";
await _roleManager.CreateAsync(role);
}
}
After you could create a controller to manage roles for the users.
Temi's answer is nearly correct, but you cannot call an asynchronous function from a non asynchronous function like he is suggesting. What you need to do is make asynchronous calls in a synchronous function like so :
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentity();
// Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
CreateRoles(serviceProvider);
}
private void CreateRoles(IServiceProvider serviceProvider)
{
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
Task<IdentityResult> roleResult;
string email = "someone#somewhere.com";
//Check that there is an Administrator role and create if not
Task<bool> hasAdminRole = roleManager.RoleExistsAsync("Administrator");
hasAdminRole.Wait();
if (!hasAdminRole.Result)
{
roleResult = roleManager.CreateAsync(new IdentityRole("Administrator"));
roleResult.Wait();
}
//Check if the admin user exists and create it if not
//Add to the Administrator role
Task<ApplicationUser> testUser = userManager.FindByEmailAsync(email);
testUser.Wait();
if (testUser.Result == null)
{
ApplicationUser administrator = new ApplicationUser();
administrator.Email = email;
administrator.UserName = email;
Task<IdentityResult> newUser = userManager.CreateAsync(administrator, "_AStrongP#ssword!");
newUser.Wait();
if (newUser.Result.Succeeded)
{
Task<IdentityResult> newUserRole = userManager.AddToRoleAsync(administrator, "Administrator");
newUserRole.Wait();
}
}
}
The key to this is the use of the Task<> class and forcing the system to wait in a slightly different way in a synchronous way.
I use this (DI):
public class IdentitySeed
{
private readonly ApplicationDbContext _context;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<ApplicationRole> _rolesManager;
private readonly ILogger _logger;
public IdentitySeed(
ApplicationDbContext context,
UserManager<ApplicationUser> userManager,
RoleManager<ApplicationRole> roleManager,
ILoggerFactory loggerFactory) {
_context = context;
_userManager = userManager;
_rolesManager = roleManager;
_logger = loggerFactory.CreateLogger<IdentitySeed>();
}
public async Task CreateRoles() {
if (await _context.Roles.AnyAsync()) {// not waste time
_logger.LogInformation("Exists Roles.");
return;
}
var adminRole = "Admin";
var roleNames = new String[] { adminRole, "Manager", "Crew", "Guest", "Designer" };
foreach (var roleName in roleNames) {
var role = await _rolesManager.RoleExistsAsync(roleName);
if (!role) {
var result = await _rolesManager.CreateAsync(new ApplicationRole { Name = roleName });
//
_logger.LogInformation("Create {0}: {1}", roleName, result.Succeeded);
}
}
// administrator
var user = new ApplicationUser {
UserName = "Administrator",
Email = "something#something.com",
EmailConfirmed = true
};
var i = await _userManager.FindByEmailAsync(user.Email);
if (i == null) {
var adminUser = await _userManager.CreateAsync(user, "Something*");
if (adminUser.Succeeded) {
await _userManager.AddToRoleAsync(user, adminRole);
//
_logger.LogInformation("Create {0}", user.UserName);
}
}
}
//! By: Luis Harvey Triana Vega
}
The following code will work ISA.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
IServiceProvider serviceProvider)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentity();
// Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
CreateRolesAndAdminUser(serviceProvider);
}
private static void CreateRolesAndAdminUser(IServiceProvider serviceProvider)
{
const string adminRoleName = "Administrator";
string[] roleNames = { adminRoleName, "Manager", "Member" };
foreach (string roleName in roleNames)
{
CreateRole(serviceProvider, roleName);
}
// Get these value from "appsettings.json" file.
string adminUserEmail = "someone22#somewhere.com";
string adminPwd = "_AStrongP1#ssword!";
AddUserToRole(serviceProvider, adminUserEmail, adminPwd, adminRoleName);
}
/// <summary>
/// Create a role if not exists.
/// </summary>
/// <param name="serviceProvider">Service Provider</param>
/// <param name="roleName">Role Name</param>
private static void CreateRole(IServiceProvider serviceProvider, string roleName)
{
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
Task<bool> roleExists = roleManager.RoleExistsAsync(roleName);
roleExists.Wait();
if (!roleExists.Result)
{
Task<IdentityResult> roleResult = roleManager.CreateAsync(new IdentityRole(roleName));
roleResult.Wait();
}
}
/// <summary>
/// Add user to a role if the user exists, otherwise, create the user and adds him to the role.
/// </summary>
/// <param name="serviceProvider">Service Provider</param>
/// <param name="userEmail">User Email</param>
/// <param name="userPwd">User Password. Used to create the user if not exists.</param>
/// <param name="roleName">Role Name</param>
private static void AddUserToRole(IServiceProvider serviceProvider, string userEmail,
string userPwd, string roleName)
{
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
Task<ApplicationUser> checkAppUser = userManager.FindByEmailAsync(userEmail);
checkAppUser.Wait();
ApplicationUser appUser = checkAppUser.Result;
if (checkAppUser.Result == null)
{
ApplicationUser newAppUser = new ApplicationUser
{
Email = userEmail,
UserName = userEmail
};
Task<IdentityResult> taskCreateAppUser = userManager.CreateAsync(newAppUser, userPwd);
taskCreateAppUser.Wait();
if (taskCreateAppUser.Result.Succeeded)
{
appUser = newAppUser;
}
}
Task<IdentityResult> newUserRole = userManager.AddToRoleAsync(appUser, roleName);
newUserRole.Wait();
}
In Configure method declare your role manager (Startup)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RoleManager<IdentityRole> roleManager)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
Task.Run(()=>this.CreateRoles(roleManager)).Wait();
}
private async Task CreateRoles(RoleManager<IdentityRole> roleManager)
{
foreach (string rol in this.Configuration.GetSection("Roles").Get<List<string>>())
{
if (!await roleManager.RoleExistsAsync(rol))
{
await roleManager.CreateAsync(new IdentityRole(rol));
}
}
}
OPTIONAL - In appsettings.JSON (it depends on you where you wanna get roles from)
{
"Roles": [
"SuperAdmin",
"Admin",
"Employee",
"Customer"
]
}
In addition to Temi Lajumoke's answer, it's worth noting that after creating the required roles and assigning them to specific users in ASP.NET Core 2.1 MVC Web Application, after launching the application, you may encounter a method error, such as registering or managing an account:
InvalidOperationException: Unable to resolve service for type
'Microsoft.AspNetCore.Identity.UI.Services.IEmailSender' while
attempting to activate
'WebApplication.Areas.Identity.Pages.Account.Manage.IndexModel'.
A similar error can be quickly corrected in the ConfigureServices method by adding the AddDefaultUI() method:
services.AddIdentity<IdentityUser, IdentityRole>()
//services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
Check
https://blogs.msdn.microsoft.com/webdev/2018/03/02/aspnetcore-2-1-identity-ui/
and related topic on github:
https://github.com/aspnet/Docs/issues/6784 for more information.
And for assigning role to specific user could be used IdentityUser class instead of ApplicationUser.
.net 6 option:
public static class WebApplicationExtensions
{
public static async Task<WebApplication> CreateRolesAsync(this WebApplication app, IConfiguration configuration)
{
using var scope = app.Services.CreateScope();
var roleManager = (RoleManager<IdentityRole>)scope.ServiceProvider.GetService(typeof(RoleManager<IdentityRole>));
var roles = configuration.GetSection("Roles").Get<List<string>>();
foreach (var role in roles)
{
if (!await roleManager.RoleExistsAsync(role))
await roleManager.CreateAsync(new IdentityRole(role));
}
return app;
}
}
In Program.cs add before app.Run()
await app.CreateRolesAsync(builder.Configuration);
Update in 2020. Here is another way if you prefer.
IdentityResult res = new IdentityResult();
var _role = new IdentityRole();
_role.Name = role.RoleName;
res = await _roleManager.CreateAsync(_role);
if (!res.Succeeded)
{
foreach (IdentityError er in res.Errors)
{
ModelState.AddModelError(string.Empty, er.Description);
}
ViewBag.UserMessage = "Error Adding Role";
return View();
}
else
{
ViewBag.UserMessage = "Role Added";
return View();
}
I'm struggling with how to set up authentication in my web service.
The service is build with the ASP.NET Core web api.
All my clients (WPF applications) should use the same credentials to call the web service operations.
After some research, I came up with basic authentication - sending a username and password in the header of the HTTP request.
But after hours of research, it seems to me that basic authentication is not the way to go in ASP.NET Core.
Most of the resources I found are implementing authentication using OAuth or some other middleware. But that seems to be oversized for my scenario, as well as using the Identity part of ASP.NET Core.
So what is the right way to achieve my goal - simple authentication with username and password in a ASP.NET Core web service?
Now, after I was pointed in the right direction, here's my complete solution:
This is the middleware class which is executed on every incoming request and checks if the request has the correct credentials. If no credentials are present or if they are wrong, the service responds with a 401 Unauthorized error immediately.
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
public AuthenticationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic"))
{
//Extract credentials
string encodedUsernamePassword = authHeader.Substring("Basic ".Length).Trim();
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));
int seperatorIndex = usernamePassword.IndexOf(':');
var username = usernamePassword.Substring(0, seperatorIndex);
var password = usernamePassword.Substring(seperatorIndex + 1);
if(username == "test" && password == "test" )
{
await _next.Invoke(context);
}
else
{
context.Response.StatusCode = 401; //Unauthorized
return;
}
}
else
{
// no authorization header
context.Response.StatusCode = 401; //Unauthorized
return;
}
}
}
The middleware extension needs to be called in the Configure method of the service Startup class
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMiddleware<AuthenticationMiddleware>();
app.UseMvc();
}
And that's all! :)
A very good resource for middleware in .Net Core and authentication can be found here:
https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/
You can implement a middleware which handles Basic authentication.
public async Task Invoke(HttpContext context)
{
var authHeader = context.Request.Headers.Get("Authorization");
if (authHeader != null && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
{
var token = authHeader.Substring("Basic ".Length).Trim();
System.Console.WriteLine(token);
var credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token));
var credentials = credentialstring.Split(':');
if(credentials[0] == "admin" && credentials[1] == "admin")
{
var claims = new[] { new Claim("name", credentials[0]), new Claim(ClaimTypes.Role, "Admin") };
var identity = new ClaimsIdentity(claims, "Basic");
context.User = new ClaimsPrincipal(identity);
}
}
else
{
context.Response.StatusCode = 401;
context.Response.Headers.Set("WWW-Authenticate", "Basic realm=\"dotnetthoughts.net\"");
}
await _next(context);
}
This code is written in a beta version of asp.net core. Hope it helps.
To use this only for specific controllers for example use this:
app.UseWhen(x => (x.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)),
builder =>
{
builder.UseMiddleware<AuthenticationMiddleware>();
});
I think you can go with JWT (Json Web Tokens).
First you need to install the package System.IdentityModel.Tokens.Jwt:
$ dotnet add package System.IdentityModel.Tokens.Jwt
You will need to add a controller for token generation and authentication like this one:
public class TokenController : Controller
{
[Route("/token")]
[HttpPost]
public IActionResult Create(string username, string password)
{
if (IsValidUserAndPasswordCombination(username, password))
return new ObjectResult(GenerateToken(username));
return BadRequest();
}
private bool IsValidUserAndPasswordCombination(string username, string password)
{
return !string.IsNullOrEmpty(username) && username == password;
}
private string GenerateToken(string username)
{
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, username),
new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(1)).ToUnixTimeSeconds().ToString()),
};
var token = new JwtSecurityToken(
new JwtHeader(new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Secret Key You Devise")),
SecurityAlgorithms.HmacSha256)),
new JwtPayload(claims));
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
After that update Startup.cs class to look like below:
namespace WebAPISecurity
{
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.AddMvc();
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = "JwtBearer";
options.DefaultChallengeScheme = "JwtBearer";
})
.AddJwtBearer("JwtBearer", jwtBearerOptions =>
{
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Secret Key You Devise")),
ValidateIssuer = false,
//ValidIssuer = "The name of the issuer",
ValidateAudience = false,
//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
};
});
}
// 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.UseAuthentication();
app.UseMvc();
}
}
And that's it, what is left now is to put [Authorize] attribute on the Controllers or Actions you want.
Here is a link of a complete straight forward tutorial.
http://www.blinkingcaret.com/2017/09/06/secure-web-api-in-asp-net-core/
I have implemented BasicAuthenticationHandler for basic authentication so you can use it with standart attributes Authorize and AllowAnonymous.
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authHeader = (string)this.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
{
//Extract credentials
string encodedUsernamePassword = authHeader.Substring("Basic ".Length).Trim();
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));
int seperatorIndex = usernamePassword.IndexOf(':', StringComparison.OrdinalIgnoreCase);
var username = usernamePassword.Substring(0, seperatorIndex);
var password = usernamePassword.Substring(seperatorIndex + 1);
//you also can use this.Context.Authentication here
if (username == "test" && password == "test")
{
var user = new GenericPrincipal(new GenericIdentity("User"), null);
var ticket = new AuthenticationTicket(user, new AuthenticationProperties(), Options.AuthenticationScheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
else
{
return Task.FromResult(AuthenticateResult.Fail("No valid user."));
}
}
this.Response.Headers["WWW-Authenticate"]= "Basic realm=\"yourawesomesite.net\"";
return Task.FromResult(AuthenticateResult.Fail("No credentials."));
}
}
public class BasicAuthenticationMiddleware : AuthenticationMiddleware<BasicAuthenticationOptions>
{
public BasicAuthenticationMiddleware(
RequestDelegate next,
IOptions<BasicAuthenticationOptions> options,
ILoggerFactory loggerFactory,
UrlEncoder encoder)
: base(next, options, loggerFactory, encoder)
{
}
protected override AuthenticationHandler<BasicAuthenticationOptions> CreateHandler()
{
return new BasicAuthenticationHandler();
}
}
public class BasicAuthenticationOptions : AuthenticationOptions
{
public BasicAuthenticationOptions()
{
AuthenticationScheme = "Basic";
AutomaticAuthenticate = true;
}
}
Registration at Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>();. With this code, you can restrict any controller with standart attribute Autorize:
[Authorize(ActiveAuthenticationSchemes = "Basic")]
[Route("api/[controller]")]
public class ValuesController : Controller
and use attribute AllowAnonymous if you apply authorize filter on application level.
As rightly said by previous posts, one of way is to implement a custom basic authentication middleware. I found the best working code with explanation in this blog:
Basic Auth with custom middleware
I referred the same blog but had to do 2 adaptations:
While adding the middleware in startup file -> Configure function, always add custom middleware before adding app.UseMvc().
While reading the username, password from appsettings.json file, add static read only property in Startup file. Then read from appsettings.json. Finally, read the values from anywhere in the project. Example:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public static string UserNameFromAppSettings { get; private set; }
public static string PasswordFromAppSettings { get; private set; }
//set username and password from appsettings.json
UserNameFromAppSettings = Configuration.GetSection("BasicAuth").GetSection("UserName").Value;
PasswordFromAppSettings = Configuration.GetSection("BasicAuth").GetSection("Password").Value;
}
You can use an ActionFilterAttribute
public class BasicAuthAttribute : ActionFilterAttribute
{
public string BasicRealm { get; set; }
protected NetworkCredential Nc { get; set; }
public BasicAuthAttribute(string user,string pass)
{
this.Nc = new NetworkCredential(user,pass);
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var req = filterContext.HttpContext.Request;
var auth = req.Headers["Authorization"].ToString();
if (!String.IsNullOrEmpty(auth))
{
var cred = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(auth.Substring(6)))
.Split(':');
var user = new {Name = cred[0], Pass = cred[1]};
if (user.Name == Nc.UserName && user.Pass == Nc.Password) return;
}
filterContext.HttpContext.Response.Headers.Add("WWW-Authenticate",
String.Format("Basic realm=\"{0}\"", BasicRealm ?? "Ryadel"));
filterContext.Result = new UnauthorizedResult();
}
}
and add the attribute to your controller
[BasicAuth("USR", "MyPassword")]
In this public Github repo
https://github.com/boskjoett/BasicAuthWebApi
you can see a simple example of a ASP.NET Core 2.2 web API with endpoints protected by Basic Authentication.
ASP.NET Core 2.0 with Angular
https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login
Make sure to use type of authentication filter
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
UPDATE: Unfortunately, a Windows reboot solved this issue -.-
In our ASP.NET Core (1.0 RC2) application, we have the following requirement: only users from the internal network should be able to access some "Debug" pages (hosted by MVC Core). It's a public website and we don't have user logins, instead we managed it until now with a custom IP-address based authorization (note: this is not a security risk in our case, because we have a proxy in between, so the IP address cannot be spoofed from outside).
We want to implement such an IP-address based authorization in ASP.NET Core, as well. We use a custom policy "DebugPages" for this and corresponding [Authorize(Policy="DebugPages")] definitions on the MVC controller. Then we noticed, that we must have an authenticated user to get the AuthorizeAttribute to jump in and we create one in the request pipeline, which yields to the following code in Startup.cs (shortened for brevity):
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthorization(options =>
{
options.AddPolicy(
"DebugPages",
policy => policy.RequireAssertion(
async context => await MyIPAuthorization.IsAuthorizedAsync()));
});
}
public void Configure(IApplicationBuilder app)
{
...
app.Use(async (context, next) =>
{
context.User = new ClaimsPrincipal(new GenericIdentity("anonymous"));
await next.Invoke();
});
...
}
Now this works fine when run in Debug by Visual Studio 2015 (with IIS Express).
But unfortunately it doesn't work when run directly by dotnet run (with Kestrel) from the command line. In this case we get the following exception:
InvalidOperationException: No authentication handler is configured to handle the scheme: Automatic
The same error occurs when we provide the current Windows principal instead of the principal with a custom anonymous identity -- so everytime when the user is automatic-ally authenticated...
So, why is there a difference between hosting in IIS Express and Kestrel? Any suggestions how to solve the issue?
So, after some research, as I mentioned in the comments, I have found that httpContext.Authentication.HttpAuthhenticationFeature.Handler is null, when you starts the application under the "selfhosted" kestrel. But when you use IIS the Handler has instantiated by Microsoft.AspNetCore.Server.IISIntegration.AuthenticationHandler. This specific handler implementation is part of the .UseIISIntegration() in Program.cs.
So, I've decided to use a part of this implementation in my App and handle nonauthenticated requests.
For my WebAPI (without any Views) service I use IdentityServer4.AccessTokenValidation that uses behind the scenes OAuth2IntrospectionAuthentication and JwtBearerAuthentication.
Create files
KestrelAuthenticationMiddleware.cs
public class KestrelAuthenticationMiddleware
{
private readonly RequestDelegate _next;
public KestrelAuthenticationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
var existingPrincipal = context.Features.Get<IHttpAuthenticationFeature>()?.User;
var handler = new KestrelAuthHandler(context, existingPrincipal);
AttachAuthenticationHandler(handler);
try
{
await _next(context);
}
finally
{
DetachAuthenticationhandler(handler);
}
}
private void AttachAuthenticationHandler(KestrelAuthHandler handler)
{
var auth = handler.HttpContext.Features.Get<IHttpAuthenticationFeature>();
if (auth == null)
{
auth = new HttpAuthenticationFeature();
handler.HttpContext.Features.Set(auth);
}
handler.PriorHandler = auth.Handler;
auth.Handler = handler;
}
private void DetachAuthenticationhandler(KestrelAuthHandler handler)
{
var auth = handler.HttpContext.Features.Get<IHttpAuthenticationFeature>();
if (auth != null)
{
auth.Handler = handler.PriorHandler;
}
}
}
KestrelAuthHandler.cs
internal class KestrelAuthHandler : IAuthenticationHandler
{
internal KestrelAuthHandler(HttpContext httpContext, ClaimsPrincipal user)
{
HttpContext = httpContext;
User = user;
}
internal HttpContext HttpContext { get; }
internal ClaimsPrincipal User { get; }
internal IAuthenticationHandler PriorHandler { get; set; }
public Task AuthenticateAsync(AuthenticateContext context)
{
if (User != null)
{
context.Authenticated(User, properties: null, description: null);
}
else
{
context.NotAuthenticated();
}
if (PriorHandler != null)
{
return PriorHandler.AuthenticateAsync(context);
}
return Task.FromResult(0);
}
public Task ChallengeAsync(ChallengeContext context)
{
bool handled = false;
switch (context.Behavior)
{
case ChallengeBehavior.Automatic:
// If there is a principal already, invoke the forbidden code path
if (User == null)
{
goto case ChallengeBehavior.Unauthorized;
}
else
{
goto case ChallengeBehavior.Forbidden;
}
case ChallengeBehavior.Unauthorized:
HttpContext.Response.StatusCode = 401;
// We would normally set the www-authenticate header here, but IIS does that for us.
break;
case ChallengeBehavior.Forbidden:
HttpContext.Response.StatusCode = 403;
handled = true; // No other handlers need to consider this challenge.
break;
}
context.Accept();
if (!handled && PriorHandler != null)
{
return PriorHandler.ChallengeAsync(context);
}
return Task.FromResult(0);
}
public void GetDescriptions(DescribeSchemesContext context)
{
if (PriorHandler != null)
{
PriorHandler.GetDescriptions(context);
}
}
public Task SignInAsync(SignInContext context)
{
// Not supported, fall through
if (PriorHandler != null)
{
return PriorHandler.SignInAsync(context);
}
return Task.FromResult(0);
}
public Task SignOutAsync(SignOutContext context)
{
// Not supported, fall through
if (PriorHandler != null)
{
return PriorHandler.SignOutAsync(context);
}
return Task.FromResult(0);
}
}
And in the Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseMiddleware<KestrelAuthenticationMiddleware>();
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = Configuration[AppConstants.Authority],
RequireHttpsMetadata = false,
AutomaticChallenge = true,
ScopeName = Configuration[AppConstants.ScopeName],
ScopeSecret = Configuration[AppConstants.ScopeSecret],
AutomaticAuthenticate = true
});
app.UseMvc();
}