Adding a custom AuthenticationHandler to a list of external login providers - authentication

I'm using ASP.NET Core 3.1, and I have multiple external login providers configured:
services.AddAuthentication()
.AddDeviantArt(d => {
d.Scope.Add("feed");
d.ClientId = Configuration["Authentication:DeviantArt:ClientId"];
d.ClientSecret = Configuration["Authentication:DeviantArt:ClientSecret"];
d.SaveTokens = true;
})
.AddTwitter(t => {
t.ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"];
t.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
t.SaveTokens = true;
});
I'd like to create a custom AuthenticationProvider that, instead of redirecting to another website, asks for that website's "API key" and treats that as the access token. (The website in question does not support any version of OAuth.)
Before actually hooking it up, I want to test that I can get a custom AuthenticationProvider working at all, so I found one that implements HTTP Basic authentication:
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> {
public CustomAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (!Request.Headers.ContainsKey("Authorization")) {
return AuthenticateResult.NoResult();
}
if (!AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out AuthenticationHeaderValue headerValue)) {
return AuthenticateResult.NoResult();
}
if (!"Basic".Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase)) {
return AuthenticateResult.NoResult();
}
byte[] headerValueBytes = Convert.FromBase64String(headerValue.Parameter);
string userAndPassword = Encoding.UTF8.GetString(headerValueBytes);
string[] parts = userAndPassword.Split(':');
if (parts.Length != 2) {
return AuthenticateResult.Fail("Invalid Basic authentication header");
}
string user = parts[0];
string password = parts[1];
bool isValidUser = true;
if (!isValidUser) {
return AuthenticateResult.Fail("Invalid username or password");
}
var claims = new[] { new Claim(ClaimTypes.Name, user) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties) {
Response.Headers["WWW-Authenticate"] = $"Basic realm=\"Custom realm name here\", charset=\"UTF-8\"";
await base.HandleChallengeAsync(properties);
}
}
I added this to Startup.cs:
.AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>("Test", "Testing", o => { })
The problem is that the HandleAuthenticateAsync method is never called. The other solutions I find to this problem generally say you need to make it the "default" authentication scheme, but I don't want this to interfere with how the other external login providers are set up.
Is there a way I can get this working without changing the default authentication scheme or adding additional attributes to my controllers or actions?

You need add a default Scheme when you add AddAuthentication
like services.AddAuthentication("Test")

Related

Jwt Token audience validation failed when deployed

