Azure SSO OnTokenValidated is not working - c#

I am using the below code on .net core 3.1 startups.cs class. It is not hitting the below line. In my case, I am getting a token from Azure AD and want to check if the user exists on our application database(external database). Below is the sample code I am using
services.AddProtectedWebApi(Configuration);
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
await existingOnTokenValidatedHandler(context);
context.Fail("user not avilable in database");
// your code to add extra claims that will be executed after the current event implementation.
};
});

Default authentication scheme in Microsoft.Identity.Web for JwtBearerOptions is JwtBearerDefaults.AuthenticationScheme ("Bearer") as mentioned in documet for AddProtectedWebApi method. The authentication scheme AzureADDefaults.JwtBearerAuthenticationScheme ("AzureADJwtBearer") is not registered so it is ignored.
Update the code by replacing AzureADDefaults.JwtBearerAuthenticationScheme with JwtBearerDefaults.AuthenticationScheme as shown below:
services.AddProtectedWebApi(Configuration);
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
await existingOnTokenValidatedHandler(context);
context.Fail("user not avilable in database");
// your code to add extra claims that will be executed after the current event implementation.
};
});
Another way to extend custom token validation can be found in below code:
services.AddProtectedWebApi(options =>
{
Configuration.Bind("AzureAd", options);
options.Events = new JwtBearerEvents();
options.Events.OnTokenValidated = async context =>
{
//your code for additional validation.
};
},
options =>
{
Configuration.Bind("AzureAd", options);
});

Related

Conditional windows auth on AspNetCore2 with HTTP.SYS

I'm using AspNetCore 2 with HTTP.SYS. I'm trying to implement an authentication scheme that
1. Performs custom (stateless) authentication if a request has a given header, or
2. If not, defaults back to windows auth
My initial attempt was adding a policy scheme that selects the appropriate authentication scheme based on the request. This doesn't quite work though - it looks that Http.sys authentication is done BEFORE my policy selector is even invoked (see comment in the code snippet).
In a desparate and clueless attempt, I've fiddled around with setting AllowAnonymous to true, but that just seems to lead to Windows auth never being used.
Is there any way to select the auth scheme properly?
var host = new WebHostBuilder()
.ConfigureServices(services =>
{
services
.AddAuthentication("DynamicAuthenticationScheme")
.AddScheme<AuthenticationSchemeOptions, CustomAuth>("Custom", _ => { })
.AddPolicyScheme("DynamicAuthenticationScheme", "Default system policy", cfgOptions =>
{
cfgOptions.ForwardDefaultSelector = ctx =>
{
// Auth looks to be done beforehand already; if setting a breakpoint here, ctx.User is already given
if (ctx.Request.Headers.ContainsKey("CUSTOM-AUTH"))
return "Custom";
return HttpSysDefaults.AuthenticationScheme;
};
});
})
.UseHttpSys(options =>
{
options.Authentication.Schemes = AuthenticationSchemes.NTLM | AuthenticationSchemes.Negotiate;
options.Authentication.AllowAnonymous = false;
})
.UseUrls("http://localhost:7200")
.Configure(app =>
{
app.UseAuthentication();
app.Map(new PathString("/test"), cfg =>
cfg.Use(async (context, next) =>
{
await context.Response.WriteAsync($"Hello {context.User.Identity?.Name}");
}));
})
.Build();
await host.StartAsync();
I've "solved" this now by adding a custom middleware:
class MyMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if(ShouldUseCustomAuth(context))
{
var authResult = await context.AuthenticateAsync("Custom");
if(authResult.Succeeded)
{
context.User = auth.Principal;
await next(context);
return;
}
}
await context.ChallengeAsync("Windows");
}
}
I'll leave the question open for a moment though in case anyone has a nicer solution (maybe involving policies after all?)

Include Claims In IdentityServer4/AspnetIdentity JwtToken

