Custom Login Path for ASP.NET CORE 6 and Azure AD - authentication

I currently have a page in "/login/index" with my logo and a form/button on it which on POST will initiate the challenge for MS Azure AD login to authenticate the user. The user is then redirected back to the home page after login. However, currently with the default setup for Azure AD authentication a user never sees this "/login/index" page because they are forced to MS Azures ADs login page for all request paths if not authenticated. Is there a way to force users to this initial login page I setup so that they can click the button to go authenticate?
My program.cs is as follows:
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using Project.Models;
using DocumentFormat.OpenXml.Office2016.Drawing.ChartDrawing;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToFolder("/Login");
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/files");
});
//authentication pipline
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
//We are using this so we can find the modified date later on. If we move to Box or Onedrive we may not need this.
var RootPath = builder.Environment.ContentRootPath;
var WebPath = builder.Environment.WebRootPath;
var fileDirectory = Path.Combine(Directory.GetParent(RootPath).Parent.ToString(), "files");
IFileProvider physicalProvider = new PhysicalFileProvider(fileDirectory);
builder.Services.AddSingleton<IFileProvider>(physicalProvider);
//Not needed. We are not using this level of abstraction but may move towards it one day so possibly keep.
var connectionString = builder.Configuration.GetConnectionString("DBContext");
builder.Services.AddDbContext<DbContext>(options => options.UseSqlServer(connectionString));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/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();
}
else
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
//We are making it so armsfiles are not accessible outside of arms so if we move to box or onedrive then the parameter may need to be removed.
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = physicalProvider,
RequestPath = "/files"
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
app.Run();
Before using Azure AD for authentication I would use this cookie policy in my startup class (I have since converted to minimal hosting model in program.cs) to force users to the login page (which although different now but still similar concept to what I am trying to achieve):
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(cookieOptions =>
{
cookieOptions.Cookie.Name = "UserLoginCookie";
cookieOptions.LoginPath = "/Login/";
cookieOptions.ExpireTimeSpan = TimeSpan.FromMinutes(30);
cookieOptions.SlidingExpiration = true;
});