I am using ASPNET core 5.0 for both front-end and back-end API. It worked perfectly on the local machine, but I deploy both the front-end and API application it always gives me audience validation failure. here is the code I am using.
"Jwt": {
"Issuer": "RestaurantPortal",
"Audience": "http://mansoor0786-001-site1.ctempurl.com/",
"Key": "ASAscethtCVdAQAAAAEAACcQAAAAEDhnGasldjaslkjdleEnGunGWR4Z79AvrtgIjYXhcWZx4OqpvWbsdsdsdSafcV/ZuPw25KbhKWhg1SIXXU2Ad7maaGAk******"
},
I have kept this in appSettings of both front end and API applications. Here is API startup code
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build();
});
services.AddAuthentication()
.AddCookie()
.AddJwtBearer(config =>
{
config.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = JwtConfiguration.JWTIssuer,
ValidAudience = JwtConfiguration.JWTAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey)),
ClockSkew = TimeSpan.Zero
};
});
Here is the validation I am doing on API end when user wants to login.
public bool ValidateToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = JwtConfiguration.JWTIssuer,
ValidAudience = JwtConfiguration.JWTAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey)),
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
}
catch (Exception)
{
return false;
}
return true;
}
Locally it works fine but when deploy these both applications it gives me an error and when I try to login it doesn't allow me to login into system. Here are the URL for both API and front-end application. This where I generate token
public string GenerateAccessToken(IEnumerable<Claim> claims)
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken(
issuer: JwtConfiguration.JWTIssuer,
audience: JwtConfiguration.JWTAudience,
claims: claims,
expires: DateTime.Now.AddHours(24),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return tokenString;
}
In this the configuration gets information from appSettings.json
public static class JwtConfiguration
{
public static readonly string JWTIssuer = Utils._config["Jwt:Issuer"];
public static readonly string JWTAudience = Utils._config["Jwt:Audience"];
public static readonly string JWTKey = Utils._config["Jwt:Key"];
}
This is my response from when I log in the user
if (apiResponseModel != null && apiResponseModel.Data != null && apiResponseModel.Data.Status == 1)
{
var claims = new List<Claim>
{
new Claim(AuthKeys.AccessToken, apiResponseModel.Data.AccessToken),
new Claim(AuthKeys.RefreshToken, apiResponseModel.Data.RefreshToken)
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
if (apiResponseModel.Data.RoleName == UserRole.Roles.Customer.GetEnumDescription())
{
return RedirectToAction("index", "Home");
}
return RedirectToAction("index", "dashboard");
}
After that it redirected to dashboard index page where I wrote base controller and added attribute on top of basecontroller which does the following.
[ServiceFilter(typeof(JWT_Authentication))]
public class BaseController : Controller
{
public readonly IOptions<AppSettingDTO> _appSetting;
protected readonly IUserProfileInfo _userService;
public readonly IHttpContextAccessor _httpContextAccessor;
protected readonly IHttpNetClientService _apiService;
public BaseController(IOptions<AppSettingDTO> AppSetting, IHttpNetClientService HttpService, IUserProfileInfo UserInfo, IHttpContextAccessor HttpContext)
{
_appSetting = AppSetting;
_apiService = HttpService;
_userService = UserInfo;
_httpContextAccessor = HttpContext;
}
}
Here is my JWT_Authentication
public class JWT_Authentication : ActionFilterAttribute
{
private readonly IHttpContextAccessor _httpContextAccessor;
protected readonly IUserProfileInfo _userService;
public JWT_Authentication(IHttpContextAccessor HttpContext, IUserProfileInfo UserInfo)
{
_httpContextAccessor = HttpContext;
_userService = UserInfo;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
string actionName = context.RouteData.Values["Action"].ToString().ToLower();
string controllerName = context.RouteData.Values["Controller"].ToString().ToLower();
if (
controllerName != "account" && actionName != "logout")
{
string accessTokens = _userService.GetToken(_httpContextAccessor);
if (!_userService.ValidateToken(accessTokens))
{
}
else
{
return;
}
context.Result = new RedirectToRouteResult(new RouteValueDictionary(){
{ "action", "LogOut" },
{ "controller", "Account" }
});
return;
}
}
}
API
http://mansoor00786-001-site1.gtempurl.com/
Front-End
http://mansoor0786-001-site1.ctempurl.com/
I am calling login API from the front-end application which is also in asp net core 5.0 but it doesn't log me into the dashboard because of validation failure and that is because of the audience.
Well, as far as i saw it, here is some points I spot
There won't ever be an exception was throw when calling ValidateToken
Cause we put it on try catch block, so where does it throw audience validation failure ? It cannot be during deployment cause catch block doesn't have logging support anywhere, therefore, it might just be your assumption. And behavior on production state should be always redirect to Login page after logout.
The way MVC project handle Jwt Token was cumbersome
As we handmade Jwt Token and validate them ourself, such thing as validation failure with the same setting (Issuer, audience,...) should not exists. If that was fine on the client, have faith and logging those setting out from production state.
And for current approach, We can validate Jwt token and restrict them from access our resource fine, but HttpContext.User object still be null, therefore Authorization process became mostly, unusable.
Instead, how about consider to write our own Authentication scheme ?
What might be the problem here ?
public class JWT_Authentication : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//... Some upper process
if (!_userService.ValidateToken(accessTokens))
{
// Doing something if the jwt invalid ?
}
else
{
return;
}
//... Some below process
}
}
If my block code idea was right, take a look at string accessTokens = _userService.GetToken(_httpContextAccessor);, log it out, as there might be a null here, due to you passing down a IHttpContextAccessor, which was singleton, not a HttpContext which scope for each request (localhost would be fine, cause we have only one client).

Blazor wasm get additional information and add to user claims