I created a new react web application using visual studio and the react application template: https://learn.microsoft.com/en-us/aspnet/core/client-side/spa/react?view=aspnetcore-5.0&tabs=visual-studio
When I created the app, I also chose the Individual user accounts authentication option:
I created an authorization policy like this:
services.AddAuthorization(config =>
{
config.AddPolicy("ShouldBeAdmin",
options => options.RequireClaim("Admin"));
});
My user in the aspnet identity database had the claim associated with it:
When I log in with my user, the jwt token I get does not contain the Admin claim, so endpoints protected with my authorization rule do not work. How do I get the claims into the jwt token?
I got a workaround for this problem. While this doesn't add the claims to the jwt token, I can look up the claims from the database each time a request comes in using the onTokenValidated event. Something like this:
services.Configure<JwtBearerOptions>(
IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
options =>
{
var onTokenValidated = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
await onTokenValidated(context);
var userManger = context.HttpContext.RequestServices
.GetRequiredService<UserManager<ApplicationUser>>();
var user = await userManger.FindByIdAsync(context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value);
if (user == null)
{
return;
}
var claims = await userManger.GetClaimsAsync(user);
var appIdentity = new ClaimsIdentity(claims);
context.Principal?.AddIdentity(appIdentity);
};
});
This solution is based on this Microsoft documentation: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-5.0#customize-the-api-authentication-handler
And this blob post:
https://joonasw.net/view/adding-custom-claims-aspnet-core-2
I'm still looking into implementing IProfileService based on abdusco's comment.

How to access custom claim in aspnet core application authorized using Identity Server

I'm following Identity Server quickstart template, and trying to setup the following
Identity server aspnet core app
Mvc client, that authenticates to is4 and also calls webapi client which is a protected api resource.
The ApplicationUser has an extra column which I add into claims from ProfileService like this:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
if (user == null)
return;
var principal = await _claimsFactory.CreateAsync(user);
if (principal == null)
return;
var claims = principal.Claims.ToList();
claims.Add(new Claim(type: "clientidentifier", user.ClientId ?? string.Empty));
// ... add roles and so on
context.IssuedClaims = claims;
}
And finally here's the configuration in Mvc Client app ConfigureServices method:
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "mvc-secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("offline_access");
options.Scope.Add("api1");
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapUniqueJsonKey("clientidentifier", "clientidentifier");
});
With GetClaimsFromUserInfoEndpoint set to true I can access the custom claim in User.Identity, but this results in 2 calls for ProfileService.
If I remove or set to false then this claim is still part of access_token, but not part of id_token, and then I can't access this specific claim from context User.
Is there a better way I can access this claim from User principal without resulting in 2 calls (as it's now)? or perhaps reading access_token from context and updating user claims once the token is retrieved?
thanks :)
Turns out that Client object in identity server has this property that does the job:
//
// Summary:
// When requesting both an id token and access token, should the user claims always
// be added to the id token instead of requring the client to use the userinfo endpoint.
// Defaults to false.
public bool AlwaysIncludeUserClaimsInIdToken { get; set; }
As explained in the lib metadata setting this to true for a client, then it's not necessary for the client to go and re-get the claims from endpoint
thanks everybody :)
If you want to access custom claims in client side over those added in identity server just follow these steps, it worked for me. I imagine you implement both client and identity server as separated projects in asp.net core and they are ready, you now want to play with claims or maybe want to authorize by role-claim and so on, alright let's go
create a class that inherits from "IClaimsTransformation" like this:
public class MyClaimsTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var userName = principal.Identity.Name;
var clone = principal.Clone();
var newIdentity = (ClaimsIdentity)clone.Identity;
var user = config.GetTestUsers().Where(p => p.Username == userName).First();
if (user != null)
{
var lstUserClaims = user.Claims.Where(p => p.Type == JwtClaimTypes.Role).ToList();
foreach (var item in lstUserClaims)
if (!newIdentity.Claims.Where(p => p.ValueType == item.ValueType && p.Value == item.Value).Select(p => true).FirstOrDefault())
newIdentity.AddClaim(item);
}
return Task.FromResult(principal);
}
}
But be aware this class will call multiple times over user authentication so i added a simple code to prevent multiple duplicate claim. also you have user name of authenticated user too.
Next create another class like this:
public class ProfileService : IProfileService
{
//private readonly UserManager<ApplicationUser> userManager;
public ProfileService(/*UserManager<ApplicationUser> userManager*/ /*, SignInManager<ApplicationUser> signInManager*/)
{
//this.userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.AddRequestedClaims(context.Subject.Claims);
var collection = context.Subject.Claims.Where(p => p.Type == JwtClaimTypes.Role).ToList();
foreach (var item in collection)
{
var lst = context.IssuedClaims.Where(p => p.Value == item.Value).ToList();
if (lst.Count == 0)
context.IssuedClaims.Add(item);
}
await Task.CompletedTask;
}
public async Task IsActiveAsync(IsActiveContext context)
{
//context.IsActive = true;
await Task.FromResult(0); /*Task.CompletedTask;*/
}
}
This class will call by several context but it's okay cause we added our custom claim(s) at part #1 at this code
foreach (var item in lstUserClaims)
if (!newIdentity.Claims.Where(p => p.ValueType == item.ValueType && p.Value == item.Value).Select(p => true).FirstOrDefault())
newIdentity.AddClaim(item);
This is your basic startup.cs at identity server side:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => options.EnableEndpointRouting = false);
services.AddTransient<Microsoft.AspNetCore.Authentication.IClaimsTransformation, MyClaimsTransformation>();
services.AddIdentityServer().AddDeveloperSigningCredential()
.AddInMemoryApiResources(config.GetApiResources())
.AddInMemoryIdentityResources(config.GetIdentityResources())
.AddInMemoryClients(config.GetClients())
.AddTestUsers(config.GetTestUsers())
.AddInMemoryApiScopes(config.GetApiScope())
.AddProfileService<ProfileService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
Pay attention to .AddProfileService<ProfileService>(); and services.AddTransient<Microsoft.AspNetCore.Authentication.IClaimsTransformation, MyClaimsTransformation>();
Now at client side go to startup.cs and do as follows:
.AddOpenIdConnect("oidc", options =>
{
//other code
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.Add(new JsonKeyClaimAction(JwtClaimTypes.Role, null, JwtClaimTypes.Role));
})
for my sample i tried to use "Role" and authorize users by my custom roles.
Next at your controller class do like this:
[Authorize(Roles = "myCustomClaimValue")] or you can create a class for custom authorization filter.
Note that you define test user in config file in your identity server project and the user has a custom claim like this new claim(JwtClaimTypes.Role, "myCustomClaimValue") and this will be back at lstUserClaims variable.
I am assuming you are passing Authorization header with Bearer JWT token while calling the API. You can read access_token from HttpContext in your API Controller.
var accessToken = await this.HttpContext.GetTokenAsync("access_token");
var handler = new JwtSecurityTokenHandler();
if (handler.ReadToken(accessToken) is JwtSecurityToken jt && (jsonToken.Claims.FirstOrDefault(claim => claim.Type == "sub") != null))
{
var subID = jt.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
}
NOTE : GetClaimsFromUserInfoEndpoint no need to set explicitly.
Here is a bit of extra info on the subject. By default, IdentityServer doesn't include identity claims in the identity token. It is allowed by setting the AlwaysIncludeUserClaimsInIdToken setting on the client configuration to true. But it is not recommended. The initial identity token is returned from the authorization endpoint via front‑channel communication either through a form post or through the URI. If it's returned via the URI and the token becomes too big, you might hit URI length restrictions, which are still dependent on the browser. Most modern browsers don't have issues with long URIs, but older browsers like Internet Explorer might. This may or may not be of concern to you. Looks like my project is similar to yours. Good luck.