I follow the official doc about Quickstart: Add sign-in with Microsoft to a web app. And I downloaded the repo and test it.
You can check the test result first, please confirm if this is what you want?
Test Result
What I changed in the sample project
Change the appsettings.json file in project.
Copy the code from Home/Index method, and create a new page LoginSuccess then paste the code to Home/LoginSuccess. And modify the attribute, you can copy my sample code directly.
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Identity.Web;
using Microsoft.Graph;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using active_directory_aspnetcore_webapp_openidconnect_v2.Models;
namespace active_directory_aspnetcore_webapp_openidconnect_v2.Controllers
{
[Authorize]
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly GraphServiceClient _graphServiceClient;
public HomeController(ILogger<HomeController> logger,
GraphServiceClient graphServiceClient)
{
_logger = logger;
_graphServiceClient = graphServiceClient;
}
[AllowAnonymous]
public async Task<IActionResult> Index()
{
//var user = await _graphServiceClient.Me.Request().GetAsync();
ViewData["ApiResult"] = "Demo";//user.DisplayName;
return View();
}
[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
public async Task<IActionResult> LoginSuccess()
{
var user = await _graphServiceClient.Me.Request().GetAsync();
ViewData["ApiResult"] = user.DisplayName;
return View();
}
[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
public IActionResult Privacy()
{
return View();
}
[AllowAnonymous]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
Create LoginSuccess page(copy from index page).

Related

Caching Issue: ASP.NET Core, failed to get access token

GetUserAccessTokenAsync by using this method I am trying to get the Token, The same code working fine in POC App, where as in the real application I am getting TokenNotFound.
User is authenticated and able to get the userId too, at the end I am getting account as null, it leads to the main error.
public async Task<string> GetUserAccessTokenAsync(string userId)
{
var account = await _app.GetAccountAsync(userId);
if (account == null) throw new ServiceException(new Error
{
Code = "TokenNotFound",
Message = "User not found in token cache. Maybe the server was restarted."
});
try
{
var result = await _app.AcquireTokenSilent(_scopes, account).ExecuteAsync();
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (Exception)
{
throw new ServiceException(new Error
{
Code = GraphErrorCode.AuthenticationFailure.ToString(),
Message = "Caller needs to authenticate. Unable to retrieve the access token silently."
});
}
}
Note: The same code working fine in POC application, I tried with same app resource and created new app resource too.
Please help me what exactly I am missing...
I have a sample with asp.net core mvc, which integrate MSAL to obtain access token for calling Ms graph api. I don't know what is POC App actually, but per my searching, AcquireTokenSilent may from MSAL.js
Here's the detail.
Adding configurations in appsetting file about azure ad:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "your_tenant_name.onmicrosoft.com",
"TenantId": "tenant_id",
"ClientId": "azure_ad_client_id",
"ClientSecret": "client_secret",
"CallbackPath": "/signin-oidc",//don't forget to set redirect url in azure portal
"SignedOutCallbackPath ": "/signout-callback-oidc"
}
installing packages:
<PackageReference Include="Microsoft.Identity.Web" Version="1.23.1" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.23.1" />
StartUp.cs, don't forget to add app.UseAuthentication();app.UseAuthorization(); in Configure method:
public void ConfigureServices(IServiceCollection services)
{
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "user.read" })
.AddInMemoryTokenCaches();
// Require authentication
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
// Add the Microsoft Identity UI pages for signin/out
.AddMicrosoftIdentityUI();
}
And my test Controller:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web;
using System.Diagnostics;
using System.Threading.Tasks;
using WebAppMvc.Models;
namespace WebAppMvc.Controllers
{
[Authorize]
public class HomeController : Controller
{
private readonly ITokenAcquisition _tokenAcquisition;
public HomeController(ITokenAcquisition tokenAcquisition)
{
_tokenAcquisition = tokenAcquisition;
}
public async Task<IActionResult> IndexAsync()
{
string[] scopes = new string[] { "user.read" };
string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
return View();
}
}
}
If the redirection url is wrong after signing in, just changing the url in browser manually to localhost:xxx/home
At Finally I have followed below link it helped to get rid of my all queries for graph API;
https://github.com/microsoftgraph/msgraph-training-aspnet-core

How to automatically login Okta user with active login session to .NET 4.5 Web Forms app

I've got a .NET 4.5 Web Forms app with Okta authentication on top. The authentication setup seems to be working fine; I can login and logout and get my Okta user info/claims from the context variable.
What I'd like to do is detect on page load whether a user already has an active Okta session in their browser and then log them into the application. Or if they don't have a session do nothing and stay on the application page.
Making a challenge call to the authentication manager
HttpContext.Current.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/Login.aspx" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
almost does what I want. If the user has an active session it'll do some redirects and log them in. But if they're not logged in they get sent to, and left on, the Okta login page. Which is not what I want.
I thought I would be able to access some cookies that Okta sets when a user logs in via an Okta page, but when checking through the browser debugger and checking Request.Cookies they don't seem to be available at that stage. And the context doesn't have access to the user info either.
edit: Also, if it helps, this is what my Startup.cs looks like
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
using IdentityModel.Client;
using Microsoft.AspNet.Identity;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using System.Collections.Generic;
using System.Configuration;
using System.Security.Claims;
[assembly: OwinStartup(typeof(WebApplication.Startup))]
namespace WebApplication
{
public class Startup
{
// These values are stored in Web.config. Make sure you update them!
private readonly string _clientId = ConfigurationManager.AppSettings["okta:ClientId"];
private readonly string _redirectUri = ConfigurationManager.AppSettings["okta:RedirectUri"];
private readonly string _authority = ConfigurationManager.AppSettings["okta:OrgUri"];
private readonly string _clientSecret = ConfigurationManager.AppSettings["okta:ClientSecret"];
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
public void ConfigureAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = _clientId,
ClientSecret = _clientSecret,
Authority = _authority,
RedirectUri = _redirectUri,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = "openid profile email offline_access",
TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name" },
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// Exchange code for access and ID tokens
var tokenClient = new TokenClient($"{_authority}/v1/token", _clientId, _clientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, _redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
var userInfoClient = new UserInfoClient($"{_authority}/v1/userinfo");
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var claims = new List<Claim>(userInfoResponse.Claims)
{
new Claim("code", n.Code),
new Claim("id_token", tokenResponse.IdentityToken),
new Claim("refresh_token", tokenResponse.RefreshToken),
new Claim("access_token", tokenResponse.AccessToken)
};
n.AuthenticationTicket.Identity.AddClaims(claims);
},
},
});
}
}
}

How to do Azure AD groups based authorization?