I am using identityserver4 for authentication and it's laid out something like this: identity server4 -> Web Api -> Blazor WASM Client(Standalone). everything is getting authenticated and working great. I get the authenticated user claims all the way to the wasm client.
I am now trying to add more claims which come directly from the database. I could have added the claims to the identityserver token but the token gets too big (> 2kb) and then identityserver stops working. apparently this is a known issue.
So iwant to build authorization and trying to keep the jwt token from identityserver small.
in the program.cs file i have a http client like so
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddOidcAuthentication(options =>// Configure your authentication provider options here.
// For more information, see https://aka.ms/blazor-standalone-auth
//builder.Configuration.Bind("Local", options.ProviderOptions);
... provider options
options.ProviderOptions.ResponseType = "code";
options.UserOptions.RoleClaim = "role";
}).AddAccountClaimsPrincipalFactory<CustomAccountClaimsPrincipalFactory>();
await builder.Build().RunAsync();
in the file CustomAccountClaimsPrincipalFactory i have this
public class CustomAccountClaimsPrincipalFactory
: AccountClaimsPrincipalFactory<RemoteUserAccount>
{
private const string Planet = "planet";
[Inject]
public HttpClient Http { get; set; }
public CustomAccountClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor)
: base(accessor) {
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
if (user.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)user.Identity;
var claims = identity.Claims.Where(a => a.Type == Planet);
if (!claims.Any())
{
identity.AddClaim(new Claim(Planet, "mars"));
}
//get user roles
//var url = $"/Identity/users/112b7de8-614f-40dc-a9e2-fa6e9d2bf85a/roles";
var dResources = await Http.GetFromJsonAsync<List<somemodel>>("/endpoint");
foreach (var item in dResources)
{
identity.AddClaim(new Claim(item.Name, item.DisplayName));
}
}
return user;
}
}
this is not working as the httpclient is not biolt when this is called and the http client uses the same builder which is building the base http client.
How do i get this to work?
You can create a IProfileService and customise it however you need:
var builder = services.AddIdentityServer(options =>
...
.AddProfileService<IdentityProfileService>();
public class IdentityProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
private readonly UserManager<ApplicationUser> _userManager;
public IdentityProfileService(IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory, UserManager<ApplicationUser> userManager)
{
_claimsFactory = claimsFactory;
_userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
if (user == null)
{
throw new ArgumentException("");
}
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
//Add more claims like this
//claims.Add(new System.Security.Claims.Claim("MyProfileID", user.Id));
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
context.IsActive = user != null;
}
}
Keep the access token small and only include the necessary claims to get past the JwtBearer authentication step.
Then in the API that receives an access token, you can simply create an authorization policy that do lookup the users additional claims and evaluate if he have access or not.
You can do that in the simple policy definitions or the more advanced authorization handlers like the code below:
public class CheckIfAccountantHandler : AuthorizationHandler<CanViewReportsRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
CanViewReportsRequirement requirement)
{
bool result = CallTheCheckIfAccountantService();
if(result)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
A sample requirement can be defined as:
public class CanViewReportsRequirement : IAuthorizationRequirement
{
public int StartHour { get; }
public int EndHour { get; }
public CanViewReportsRequirement(int startHour, int endHour)
{
StartHour = startHour;
EndHour = endHour;
}
}
The important thing is to keep the complexity of the application low and not try to make it harder than it has to be. Just to make the system easy to reason about!

Issue with custom Authentication filter asp.net core

I'm trying to create a custom authentication filter in ASP.NET Core. Need to use it in the controller to authenticate the JWT provided to me and create a Claim Principal. However when I place the authentication tag above the controller, nothing happens and the controller is getting processed without the authentication.
The following are the steps which were done:
Added the app.UseAuthentication() in the startup.cs under
Configure(IApplicationBuilder app, IHostingEnvironment env)
{
......
......
app.UseAuthentication();
}
Created a new class file ProcessAuth in the same project, containing the AuthenticationAsync and ChallengeAsync
public class ProcessAuth : Attribute, IAuthenticationFilter
{
public bool AllowMultiple { get { return false; } }
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// More code to be added for validating the JWT
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
throw new NotImplementedException(); //sample code
}
}
Addded a reference of this new file in the controller
Placed the tag [ProcessAuth] at the top of the controller
[ProcessAuth]
[Route("api/[controller]")]
[ApiController]
Used Postman to send a JSON data, along with the Authorization Header containing a valid JWT token as "Bearer "
The code just ignores the filter and processes code in the controller and returns the result
More info:
If I add [Authorize] to the controller, Postman just returns a 401 Unauthorized error
Also checked this URL, but couldn't find the issue.
Update: I checked the answers to the similar Stack Overflow questions and followed the same but still the issue remains the same.
Nuget Packages installed:
Microsoft.AspNet.WebApi.Core and also Microsoft.AspNet.WebApi
Namespaces used:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;
using System.Web.Http.Controllers;
Similar Issue - Link
How do I get this to work? Am I missing something?
According to your codes, I found you have used asp.net authentication filter in asp.net core application. This is will not work.
In asp.net core, we should use JWT bear authentication middleware to achieve your requirement.
You could create custom OnChallenge to validate the jwt token and OnTokenValidated to add the claims.
More details, you could refer to below codes:
services.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(token =>
{
token.RequireHttpsMetadata = false;
token.SaveToken = true;
token.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
//Same Secret key will be used while creating the token
IssuerSigningKey = new SymmetricSecurityKey(SecretKey),
ValidateIssuer = true,
//Usually, this is your application base URL
ValidIssuer = "http://localhost:45092/",
ValidateAudience = true,
//Here, we are creating and using JWT within the same application.
//In this case, base URL is fine.
//If the JWT is created using a web service, then this would be the consumer URL.
ValidAudience = "http://localhost:45092/",
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
token.Events = new JwtBearerEvents {
OnChallenge = async ctx => {
},
OnTokenValidated = async ctx =>
{
//Get the calling app client id that came from the token produced by Azure AD
string clientId = ctx.Principal.FindFirstValue("appid");
//Get EF context
//var db = ctx.HttpContext.RequestServices.GetRequiredService<AuthorizationDbContext>();
//Check if this app can read confidential items
bool canReadConfidentialItems = await db.Applications.AnyAsync(a => a.ClientId == clientId && a.ReadConfidentialItems);
if (canReadConfidentialItems)
{
//Add claim if yes
var claims = new List<Claim>
{
new Claim("ConfidentialAccess", "true")
};
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
}
}
};
});
Edit:
You could create the AuthenticationHandler and AuthenticationSchemeOptions class like below and register the class in the startup.cs. Then you could use [Authorize(AuthenticationSchemes = "Test")] to set the special AuthenticationSchemes.
More details, you could refer to below codes sample:
public class ValidateHashAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
public class ValidateHashAuthenticationHandler
: AuthenticationHandler<ValidateHashAuthenticationSchemeOptions>
{
public ValidateHashAuthenticationHandler(
IOptionsMonitor<ValidateHashAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
//TokenModel model;
// validation comes in here
if (!Request.Headers.ContainsKey("X-Base-Token"))
{
return Task.FromResult(AuthenticateResult.Fail("Header Not Found."));
}
var token = Request.Headers["X-Base-Token"].ToString();
try
{
// convert the input string into byte stream
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(token)))
{
// deserialize stream into token model object
//model = Serializer.Deserialize<TokenModel>(stream);
}
}
catch (System.Exception ex)
{
Console.WriteLine("Exception Occured while Deserializing: " + ex);
return Task.FromResult(AuthenticateResult.Fail("TokenParseException"));
}
//if (model != null)
//{
// // success case AuthenticationTicket generation
// // happens from here
// // create claims array from the model
// var claims = new[] {
// new Claim(ClaimTypes.NameIdentifier, model.UserId.ToString()),
// new Claim(ClaimTypes.Email, model.EmailAddress),
// new Claim(ClaimTypes.Name, model.Name) };
// // generate claimsIdentity on the name of the class
// var claimsIdentity = new ClaimsIdentity(claims,
// nameof(ValidateHashAuthenticationHandler));
// // generate AuthenticationTicket from the Identity
// // and current authentication scheme
// var ticket = new AuthenticationTicket(
// new ClaimsPrincipal(claimsIdentity), this.Scheme.Name);
// // pass on the ticket to the middleware
// return Task.FromResult(AuthenticateResult.Success(ticket));
//}
return Task.FromResult(AuthenticateResult.Fail("Model is Empty"));
}
}
public class TokenModel
{
public int UserId { get; set; }
public string Name { get; set; }
public string EmailAddress { get; set; }
}
Startup.cs add below codes into the ConfigureServices method:
services.AddAuthentication(options =>
{
options.DefaultScheme
= "Test";
})
.AddScheme<ValidateHashAuthenticationSchemeOptions, ValidateHashAuthenticationHandler>
("Test", null);
Controller:
[Authorize(AuthenticationSchemes = "Test")]