.net core 3.0 Certificate authentication

Recently I have migrated an API from .net core 2.2 to .net core 3.0 in order to implement an authentication using certificates.
I have followed this Microsoft documentations : https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.0 to do my work. But I'm facing an issue :
When I call a controller Method decorated with an [Authorize] attribute, the certificate validation is never performed. If there is no certificate in the request's header, I get a 403 which is the required behavior, but If I put a certificate the supposed behavior should be the verification by the thumbprint, but nothing...
Here my certificate validation service :
public class CertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate, string thumbprintToChek) =>
clientCertificate.Thumbprint.Equals(thumbprintToChek);
}
As said in the documentation, in StartUp.cs I have set up the following lines of code inside the ConfigureServices method
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options => // code from ASP.NET Core sample
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService = context.HttpContext.RequestServices.GetService<CertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate, Configuration.GetValue<string>("Apim:CertificateThumbprint")))
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
else
{
context.Fail("Invalid certificate.");
}
return Task.CompletedTask;
}
};
options.Events = new CertificateAuthenticationEvents
{
OnAuthenticationFailed = context =>
{
context.Fail("Certificate not valid.");
return Task.CompletedTask;
}
};
});
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-ARR-ClientCert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
byte[] bytes = headerValue.ToByteArray();
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
I have also populated the Configure method with the following code :
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
So when I make a request to the API, the CertificateForwarding delegation is well called, but Authentication delegation, never.
However, I have tried to implement this authentication in a brand new project (just for tests) it worked perfectly.
Your assignment to options.Events is done twice. This way the default implementation of OnCertificateValidated overwrites your eventhandler and your CertificateValidationService will never be called.
Combining the two eventhandlers should give the expected result:
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
// Your implementation here.
},
OnAuthenticationFailed = context =>
{
// Your implementation here.
}
};