net core web api application. I have configured swagger for my web api app. I am doing authentication and authorization from swagger and I do not have webapp or SPA. Now I want to do authorization based on groups. When I saw JWT token I saw hasgroups: true rather than group ids. This is changed If more than 5 groups are associated with user. Please correct me If my understanding is wrong. So I have now hasgroups: true. So to get groups I need to call graph api. Once I get groups from graph API I need to create policies. This is my understanding and please correct me If I am on wrong track. Now I have my below web api app.
Startup.cs
public Startup(IConfiguration configuration)
{
Configuration = configuration;
azureActiveDirectoryOptions = Configuration.GetSection("AzureAd").Get<AzureActiveDirectoryOptions>();
swaggerUIOptions = Configuration.GetSection("Swagger").Get<SwaggerUIOptions>();
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services
.AddAuthentication(o =>
{
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Authority = azureActiveDirectoryOptions.Authority;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = new List<string>
{
azureActiveDirectoryOptions.AppIdUri,
azureActiveDirectoryOptions.ClientId
},
};
});
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = swaggerUIOptions.AuthorizationUrl,
TokenUrl = swaggerUIOptions.TokenUrl
});
c.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", new[] { "readAccess", "writeAccess" } }
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.OAuthClientId(swaggerUIOptions.ClientId);
c.OAuthClientSecret(swaggerUIOptions.ClientSecret);
c.OAuthRealm(azureActiveDirectoryOptions.ClientId);
c.OAuthAppName("Swagger");
c.OAuthAdditionalQueryStringParams(new { resource = azureActiveDirectoryOptions.ClientId });
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseAuthentication();
app.UseMvc();
}
}
I have API as below.
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private IHttpContextAccessor _httpContextAccessor;
public ValuesController(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
// GET api/values
[HttpGet]
public ActionResult<string> Get()
{
string owner = (User.FindFirst(ClaimTypes.Name))?.Value;
var accessToken = _httpContextAccessor.HttpContext.Request.Headers["Authorization"];
return owner;
}
}
Now After log in I can hit to API. Now I want to have something like Authorize(admin/user) based on the groups I want to control authorization. Now I am having trouble, where I should call graph api and get groups. Can some one help me to understand this? Any help would be appreciated. Thanks
Which protol and which flow you are using. ?
Yes , implict flow has the limit for groups claim . To use Microsoft Graph to get current user's groups , you can try below ways :
Use the on-behalf-of grant to acquire an access token that allows the API to call MS Graph as the user , here is code sample .
Use client credentials flow to acquire Microsoft Graph's access token in web api, this flow uses application's permission with no user context . Code sample here is for Azure AD V1.0 using ADAL . And here is code sample for Azure AD V2.0 using MSAL .

.Net Core 3 Identity register or login links not functional [duplicate]

After having a hard time getting my area to show with endpoint routing i managed to fix it in this self answered thread (albeit not in a very satisfactory way) : Issue after migrating from 2.2 to 3.0, default works but can't access area, is there anyway to debug the endpoint resolution?
However Identity UI doesn't show at all for me, i get redirected on challenge to the proper url but the page is blank. I have the identity UI nugget package added and, changing from mvc routing to endpoint routing, i didn't change anything that should break it.
I also don't seem to do much different than what the default project does and identity works there even if i add a route as i did in my hack.
As often the issue hides around the line and not on it i'm posting my whole startup file.
Regular (default) controllers work.
Admin area works (one of the page doesn't have authentication and i can access it)
Any other Admin area page redirect me to /Identity/Account/Login?ReturnUrl=%2Fback (expected behavior) but that page as well as any other /Identity page i tested is blank with no error while running in debug and with a debugger attached.
Any help is most appreciated, full startup bellow:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using FranceMontgolfieres.Models;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
namespace FranceMontgolfieres
{
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.AddSingleton<IConfiguration>(Configuration);
services
.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services
.AddDbContext<FMContext>(options => options
.UseLazyLoadingProxies(true)
.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services
.AddDefaultIdentity<IdentityUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<FMContext>();
services
.AddMemoryCache();
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = Configuration.GetConnectionString("SessionConnection");
options.SchemaName = "dbo";
options.TableName = "SessionCache";
});
services.AddHttpContextAccessor();
services
.AddSession(options => options.IdleTimeout = TimeSpan.FromMinutes(30));
services.AddControllersWithViews();
services.AddRazorPages();
}
// 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.UseDatabaseErrorPage();
}
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.UseRouting();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapAreaControllerRoute("Back", "Back", "back/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("default","{controller=Home}/{action=Index}/{id?}");
});
}
private async Task CreateRoles(IServiceProvider serviceProvider)
{
//initializing custom roles
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
string[] roleNames = { "Admin", "Manager", "Member" };
IdentityResult roleResult;
foreach (var roleName in roleNames)
{
roleResult = await RoleManager.CreateAsync(new IdentityRole(roleName));
}
}
}
}
The Identity UI is implemented using Razor Pages. For endpoint-routing to map these, add a call to MapRazorPages in your UseEndpoints callback:
app.UseEndpoints(endpoints =>
{
// ...
endpoints.MapRazorPages();
});

Assigning a user to a Role inside asp.net core will return this error "You do not have access to this resource."