AddJwtBearer in multi-tenant application

I need to implement multi-tenant REST API on asp.net core and use the Jwt Web token for authentication.
Asp.net core docs suggest using the following code in Startup.cs ConfigureServices method:
services.AddAuthentication().AddJwtBearer("Bearer", options =>
{
options.Audience = "MyAudience";
options.Authority = "https://myauhorityserver.com";
}
The issue is that my REST API application is multi-tenant. The tenant is discovered from the URL, e.g.
https://apple.myapi.com,
https://samsung.myapi.com,
https://google.myapi.com
So each of such URLs will eventually point to the same IP, but based on the first word in the URL the app discovers tenant on using the appropriate DB connection.
Each such tenant has its own Authority URL. We use Keycloak as an identity management server, so each tenant on it has its own REALM.
So the Authority URL per tenant is something like that:
https://mykeycloack.com/auth/realms/11111111,
https://mykeycloack.com/auth/realms/22222222,
https://mykeycloack.com/auth/realms/33333333
The API application should be able to add and remove tenants dynamically, without restarting the application so, setting all the tenants in the application startup is not a good idea.
I was trying to add more schemas with more calls to AddJwtBearer, however, all the calls go to the schema "Bearer", according to options.Events.OnAuthenticationFailed event. It's not clear how to make other schemas to handle calls with Bearer token in HTTP header. Even though if it's somehow possible with a help of custom middle-ware, as I mention before providing tenant-specific configuration for Bearer token authentication in the app startup is not a solution as new tenants need to be added dynamically.
Additional info:
According to the fiddler, the Authority URL is finally combined with
/.well-known/openid-configuration
and called when the first request comes to the API endpoint marked with
[Authorize]
If the to configuration fails, the API call fails too. If the call to configuration is successful, the application is not calling it again on the next API requests.
I have found a solution and made it a Nuget Package for reusability you can take a look.
Pretty much I replace the JwtBearerTokenHandler by a DynamicBearerTokenHandler, it gives the flexibility, to resolve OpenIdConnectOptions at runtime.
I've also made another package that cares care of that for you, the only thing you need is to resolve the authority for the ongoing request, you receive the HttpContext.
Here is the package:
https://github.com/PoweredSoft/DynamicJwtBearer
How to use it.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddMemoryCache();
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddDynamicJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.ValidateAudience = false;
})
.AddDynamicAuthorityJwtBearerResolver<ResolveAuthorityService>();
services.AddControllers();
}
}
The service
internal class ResolveAuthorityService : IDynamicJwtBearerAuthorityResolver
{
private readonly IConfiguration configuration;
public ResolveAuthorityService(IConfiguration configuration)
{
this.configuration = configuration;
}
public TimeSpan ExpirationOfConfiguration => TimeSpan.FromHours(1);
public Task<string> ResolveAuthority(HttpContext httpContext)
{
var realm = httpContext.Request.Headers["X-Tenant"].FirstOrDefault() ?? configuration["KeyCloak:MasterRealm"];
var authority = $"{configuration["KeyCloak:Endpoint"]}/realms/{realm}";
return Task.FromResult(authority);
}
}
Whats different from the original one
// before
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await
Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
// after
var currentConfiguration = await this.dynamicJwtBearerHanderConfigurationResolver.ResolveCurrentOpenIdConfiguration(Context);
How its replaced
public static AuthenticationBuilder AddDynamicJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action<JwtBearerOptions> action = null)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
if (action != null)
return builder.AddScheme<JwtBearerOptions, DynamicJwtBearerHandler>(authenticationScheme, null, action);
return builder.AddScheme<JwtBearerOptions, DynamicJwtBearerHandler>(authenticationScheme, null, _ => { });
}
Source of the Handler
public class DynamicJwtBearerHandler : JwtBearerHandler
{
private readonly IDynamicJwtBearerHanderConfigurationResolver dynamicJwtBearerHanderConfigurationResolver;
public DynamicJwtBearerHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IDynamicJwtBearerHanderConfigurationResolver dynamicJwtBearerHanderConfigurationResolver) : base(options, logger, encoder, clock)
{
this.dynamicJwtBearerHanderConfigurationResolver = dynamicJwtBearerHanderConfigurationResolver;
}
/// <summary>
/// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using <see cref="TokenValidationParameters"/> set in the options.
/// </summary>
/// <returns></returns>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string token = null;
try
{
// Give application opportunity to find from a different location, adjust, or reject token
var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);
// event can set the token
await Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.Result != null)
{
return messageReceivedContext.Result;
}
// If application retrieved token from somewhere else, use that.
token = messageReceivedContext.Token;
if (string.IsNullOrEmpty(token))
{
string authorization = Request.Headers[HeaderNames.Authorization];
// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}
// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
}
var currentConfiguration = await this.dynamicJwtBearerHanderConfigurationResolver.ResolveCurrentOpenIdConfiguration(Context);
var validationParameters = Options.TokenValidationParameters.Clone();
if (currentConfiguration != null)
{
var issuers = new[] { currentConfiguration.Issuer };
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;
validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(currentConfiguration.SigningKeys)
?? currentConfiguration.SigningKeys;
}
List<Exception> validationFailures = null;
SecurityToken validatedToken;
foreach (var validator in Options.SecurityTokenValidators)
{
if (validator.CanReadToken(token))
{
ClaimsPrincipal principal;
try
{
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
}
catch (Exception ex)
{
Logger.TokenValidationFailed(ex);
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
&& ex is SecurityTokenSignatureKeyNotFoundException)
{
Options.ConfigurationManager.RequestRefresh();
}
if (validationFailures == null)
{
validationFailures = new List<Exception>(1);
}
validationFailures.Add(ex);
continue;
}
Logger.TokenValidationSucceeded();
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
{
Principal = principal,
SecurityToken = validatedToken
};
await Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
{
return tokenValidatedContext.Result;
}
if (Options.SaveToken)
{
tokenValidatedContext.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = "access_token", Value = token }
});
}
tokenValidatedContext.Success();
return tokenValidatedContext.Result;
}
}
if (validationFailures != null)
{
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
return AuthenticateResult.Fail(authenticationFailedContext.Exception);
}
return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
}
catch (Exception ex)
{
Logger.ErrorProcessingMessage(ex);
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = ex
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
throw;
}
}
}
If you're still trying to figure this out you can try Finbuckle.
https://www.finbuckle.com/MultiTenant/Docs/Authentication