How to add own claims after logging in to Azure AD with OIDC in ASP.NET Core 2 with dependency injection?

In ASP.NET Core 2 logging in to Azure AD is fairly easy, in ConfigureServices(IServiceCollection services) just add the following
// Azure AD login
services.AddAuthentication(a =>
{
a.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
a.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
a.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(o => o.LoginPath = new PathString("/Account/SignIn"))
.AddOpenIdConnect(o =>
{
o.ClientId = Configuration["Authentication:AzureAd:ClientId"];
o.ClientSecret = Configuration["Authentication:AzureAd:ClientSecret"];
o.Authority = Configuration["Authentication:AzureAd:AADInstance"] +
Configuration["Authentication:AzureAd:TenantId"];
o.CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"];
o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
o.Events = new OpenIdConnectEvents
{
OnRemoteFailure = RemoteFailure,
OnTokenValidated = TokenValidated
};
});
and everything works fine. Then I can add Claims in TokenValidated and that works fine aswell:
private Task TokenValidated(TokenValidatedContext context)
{
var claims = new List<Claim>();
var claim = new Claim(ClaimTypes.Role, "Test", ClaimValueTypes.String, "Issuername")
context.Principal.AddIdentity(new ClaimsIdentity(claims));
return Task.FromResult(0);
}
However, it's never quite that easy. The Claims I want are dependent on a external calls to a service, and the address is stored in the configuration.
In ConfigureServices I also have various classes added for dependency injection with works fine for the controllers.
services.AddTransient<IRoleClaims, RoleClaims>();
This RoleClaims is a class I want to call from the TokenValidated method, but as far as I can see I cannot use DI here. Nor can I access the ServiceCollection to get it via ActivatorUtilities.CreateInstance.
The constructor to RoleClaims looks like this:
public RoleClaims(IOptions<EmployeeConfiguration> configuration)
So, the big question:
How is this supposed to work? Can I somehow use dependency injection in the TokenValidated method? Am I trying to add my own claims in the wrong place?
In ASP.NET Core 2.0, you can get a service from the contain using:
private async Task TokenValidated(TokenValidatedContext context)
{
var widget = ctx.HttpContext.RequestServices.GetRequiredService<Widget>();
...
}
I succeeded in authenticating against IdentityServer4 in a multi-tenancy scenario where I needed to inject client credentials and other stuff on a per-request basis. That's why I also "messed up" my code with custom OpenIdConnectEvents.
The OnTokenValidated func is the right spot. The goal is to assign a value to the TokenValidatedContext.Result (whose setter is unfortunately protected).
You can however call the .Success() method, which sets the property accordingly to what available:
Task TokenValidated(TokenValidatedContext context)
{
//[...] gathering claims
var ci = new ClaimsIdentity(context.Scheme.Name, "name", "role");
ci.AddClaims(my_previously_gathered_Claims);
context.Principal = new ClaimsPrincipal(ci);
// .Success() uses
// 1. the principal just set above
// 2. the context properties
// 3. the context scheme
// to create the underlying ticket
context.Success();
}
That should do the trick.
I personally would have preferred a public setter for .Result.
Found a way to do it. It might not be pretty, but it seems to work.
If anyone have any better way to do it, of if some of this is bad practice I would like to hear it.
public class Startup
{
private IServiceCollection _serviceCollection;
public void ConfigureServices(IServiceCollection services)
{
_serviceCollection = services; // Hacky way to access services in other methods :s
// services.AddStuff() down here, including the AzureAD OIDC
}
private async Task TokenValidated(TokenValidatedContext context)
{
IRoleClaims roleClaims; // My class for reading from services/database
// and create claims
// This is the magic DI workaround I was looking for
var scopeFactory = _serviceCollection.BuildServiceProvider()
.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
var provider = scope.ServiceProvider;
roleClaims = provider.GetRequiredService<IRoleClaims>();
}
// Getting the ObjectID for the user from AzureAD
var objectId = context.SecurityToken.Claims
.Where(o => o.Type == "oid")
.Select(o => o.Value)
.SingleOrDefault();
var claims = await roleClaims.CreateRoleClaimsForUser(objectId);
context.Principal.AddIdentity(new ClaimsIdentity(claims));
}
// Rest of the methods not shown
}

Categories