I want to log stats for all calls to my .netcore webapi.
I have added a IAsyncActionFilter for this purpose and it picks up on all the actions.
But I also have Jwt Bearer Authentication enabled and am using the AuthorizeAttribute on my controller to limit access. When access is denied the Action filter will not be hit.
Whats the best way to add some custom logging (statsd) for authentication in general and failures in particular?
public void ConfigureServices(IServiceCollection services)
{
....
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// base-address of your identityserver
options.Authority = Configuration["Authority"]; ;
// name of the API resource
options.Audience = "myAudience";
});
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseAuthentication();
...
}
I Notice that JwtBearerOptions has JwtBearerEvents Events but I cant get this to work.
Edit : It looks like I am hitting the api with no token at all and the JWT Auth handler returns AuthenticateResult.NoResult() without calling the Events.AuthenticationFailed
https://github.com/aspnet/Security/blob/ba1eb281d135400436c52c17edc71307bc038ec0/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L63-L83
Edit 2 : Very frustrating. Looks like the correct place to log would be in Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter But this is automatically added when you use [Authorize] and is impossible to override, remove or replace?
The JwtBearerOptions.cs class exposes an JwtBearerEvents parameter where you can declare your events like this
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// base-address of your identityserver
options.Authority = Configuration["Authority"]; ;
// name of the API resource
options.Audience = "myAudience";
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
//Log failed authentications
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
//Log successful authentications
return Task.CompletedTask;
}
};
});
I don´t have any experience with Jwt Bearer Authentication - but we´ve had a similar situation. We´ve ended up with a new BaseController where we´re able to Log basic user information and distinguish between [AllowAnonymous] and [Authorize]:
public class NewBaseController : Controller
{
protected UserManager<MyUser> UserManager;
protected MyUser CurrentUser;
protected ILogger Logger;
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
UserManager = context.HttpContext.RequestServices.GetService<UserManager<MyUser>>();
var loggerFactory = context.HttpContext.RequestServices.GetService<ILoggerFactory>();
Logger = loggerFactory.CreateLogger(GetType());
// Check if Action is annotated with [AllowAnonymous]
var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
var anonymousAllowed = controllerActionDescriptor.MethodInfo
.GetCustomAttributes(inherit: true)
.Any(a => a.GetType().Equals(typeof(AllowAnonymousAttribute)));
if (!anonymousAllowed)
{
ApplicationUser = UserManager.GetUserAsync(User).Result;
if (ApplicationUser == null)
// do some stuff
Logger.LogInformation("User is {UserId}", CurrentUser.Id);
}
else
{
Logger.LogInformation("User is {User}", anonymous);
}
}
}
Bonus: Every Controller that derives from this base already has an instance of UserManager, ILogger and CurrentUser.
Hope that comes close to what you´re looking for...
Related
I have created a microservice using .Net 5 which has some endpoints which can only be called with a jwtBearertoken.
The ConfigureServices and Configure methods in the StartUp class look like this:
public void ConfigureServices(IServiceCollection services)
{
ConfigureDatabaseServices(services);
ConfigureMyProjectClasses(services);
services.AddVersioning();
services.AddControllers();
services.AddAuthentication(_configuration);
// Add framework services.
var mvcBuilder = services
.AddMvc()
.AddControllersAsServices();
ConfigureJsonSerializer(mvcBuilder);
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment webEnv,
ILoggerFactory loggerFactory,
IHostApplicationLifetime applicationLifetime)
{
_logger = loggerFactory.CreateLogger("Startup");
try
{
app.Use(async (context, next) =>
{
var correlationId = Guid.NewGuid();
System.Diagnostics.Trace.CorrelationManager.ActivityId = correlationId;
context.Response.Headers.Add("X-Correlation-ID", correlationId.ToString());
await next();
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
applicationLifetime.ApplicationStopped.Register(() =>
{
LogManager.Shutdown();
});
}
catch (Exception e)
{
_logger.LogError(e.Message);
throw;
}
}
AuthenticationExtensions:
public static class AuthenticationExtensions
{
public static void AddAuthentication(this IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.Authority = configuration["Authorization:Authority"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
}
I'm using an authorization server for the microservice to validate the token.
After adding an [Authorize] attribute above the controllers postman returns 401 Unauthorized and the integration tests I had created before adding Authentication also return Unauthorized as expected.
Now I am trying to figure out how I can change my integration tests by adding a JwtBearerToken and mocking the response from the authorization server so my tests will pass again.
How can I achieve this?
My answer is not 100% integrated, because we will add an extra auth scheme. TL;DR: You are not testing if your auth works, but working around it.
It would be best to use an ACTUAL token, but perhaps this solution is a nice middle ground.
You could create another auth scheme like DevBearer where you can specify an account, for example if you send the auth header DevBearer Customer-John, the application would recognize you as Customer John.
I use this approach during development because it is very easy to just test different users quickly. My code looks something like this:
Startup.Auth.cs
private void ConfigureAuthentication(IServiceCollection services)
{
services.AddHttpContextAccessor();
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = "Audience";
options.Authority = "Authority";
});
#if DEBUG
if (Environment.IsDevelopment())
{
AllowDevelopmentAuthAccounts(services);
return;
}
#endif
// This is custom and you might need change it to your needs.
services.AddAuthorization();
}
#if DEBUG
// If this is true, you can use the Official JWT bearer login flow AND Development Auth Account (DevBearer) flow for easier testing.
private static void AllowDevelopmentAuthAccounts(IServiceCollection services)
{
services.AddAuthentication("DevBearer").AddScheme<DevelopmentAuthenticationSchemeOptions, DevelopmentAuthenticationHandler>("DevBearer", null);
// This is custom and you might need change it to your needs.
services.AddAuthorization();
}
#endif
Custom Policies Hint
// Because my Policies/Auth situation is different than yours, I will only post a hint that you might want to use.
// I want to allow calls from the REAL flow AND DevBearer flow during development so I can easily call my API using the DevBearer flow, or still connect it to the real IDentityServer and front-end for REAL calls.
var policyBuilder = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser();
// The #IF adds an extra "security" check so we don't accidentally activate the development auth flow on production
#if DEBUG
if (_allowDevelopmentAuthAccountCalls)
{
policyBuilder.AddAuthenticationSchemes("DevBearer").RequireAuthenticatedUser();
}
#endif
return policyBuilder;
Auth handler
#if DEBUG
using System;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace NAMESPACE
{
public class DevelopmentAuthenticationHandler : AuthenticationHandler<DevelopmentAuthenticationSchemeOptions>
{
public DevelopmentAuthenticationHandler(
IOptionsMonitor<DevelopmentAuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Context.Request.Headers.TryGetValue("Authorization", out var authorizationHeader))
{
return AuthenticateResult.Fail("Unauthorized");
}
var auth = AuthenticationHeaderValue.Parse(authorizationHeader);
if (auth.Scheme == "Bearer")
{
// If Bearer is used, it means the user wants to use the REAL authentication method and not the development accounts.
return AuthenticateResult.Fail("Bearer requests should use the real JWT validation scheme");
}
// Dumb workaround for NSwag/Swagger: I can't find a way to make it automatically pass "DevBearer" in the auth header.
// Having to type DevBearer everytime is annoying. So if it is missing, we just pretend it's there.
// This means you can either pass "ACCOUNT_NAME" in the Authorization header OR "DevBearer ACCOUNT_NAME".
if (auth.Parameter == null)
{
auth = new AuthenticationHeaderValue("DevBearer", auth.Scheme);
}
IEnumerable<Claim> claims;
try
{
var user = auth.Parameter;
claims = GetClaimsForUser(user);
}
catch (ArgumentException e)
{
return AuthenticateResult.Fail(e);
}
var identity = new ClaimsIdentity(claims, "DevBearer");
var principal = new ClaimsPrincipal(identity);
// Add extra claims if you want to
await Options.OnTokenValidated(Context, principal);
var ticket = new AuthenticationTicket(principal, "DevBearer");
return AuthenticateResult.Success(ticket);
}
private static IEnumerable<Claim> GetClaimsForUser(string? user)
{
switch (user?.ToLowerInvariant())
{
// These all depend on your needs.
case "Customer-John":
{
yield return new("ID_CLAIM_NAME", Guid.Parse("JOHN_GUID_THAT_EXISTS_IN_YOUR_DATABASE").ToString(), ClaimValueTypes.String);
yield return new("ROLE_CLAIM_NAME", "Customer", ClaimValueTypes.String);
break;
}
default:
{
throw new ArgumentException("Can't set specific account for local development because the user is not recognized", nameof(user));
}
}
}
}
public class DevelopmentAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
public Func<HttpContext, ClaimsPrincipal, Task> OnTokenValidated { get; set; } = (context, principal) => { return Task.CompletedTask; };
}
}
#endif
With something like this, you could do an API call with an authorization header like DevBearer Customer-John and it would add the ID and role claim to the context, allowing auth to succeed :)
I am working on ASP.NET Core Web API. I am trying to create a custom Authorize attribute but I am stuck. I could not understand what I am missing. I have the following code for the Authorize attribute and filter:
public class AuthorizeAttribute : TypeFilterAttribute
{
public AuthorizeAttribute(params string[] claim) : base(typeof(AuthorizeFilter))
{
Arguments = new object[] { claim };
}
}
public class AuthorizeFilter : IAuthorizationFilter
{
readonly string[] _claim;
public AuthorizeFilter(params string[] claim)
{
_claim = claim;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var IsAuthenticated = context.HttpContext.User.Identity.IsAuthenticated;
var claimsIndentity = context.HttpContext.User.Identity as ClaimsIdentity;
if (IsAuthenticated)
{
bool flagClaim = false;
foreach (var item in _claim)
{
if (context.HttpContext.User.HasClaim("Role", item))
flagClaim = true;
}
if (!flagClaim)
{
//if (context.HttpContext.Request.IsAjaxRequest())
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; //Set HTTP 403
//else
// context.Result = new RedirectResult("~/Login/Index");
}
}
else
{
//if (context.HttpContext.Request.IsAjaxRequest())
//{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; //Set HTTP 401 -
//}
//else
//{
// context.Result = new RedirectResult("~/Login/Index");
//}
}
return;
}
}
I have copied this code from somewhere and commented unnecessary lines.
Here is my controller class where I am trying to put this:
[Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
public class JobController : ControllerBase
{
// GET: api/<JobController>
[HttpGet]
[ActionName("GetAll")]
public List<Job> Get()
{
return JobDataLog.GetAllJobQueue();
}
// GET api/<JobController>/5
[HttpGet("{ID}")]
[ActionName("GetByID")]
public Job Get(Guid ID)
{
return JobDataLog.GetJob(ID);
}
// GET api/<JobController>/5
[HttpGet]
[ActionName("GetCount")]
public int GetCount()
{
return JobDataLog.GetJobTotal();
}
}
Also the Configure and ConfigureService methods of Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(60);
});
var tokenKey = Configuration.GetValue<string>("TokenKey");
var key = Encoding.ASCII.GetBytes(tokenKey);
services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; })
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddSingleton<IJWTAuthenticationManager>(new JWTAuthenticationManager(tokenKey));
}
// 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();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCookiePolicy();
app.UseSession();
app.Use(async (context, next) =>
{
var JWToken = context.Session.GetString("JWToken");
if (!string.IsNullOrEmpty(JWToken))
{
context.Request.Headers.Add("Authorization", "Bearer " + JWToken);
}
await next();
});
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
The problem is that even this controller has the Authorize attribute, all the actions are being called even the Authorize filter invalidates the authorization.
Also when I placed the following code in the OnAuthorization method:
context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized);
It blocked the access of all actions, including those which have an AllowAnnoynmous attribute.
Please help me, I have been stuck on this for last 3 hours.
If you really want to use a custom AuthorizeAttribute, here you go, this works. :)
You'll have a few squiggly lines, but VS will be able to automatically add the using statements.
The original code had multiple problems:
Setting Reponse.StatusCode doesn't actually lead to a response being returned.
HttpContext.User wouldn't be populated in the first place, because ASP.NET Core only attempts to authenticate the user and populate the user's claims/identity if an endpoint is secured with the built-in AuthorizeAttribute. The following code solves this by deriving from AuthorizeAttribute.
In this case the additional filter factory class wasn't needed, since you're not injecting dependencies. Though, if you had to inject, you'd be out of luck I think, because you couldn't derive both from TypeFilterAttribute and AuthorizeAttribute, and the claims list would be always empty.
Working code
public class MyAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
readonly string[] _requiredClaims;
public MyAuthorizeAttribute(params string[] claims)
{
_requiredClaims = claims;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var isAuthenticated = context.HttpContext.User.Identity.IsAuthenticated;
if (!isAuthenticated)
{
context.Result = new UnauthorizedResult();
return;
}
var hasAllRequredClaims = _requiredClaims.All(claim => context.HttpContext.User.HasClaim(x => x.Type == claim));
if (!hasAllRequredClaims)
{
context.Result = new ForbidResult();
return;
}
}
}
You should probably use policies instead
The reason why this works in such a crappy way is that the ASP.NET Core team doesn't want you to write custom Authorize Attributes. See this answer on the subject. The 'proper' way is to create policies, and assign your claim requirements to those policies. But I also think it's silly that authorization is so inflexible and lacking support for basic scenarios.
Here is Startup.cs from SignInWithAppleSample:
public void ConfigureServices(IServiceCollection services)
{
// ...
services
.AddAuthentication(options => options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/signin";
options.LogoutPath = "/signout";
})
.AddApple(options =>
{
options.ClientId = Configuration["AppleClientId"];
options.KeyId = Configuration["AppleKeyId"];
options.TeamId = Configuration["AppleTeamId"];
options.UsePrivateKey(
(keyId) =>
Environment.ContentRootFileProvider.GetFileInfo($"AuthKey_{keyId}.p8"));
});
// ...
}
public void Configure(IApplicationBuilder app)
{
// ...
app.UseAuthentication();
// ...
}
Here's controller's method, which uses for authentication:
public class AuthenticationController : Controller
{
[HttpPost("~/signin")]
public IActionResult SignIn()
=> Challenge(new AuthenticationProperties { RedirectUri = "/redirect/signin/apple" }, AppleAuthenticationDefaults.AuthenticationScheme);
[HttpGet("~/signout")]
[HttpPost("~/signout")]
public IActionResult SignOut()
=> SignOut(new AuthenticationProperties { RedirectUri = "/redirect/signout/apple" }, CookieAuthenticationDefaults.AuthenticationScheme);
}
It works, cookies set, I can see my claims.
In my project, I don't use built-in ASP.NET Core, I simply need to handle request on /redirect/signin/apple and return access token, identity token, etc. to my clients, and maybe check in DB that this user is already registered. Client's don't want cookies, they accept only Bearer tokens.
So, the question is, how can I get Bearer token instead of cookies?
I've setup oauth authorization in an MVC aspnet core 2 application.
It works as intended when I use the [Authorize] attribute but I can't get it to work with my middleware RequestHandler.
I tried creating a service that calls the context.ChallengeAsync() method but it fails when called from the middleware (the call never redirects).
If the user isn't already logged in the consent page is never shown and the token returned is null.
If the user was already logged in the call returns the token.
The service does work when called from inside a controller (instead of using [Authorize])
So how do I get it to work ? I want to make sure the user is authorized (so that i have access to a token) before continuing...
Here are the relevant code sections:
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OAuthOptionsDefaults.SchemeName;
})
.AddCookie()
.AddOAuth(OAuthOptionsDefaults.SchemeName,
options => { options.SaveTokens = true; /*other options*/ });
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IOAuthService, OAuthService>();
services.AddTransient<IRequestHandler, RequestHandler>();
}
public void Configure(IApplicationBuilder app, IRequestHandler requestHandler, ..)
{
app.UseExceptionHandler("/Home/Error");
app.UseStaticFiles();
app.UseAuthentication();
app.Map(new PathString("/myapi"), appbuilder =>
{
appbuilder.Run(async (context) =>
{
await requestHandler.Handle(context, "url to an api", "an api key");
});
});
app.UseMvcWithDefaultRoute();
}
}
RequestHandler.cs
public class RequestHandler : IRequestHandler
{
//[Authorize] doesn't work
public async Task Handle(HttpContext context, string apiUrl, string apiKey)
{
//injected IOAuthService _service;
//tried with and without context as parameter
var accessToken = await _service.GetAccesTokenAsync();
//do stuff ...
}
}
OAuthService.cs
public class OAuthService : IOAuthService
{
private async Task<string> GetAccesTokenAsyncHelper(HttpContext context)
{
var isAuthenticated = ((ClaimsIdentity)context.User.Identity)?.IsAuthenticated ?? false;
if (!isAuthenticated)
{
//tried with and without setting schemename
await context.ChallengeAsync(OAuthOptionsDefaults.SchemeName);
}
return await context.GetTokenAsync(OAuthOptionsDefaults.SchemeName, "access_token");
}
public async Task<string> GetAccesTokenAsync()
{
//injected IHttpContextAccessor _accessor;
var context = _accessor.HttpContext;
return await GetAccesTokenAsyncHelper(context);
}
public async Task<string> GetAccesTokenAsync(HttpContext context)
{
return await GetAccesTokenAsyncHelper(context);
}
}
Edit: made the question shorter and more to point in the hopes of someone answering it.
You need to add Identity to the user property of HttpContext
context.User.AddIdentity((ClaimsIdentity) principal.Identity);
I have added a tutorial here explaining how to authorize HttpContext in Middleware with JWT from the URL
hope this will be helpful :)
I am working on a web app that needs to integrate with an existing user database. I would still like to use the [Authorize] attributes, but I don't want to use the Identity framework. If I did want to use the Identity framework I would add something like this in the startup.cs file:
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireNonLetterOrDigit = false;
}).AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
I'm assuming I have to add something else there, and then create some kind of class that implements a specific interface? Can somebody point me in the right direction? I'm using RC1 of of asp.net 5 right now.
From what I learned after several days of research,
Here is the Guide for ASP .Net Core MVC 2.x Custom User Authentication
In Startup.cs :
Add below lines to ConfigureServices method :
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(
CookieAuthenticationDefaults.AuthenticationScheme
).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
});
services.AddMvc();
// authentication
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
services.AddTransient(
m => new UserManager(
Configuration
.GetValue<string>(
DEFAULT_CONNECTIONSTRING //this is a string constant
)
)
);
services.AddDistributedMemoryCache();
}
keep in mind that in above code we said that if any unauthenticated user requests an action which is annotated with [Authorize] , they well force redirect to /Account/Login url.
Add below lines to Configure method :
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler(ERROR_URL);
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: DEFAULT_ROUTING);
});
}
Create your UserManager class that will also manage login and logout. it should look like below snippet (note that i'm using dapper):
public class UserManager
{
string _connectionString;
public UserManager(string connectionString)
{
_connectionString = connectionString;
}
public async void SignIn(HttpContext httpContext, UserDbModel user, bool isPersistent = false)
{
using (var con = new SqlConnection(_connectionString))
{
var queryString = "sp_user_login";
var dbUserData = con.Query<UserDbModel>(
queryString,
new
{
UserEmail = user.UserEmail,
UserPassword = user.UserPassword,
UserCellphone = user.UserCellphone
},
commandType: CommandType.StoredProcedure
).FirstOrDefault();
ClaimsIdentity identity = new ClaimsIdentity(this.GetUserClaims(dbUserData), CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
}
}
public async void SignOut(HttpContext httpContext)
{
await httpContext.SignOutAsync();
}
private IEnumerable<Claim> GetUserClaims(UserDbModel user)
{
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id().ToString()));
claims.Add(new Claim(ClaimTypes.Name, user.UserFirstName));
claims.Add(new Claim(ClaimTypes.Email, user.UserEmail));
claims.AddRange(this.GetUserRoleClaims(user));
return claims;
}
private IEnumerable<Claim> GetUserRoleClaims(UserDbModel user)
{
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id().ToString()));
claims.Add(new Claim(ClaimTypes.Role, user.UserPermissionType.ToString()));
return claims;
}
}
Then maybe you have an AccountController which has a Login Action that should look like below :
public class AccountController : Controller
{
UserManager _userManager;
public AccountController(UserManager userManager)
{
_userManager = userManager;
}
[HttpPost]
public IActionResult LogIn(LogInViewModel form)
{
if (!ModelState.IsValid)
return View(form);
try
{
//authenticate
var user = new UserDbModel()
{
UserEmail = form.Email,
UserCellphone = form.Cellphone,
UserPassword = form.Password
};
_userManager.SignIn(this.HttpContext, user);
return RedirectToAction("Search", "Home", null);
}
catch (Exception ex)
{
ModelState.AddModelError("summary", ex.Message);
return View(form);
}
}
}
Now you are able to use [Authorize] annotation on any Action or Controller.
Feel free to comment any questions or bug's.
Creating custom authentication in ASP.NET Core can be done in a variety of ways. If you want to build off existing components (but don't want to use identity), checkout the "Security" category of docs on docs.asp.net. https://docs.asp.net/en/latest/security/index.html
Some articles you might find helpful:
Using Cookie Middleware without ASP.NET Identity
Custom Policy-Based Authorization
And of course, if that fails or docs aren't clear enough, the source code is at
https://github.com/dotnet/aspnetcore/tree/master/src/Security which includes some samples.
I would like to add something to brilliant #AmiNadimi answer for everyone who going implement his solution in .NET Core 3:
First of all, you should change signature of SignIn method in UserManager class from:
public async void SignIn(HttpContext httpContext, UserDbModel user, bool isPersistent = false)
to:
public async Task SignIn(HttpContext httpContext, UserDbModel user, bool isPersistent = false)
It's because you should never use async void, especially if you work with HttpContext. Source: Microsoft Docs
The last, but not least, your Configure() method in Startup.cs should contains app.UseAuthorization and app.UseAuthentication in proper order:
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.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
#Manish Jain, I suggest to implement the method with boolean return:
public class UserManager
{
// Additional code here...
public async Task<bool> SignIn(HttpContext httpContext, UserDbModel user)
{
// Additional code here...
// Here the real authentication against a DB or Web Services or whatever
if (user.Email != null)
return false;
ClaimsIdentity identity = new ClaimsIdentity(this.GetUserClaims(dbUserData), CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
// This is for give the authentication cookie to the user when authentication condition was met
await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
return true;
}
}