Razor Pages .NET Core 2.1 Integration Testing post authentication

I am looking for some guidance...
I'm currently looking at trying to write some integration tests for a Razor Pages app in .net core 2.1, the pages I'm wanting to test are post authentication but I'm not sure about the best way of approaching it. The docs seem to suggest creating a CustomWebApplicationFactory, but apart from that I've got a little bit lost as how I can fake/mock an authenticated user/request, using basic cookie based authentication.
I've seen that there is an open GitHub issue against the Microsoft docs (here is the actual GitHub issue), there was a mentioned solution using IdentityServer4 but I’m just looking how to do this just using cookie based authentication.
Has anyone got any guidance they may be able to suggest?
Thanks in advance
My Code so far is:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseMySql(connectionString);
options.EnableSensitiveDataLogging();
});
services.AddLogging(builder =>
{
builder.AddSeq();
});
services.ConfigureAuthentication();
services.ConfigureRouting();
}
}
ConfigureAuthentication.cs
namespace MyCarparks.Configuration.Startup
{
public static partial class ConfigurationExtensions
{
public static IServiceCollection ConfigureAuthentication(this IServiceCollection services)
{
services.AddIdentity<MyCarparksUser, IdentityRole>(cfg =>
{
//cfg.SignIn.RequireConfirmedEmail = true;
})
.AddDefaultUI()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = $"/Identity/Account/Login";
options.LogoutPath = $"/Identity/Account/Logout";
options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options =>
{
options.AllowAreas = true;
options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout");
options.Conventions.AuthorizeFolder("/Sites");
});
return services;
}
}
}
Integration Tests
PageTests.cs
namespace MyCarparks.Web.IntegrationTests
{
public class PageTests : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly CustomWebApplicationFactory<Startup> factory;
public PageTests(CustomWebApplicationFactory<Startup> webApplicationFactory)
{
factory = webApplicationFactory;
}
[Fact]
public async Task SitesReturnsSuccessAndCorrectContentTypeAndSummary()
{
var siteId = Guid.NewGuid();
var site = new Site { Id = siteId, Address = "Test site address" };
var mockSite = new Mock<ISitesRepository>();
mockSite.Setup(s => s.GetSiteById(It.IsAny<Guid>())).ReturnsAsync(site);
// Arrange
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("http://localhost:44318/sites/sitedetails?siteId=" + siteId);
// Assert
response.EnsureSuccessStatusCode();
response.Content.Headers.ContentType.ToString()
.Should().Be("text/html; charset=utf-8");
var responseString = await response.Content.ReadAsStringAsync();
responseString.Should().Contain("Site Details - MyCarparks");
}
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseStartup<Startup>();
}
}
}
For implement your requirement, you could try code below which creates the client with the authentication cookies.
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
});
base.ConfigureWebHost(builder);
}
public new HttpClient CreateClient()
{
var cookieContainer = new CookieContainer();
var uri = new Uri("https://localhost:44344/Identity/Account/Login");
var httpClientHandler = new HttpClientHandler
{
CookieContainer = cookieContainer
};
HttpClient httpClient = new HttpClient(httpClientHandler);
var verificationToken = GetVerificationToken(httpClient, "https://localhost:44344/Identity/Account/Login");
var contentToSend = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("Email", "test#outlook.com"),
new KeyValuePair<string, string>("Password", "1qaz#WSX"),
new KeyValuePair<string, string>("__RequestVerificationToken", verificationToken),
});
var response = httpClient.PostAsync("https://localhost:44344/Identity/Account/Login", contentToSend).Result;
var cookies = cookieContainer.GetCookies(new Uri("https://localhost:44344/Identity/Account/Login"));
cookieContainer.Add(cookies);
var client = new HttpClient(httpClientHandler);
return client;
}
private string GetVerificationToken(HttpClient client, string url)
{
HttpResponseMessage response = client.GetAsync(url).Result;
var verificationToken =response.Content.ReadAsStringAsync().Result;
if (verificationToken != null && verificationToken.Length > 0)
{
verificationToken = verificationToken.Substring(verificationToken.IndexOf("__RequestVerificationToken"));
verificationToken = verificationToken.Substring(verificationToken.IndexOf("value=\"") + 7);
verificationToken = verificationToken.Substring(0, verificationToken.IndexOf("\""));
}
return verificationToken;
}
}
Following Chris Pratt suggestion, but for Razor Pages .NET Core 3.1 I use a previous request to authenticate against login endpoint (which is also another razor page), and grab the cookie from the response. Then I add the same cookie as part of the http request, and voila, it's an authenticated request.
This is a piece of code that uses an HttpClient and AngleSharp, as the official microsoft documentation, to test a razor page. So I reuse it to grab the cookie from the response.
private async Task<string> GetAuthenticationCookie()
{
var formName = nameof(LoginModel.LoginForm); //this is the bounded model for the login page
var dto =
new Dictionary<string, string>
{
[$"{formName}.Username"] = "foo",
[$"{formName}.Password"] = "bar",
};
var page = HttpClient.GetAsync("/login").GetAwaiter().GetResult();
var content = HtmlHelpers.GetDocumentAsync(page).GetAwaiter().GetResult();
//this is the AndleSharp
var authResult =
await HttpClient
.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='login-form']"),
(IHtmlButtonElement)content.QuerySelector("form[id='login-form']")
.QuerySelector("button"),
dto);
_ = authResult.Headers.TryGetValues("Set-Cookie", out var values);
return values.First();
}
Then that value can be reused and passed with the new http request.
//in my test, the cookie is a given (from the given-when-then approach) pre-requirement
protected void Given()
{
var cookie = GetAuthenticationCookie().GetAwaiter().GetResult();
//The http client is a property that comes from the TestServer, when creating a client http for tests as usual. Only this time I set the auth cookie to it
HttpClient.DefaultRequestHeaders.Add("Set-Cookie", cookie);
var page = await HttpClient.GetAsync($"/admin/protectedPage");
//this will be a 200 OK because it's an authenticated request with whatever claims and identity the /login page applied
}