In a blazor web assembly, for a authenticated user, I want to restrict any access to the app if they do not belong to certain AD group with certain role. Lets say I have a group with role 'xyz' configured in Azure portal. My app should only allow access to those users. For others, it should show a 401. I am trying to do it at a global level, and not individually to a controller or view. I have this set up in startup.cs in ConfigureServices :
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireRole("xyz")
.Build();
services.AddMvc(options => options.Filters.Add(new AuthorizeFilter(policy)));
This is not effective. I am expecting a 401 response or something along those lines. Please advice on what I am missing or something that helps me understand this a bit better.
To authenticate user with specified app role we need to add users as admin or viewer at AZURE AD> SELECT ROLE(viwer,admin) for both client and server app for app roles.
For example in startup.cs we can add something like below:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options =>
{
Configuration.Bind("AzureAd", options);
options.TokenValidationParameters.RoleClaimType =
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role";
},
options => { Configuration.Bind("AzureAd", options); });
services.AddControllersWithViews();
services.AddRazorPages();
}
For complete setup please refer this BLOG & for returning 401 response you can refer this Blog as well.
Related
Odd behavior in VS Code using Kestrel. In the Claims transformer I can get the identity.Name as the logged in user...me. But in the Role Handler the claims come up null and no user name. I would think if Windows authentication weren't working then it wouldn't work at all. Works in ASP.NET Core 5 with Visual Studio 2019 but not in ASP.NET Core 6 in VS Code. Yes, I understand that Visual Studio is using IIS Express and VS Code is using Kestrel.
UPDATE: I have looked at the request in Fiddler and it seems like the second request is actually being authenticated, I see a valid Authorization header in the request with Negotiate. So that would seem to indicate that the Windows auth part of this is working. I then see a 400 error which means that the request is badly formed. I'll have to take a look at that.
Further update. The real issue was authentication. I created a service principal name and got it working.
The explicit message in Fiddler is:
Authorization Header (Negotiate) appears to contain a Kerberos ticket:
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
ClaimsIdentity identity = principal.Identity as ClaimsIdentity;
string userName = identity.Name.ToLower();
bool auth = identity.IsAuthenticated;
string[] roles = new string[]{"admin"};
foreach (string role in roles)
{
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
return Task.FromResult(principal);
}
public class AuthenticatedRoleHandler : AuthorizationHandler<AuthenticatedRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,AuthenticatedRequirement requirement)
{
ClaimsPrincipal user = context.User;
if (user != null && user.Identity.IsAuthenticated) {
context.Succeed(requirement);
} else {
context.Fail();
}
return Task.CompletedTask;
}
}
I'm adding the Program.cs code.
string CorsPolicy = "CorsPolicy";
WebApplicationBuilder? builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseKestrel();
ConfigurationManager _configuration = builder.Configuration;
// Add services to the container.
IServiceCollection? services = builder.Services;
services.AddTransient<IActiveDirectoryUserService,
ActiveDirectoryUserService>();
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();
services.AddControllersWithViews();
services.AddScoped<IClaimsTransformation, MyClaimsTransformer>();//only
runs if authenticated, not being authenticated here
services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
options.AddPolicy("AuthenticatedOnly", policy => {
policy.Requirements.Add(new AuthenticatedRequirement(true));
});
});
services.AddSingleton<IAuthorizationHandler, AppUserRoleHandler>();
services.AddSingleton<IAuthorizationHandler, AuthenticatedRoleHandler>
();
services.AddCors(options =>
{
options.AddPolicy(CorsPolicy,
builder => builder
.WithOrigins("https://localhost:7021","https://localhost:44414") //Note: The URL must be specified without a trailing slash (/).
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
WebApplication app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
According to my test, it works well when I am using the Kestrel(.net 6) inside the VS(its same as run the application inside the Kestrel like VS code). The only difference between the windows authentication for the IIS express and the Kestrel is if you enable the windows authentication for the IIS express, all the request which come to the application will firstly need the authentication which means all the request come to the AuthenticatedRoleHandler has already been authenticated.
But if you use services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();`, the request will not firstly be authenticated, if the application find it doesn't contain any authentication relatede information, then it will return response to let the client put in the windows credential. This makes the AuthenticatedRoleHandler fired twice. The first one is authentication user name is null, but after the user typed in the credential, we could get the user name inside it.
My test result like below:
If this still not work at your side, please update the whole codes which related with how you register the requirement and how you use it.
I've had no luck getting any Role or Policy attributes working in .Net Core 3. I started my project with the .Net Core Angular starter project with authentication.
I figured this was something to do with the new .AddDefault methods so I have simplified it as much as I possibly can and it still doesn't work.
Here is my policy:
services.AddAuthorization(options =>
{
options.AddPolicy("IsAdmin", policy =>
policy.RequireClaim("role", "admin"));
});
Here is my controller:
[Authorize(Policy = "IsAdmin")]
[Route("api/[controller]")]
public class AdminController : Controller
{
...
I made a custom Profile service that adds the claim to the token,
var claims = new List<Claim>();
if (await _userManager.IsInRoleAsync(user, "Admin"))
{
claims.Add(new Claim(JwtClaimTypes.Role, "admin"));
}
context.IssuedClaims.AddRange(claims);
Inside my access token (from jwt.io):
Other parts of configure services:
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
...
services.AddAuthentication()
.AddIdentityServerJwt();
The plain [Authorize] tag is working fine with the access token on other controllers.
When I hit this controller with the access token I get a 403 response
What am I missing that is preventing this from working?
I try your code and find that the role claim key has been transformed to the standard Role ClaimsType : http://schemas.microsoft.com/ws/2008/06/identity/claims/role
So using ClaimTypes.Role will fix the problem:
services.AddAuthorization(options => {
options.AddPolicy("IsAdmin", policy =>
{
policy.RequireClaim(ClaimTypes.Role,"admin");
});
});
Demo
You should also be able to achieve this without needing a policy. ASP.NET automatically maps common claims to the Microsoft schema.
When you inspect your access token. You will see you are sending the role claim. But when you look at the claims in the controller, you will notice that it has been transformed to http://schemas.microsoft.com/ws/2008/06/identity/claims/role.
There are two things you can do. Either set the RoleClaimType to ClaimTypes.Role. Like so:
services.Configure<JwtBearerOptions>(IdentityServerJwtConstants.IdentityServerJwtBearerScheme, options => {
options.TokenValidationParameters.RoleClaimType = ClaimTypes.Role;
});
Or tell the JwtSecurityTokenHandler not to map default inbound claims like this:
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
Since it's a static property this can be done at any time. But I set it somewhere during my service registrations.
It seems to set up OpenIdConnect authentication from .NET Core 2.2 to IdentityServer3 I have to setup through generic AddOpenIdConnect() call, and in order for scope policy to work, I have overridden OnTokenValidated, where I parse the access token received, and add the scopes in it to the ClaimsPrincipal object.
I have found no other way of getting scope policy to work. This seems a bit hackish though. Is there a better or simpler way, so I don't need to override events, or at least not parse the access token? It is parsed in the framework anyhow, so I would suspect there were other functionality available to get scopes into the claims principal.
Moving our code from .NET 4.5.2 to .NET Core 2.2, I need to set up authentication towards our IdentityServer3 server in a very different way.
I was hoping new functionality in later framework allowed for simple setup of authentication towards IdentityServer3, but I've found no fitting example.
I saw someone saying that IdentityServer4.AccessTokenValidation NuGet package could work towards IdentityServer3, but only example I've found has been with simple JWT authentication not allowing implicit user login flow.
Consequently, I've ended up using standard ASP.NET Core libraries to set up openidconnect, and then I need to tweak the code to make it work.
Not sure if the code below handles all it needs to, but at least I've gotten where I can log in and use the new web site, and write cypress tests. Any suggestions on how to do this better or simpler would be appreciated.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseMvc();
}
public void ConfigureServices(IServiceCollection services)
{
// Without this, I get "Correlation failed." error from Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(o => {
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie().AddOpenIdConnect(o =>
{
o.Authority = "https://myidentityserver3.myfirm.com";
o.ClientId = "myidentityserver3clientname";
o.SignedOutRedirectUri = "https://localhost:50011/signout";
o.ResponseType = "id_token token";
o.SaveTokens = true;
o.Scope.Add("openid");
o.Scope.Add("roles");
o.Scope.Add("profile");
o.Scope.Add("customrequiredscopeforapi");
o.GetClaimsFromUserInfoEndpoint = false;
{
var old = o.Events.OnTokenValidated;
o.Events.OnTokenValidated = async ctx =>
{
if (old != null) await old(ctx);
var token = MyCustomAuthUtils.ParseBearerToken(ctx.ProtocolMessage.AccessToken);
foreach (var scope in token.Scopes)
{
ctx.Principal.AddIdentity(new ClaimsIdentity(new[] { new Claim("Scope", scope) }));
}
// Our controllers need access token to call other web api's, so putting it here.
// Not sure if that is a good way to do it.
ctx.Principal.AddIdentity(new ClaimsIdentity(new[] { new Claim("access_token", ctx.ProtocolMessage.AccessToken) }));
};
}
});
var mvcBuilder = services.AddMvc(o =>
{
o.Filters.Add(new AuthorizeFilter(ScopePolicy.Create("customrequiredscopeforapi")));
});
services.AddAuthorization();
}
The first thing is you don't need to manally decode the access token , just use ctx.SecurityToken.Claims in OnTokenValidated event to get all claims included in the token .
I'm not sure why you need to use scope to identify the permission . The scope parameter in the OIDC-conformant pipeline determines:
The permissions that an authorized application should have for a given resource server
Which standard profile claims should be included in the ID Token (if the user consents to provide this information to the application)
You can use role to identify whether current login user could access the protected resource . And the OpenID Connect middleware will help mapping the role claim to claim principle .
I would like to get help from community for one problem that I don't understand.
I create asp.net core 2 web application and I would like to configure the app to be able to login from the app via aspnetuser table or by using O365 Company account.
Then I followed multiple techniques described on the web included on MSDN website.
The app authentication works fine but Azure add returned : Error loading external login information.
I checked inside the code by generating identity views, the app failed on:
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
ErrorMessage = "Error loading external login information.";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
await _signInManager.GetExternalLoginInfoAsync(); return null and return the error message.
The application is correctly configured in azure AD and it work from my app if I remove the authentication from the app.
I configured my app middlewares as follow:
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.AddAuthentication(AzureADDefaults.AuthenticationScheme).AddCookie()
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = true;
});
And in configure method I added
app.UseAuthentication();
When I arrive on my login screen app (scaffolded by VS) all seems correct:
Login screen with two possibilities for authentication]:
Error message when i try Azure Active Directory method:
Can someone explain and help me to solve this problem?
Thanks in advance
The solution is to add cookieschemename as externalscheme. Below is sample code block in Startup.cs file.
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => { Configuration.Bind("AzureAd", options); options.CookieSchemeName = IdentityConstants.ExternalScheme; });
Unfortunately I had more or less the exact same problem. Although the Azure sample worked on its own, when I tried to integrate it to an existing application that uses Identity and other external authentication services, I could not get AzureAD to work. The interesting thing is that although in the output window I could see logging messages saying that the login was accomplished.
What I did (and this is more of a workaround rather than an exact solution to the problem) was to abandon using the Microsoft.AspNetCore.Authentication.AzureAD.UI package and I opted to go the longer way and configure OpenID manually for Azure. This article helped me immensely towards that end.
Having said that, I hope someone posts a more direct answer to your question.
I am applying authorize attibutes on each classes.
So is it possible to avoid this, and secure my entire web application at once?
Something like at "Namespace" level?
I am using .net core mvc application.
You should add your Authorization filter in ConfigureServices method on startup.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add(typeof(YourCustomAuthorizationAttribute));
});
}
None of the above worked. But I got the solution. So following worked for me.
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
I know this may be late. But ideally, the check must happen only if there is an authorization header found. It is never possible for all pages in a project to require authentication... There must be at least one login page that does not need authentication