I created a new asp.net core web application which uses individual user accounts. now i am trying to implement a simple role assignment scenario.
so i register a test user, where the user got added inside the AspNetUser table:-
then i add a new Role named "Administrator" inside the AspNetRole:-
then i added a new AspNetUserRole to link the user to the Role:-
then i added the following Authorize annotation on the About action method:-
[Authorize(Roles = "Administrator")]
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
but when i try to access the About action method using the user, i got this error:-
You do not have access to this resource."
EDIT
Here is the startup.cs , which i have not modified, so i think it contain the built-in code:-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WebApplication2.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace WebApplication2
{
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.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
I guess you manually create role and link role in AspNetUserRoletable after creating your user . Please don't forget to Logout user and login again , so role claims will get/update the new added role .
Your identity service is not configured for roles. AddDefaultIdentity cannot handle roles. You need AddIdentity
Instead of:
services.AddDefaultIdentity<IdentityUser>().AddEntityFrameworkStores<ApplicationDbContext>();
Try:
services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
Short answer
Add IdentityRole :
services.AddDefaultIdentity<IdentityUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
Long Answer
For properly using of roles/policies, you need to follow the below steps:
configure ApplicationDbContext for using IdentityRole
configure Identity service to use IdentityRole
configure application cookie
define authorization policies
configure authorization for razor pages
Notice : if you are using razor pages, Authorization attributes must be applied to the PageModel model not the actions
before proceeding with the solution, it is worth to mention that it is a best practice to use custom user and role models instead of IdentityUser and IdentityModel. This will help you add custom fields to the user and role easily.
So, first lets create our custom user and role models:
public class AppUser : IdentityUser
{
//custom fields can be defined here
}
public class AppRole : IdentityRole
{
//custom fields can be defined here
}
public class AppUserRole : IdentityUserRole<string>
{
public virtual AppUser User { get; set; }
public virtual AppRole Role { get; set; }
}
Now we can start with configuring ApplicationDbContext:
public class ApplicationDbContext : IdentityDbContext<AppUser, AppRole, string, IdentityUserClaim<string>, AppUserRole, IdentityUserLogin<string>, IdentityRoleClaim<string>, IdentityUserToken<string>>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
// AppUserRole relationship solution from so
// https://stackoverflow.com/questions/51004516/net-core-2-1-identity-get-all-users-with-their-associated-roles/51005445#51005445
builder.Entity<AppUserRole>(userRole =>
{
userRole.HasKey(ur => new { ur.UserId, ur.RoleId });
userRole.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
userRole.HasOne(ur => ur.User)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
}
}
}
configuring Identity
services.AddIdentity<AppUser, AppRole>(ops =>
{
ops.SignIn.RequireConfirmedEmail = true;
// Lockout settings
ops.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
ops.Lockout.MaxFailedAccessAttempts = 9;
ops.Lockout.AllowedForNewUsers = true;
// User settings
ops.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
configure application cookie
services.ConfigureApplicationCookie(ops =>
{
// Cookie settings
ops.Cookie.HttpOnly = false;
ops.ExpireTimeSpan = TimeSpan.FromMinutes(30);
// If the LoginPath isn't set, ASP.NET Core defaults the path to /Account/Login.
ops.LoginPath = $"/Identity/Account/Login";
// If the AccessDeniedPath isn't set, ASP.NET Core defaults the path to /Account/AccessDenied.
ops.AccessDeniedPath = $"/Identity/Account/AccessDenied";
ops.SlidingExpiration = true;
});
define authorization policies
services.AddAuthorization(ops =>
{
ops.AddPolicy("Administrator", policy =>
{
policy.RequireRole("Administrator");
});
});
Now it is possible to use roles/policies in different ways:
1- define authorization policies in startup
services.AddMvc()
.AddRazorPagesOptions(ops =>
{
ops.Conventions.AuthorizeFolder("/", "Administrator");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
2- apply authorization attributes on actions in case of MVC
[Authorize(Roles = "Administrator")]
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
3- or apply policy on PageModel for Razor Pages
[Authorize(Policy = "Administrator")]
public class AboutModel : PageModel
{
//-----
}
[UPDATE]
following to your comment below;
Let's consider that you will develop a news website management panel; basically you will need roles like Admins to manage the site settings and Authors to post the news pages, and probably Managers to approve the posted news. With this scenario you can survive with the default Identity settings and role based authorization.
But for example; if you need to allow only authors with more than 100 posted articles and are older than 25 to be able to approve their posts without the Managers approval then you need to customize the IdentityUser and use policy/claim based authorization, in this case the long answer will help you more to develop the application.
you can read more about authorization in the docs