Is it possible with an Owin Middleware implementation to add claims prior to the execution of a Web API controller?
Created an OwinMiddleware implementation and added an identity:
var id = new ClaimsIdentity();
id.AddClaim(new Claim("Whatever", "is possible"));
context.Authentication.User.AddIdentity(id);
await Next.Invoke(context);
However, even this Invoke method call the identities are not updated (just the internal claims array). And the controller when executed of course never gets the new dummy claim.
Ideas?
There's already a class that can provide claims enrichment ClaimsAuthenticationManager, which you can extend so it handles your domain-specific claims, for example...
public class MyClaimsAuthenticationManager : ClaimsAuthenticationManager
{
public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated)
{
return base.Authenticate(resourceName, incomingPrincipal);
}
return AddApplicationClaims(incomingPrincipal);
}
private ClaimsPrincipal AddApplicationClaims(ClaimsPrincipal principal)
{
// TODO: Add custom claims here based on current principal.
return principal;
}
}
Next task is to provide appropriate middleware to invoke this. For my projects I've written the following classes...
/// <summary>
/// Middleware component to apply claims transformation to current context
/// </summary>
public class ClaimsTransformationMiddleware
{
private readonly Func<IDictionary<string, object>, Task> next;
private readonly IServiceProvider serviceProvider;
public ClaimsTransformationMiddleware(Func<IDictionary<string, object>, Task> next, IServiceProvider serviceProvider)
{
this.next = next;
this.serviceProvider = serviceProvider;
}
public async Task Invoke(IDictionary<string, object> env)
{
// Use Katana's OWIN abstractions
var context = new OwinContext(env);
if (context.Authentication != null && context.Authentication.User != null)
{
var manager = serviceProvider.GetService<ClaimsAuthenticationManager>();
context.Authentication.User = manager.Authenticate(context.Request.Uri.AbsoluteUri, context.Authentication.User);
}
await next(env);
}
}
And then a wiring extension...
public static class AppBuilderExtensions
{
/// <summary>
/// Add claims transformation using <see cref="ClaimsTransformationMiddleware" /> any depdendency resolution is done via IoC
/// </summary>
/// <param name="app"></param>
/// <param name="serviceProvider"></param>
/// <returns></returns>
public static IAppBuilder UseClaimsTransformation(this IAppBuilder app, IServiceProvider serviceProvider)
{
app.Use<ClaimsTransformationMiddleware>(serviceProvider);
return app;
}
}
I know this is service locator anti-pattern but using IServiceProvider is container neutral and seems to be the accepted way of putting dependencies into Owin middleware.
Last you need to wire this up in your Startup, example below presumes Unity and registering/exposing a IServiceLocator property...
// Owin config
app.UseClaimsTransformation(UnityConfig.ServiceLocator);
You may find useful inheriting from Authorizate Attribute and extending it to meet your requirements:
public class DemoAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext){
if (Authorize(actionContext)){
return;
}
HandleUnauthorizedRequest(actionContext);
}
protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext){
var challengeMessage = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized;
//Adding your code here
var id = new ClaimsIdentity();
id.AddClaim(new Claim("Whatever", "is possible"));
context.Authentication.User.AddIdentity(id);
challengeMessage.Headers.Add("WWW-Authenticate", "Basic");
throw new HttpResponseException(challengeMessage);
}
private bool Authorize(System.Web.Http.Controllers.HttpActionContext actionContext){
try{
var someCode = (from h in actionContext.Request.Headers where h.Key == "demo" select h.Value.First()).FirstOrDefault();
// or check for the claims identity property.
return someCode == "myCode";
}
catch (Exception){
return false;
}
}
}
And in your controller:
[DemoAuthorize]
public class ValuesController : ApiController{
Here is a link on other custom implemenation for WebApi Authorizations:
http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-membership-provider/
This is how I ended up adding a new claim in owin middleware, based on the OP's comment about hooking into UseOAuthBearerAuthentication. It uses IdentityServer3.AccessTokenValidation, which calls UseOAuthBearerAuthentication internally and passes the OAuthBearerAuthenticationProvider through to it.
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityServer3.AccessTokenValidation;
using Owin;
using Microsoft.Owin.Security.OAuth;
//...
public void Configuration(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://127.0.0.1/identityserver",
TokenProvider = new OAuthBearerAuthenticationProvider
{
OnValidateIdentity = AddClaim
}
});
}
private Task AddClaim(OAuthValidateIdentityContext context)
{
context.Ticket.Identity.AddClaim(new Claim("test", "123"));
return Task.CompletedTask;
}
Related
I have a piece of middleware I'm trying to build that will check if a user has a particular key and if they can receive a authentication token and if so impersonate a windows user. That particular portion we were able to get to run. However I'm having trouble passing IConfiguration to said middleware. Whenever I run the application I get the following error:
A suitable constructor for type LocalDevelopmentImpersonation.LocalDevelopmentImpersonation' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
namespace LocalDevelopmentImpersonation
{
public sealed class LocalDevelopmentImpersonation : IMiddleware
{
private readonly RequestDelegate _Next;
private readonly SafeAccessTokenHandle _SafeAccessTokenHandle;
private readonly bool _IsLocal;
public LocalDevelopmentImpersonation(RequestDelegate next, IConfiguration configuration)
{
this._SafeAccessTokenHandle = LocalDevelopmentImpersonationSetup.GetSafeAccessTokenHandle(out var receivedSafeAccessTokenSuccessfully);
this._Next = next;
this._IsLocal = LocalDevelopmentImpersonationSetup.IsLocal(receivedSafeAccessTokenSuccessfully, configuration);
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (this._IsLocal)
{
await WindowsIdentity.RunImpersonated(this._SafeAccessTokenHandle, async () =>
{
await this._Next.Invoke(context);
});
}
await this._Next.Invoke(context);
}
}
Methods to use in Program.cs on the instance we created of WebApplication.
namespace LocalDevelopmentImpersonation.ExtensionMethods
{
public static class LocalDevelopmentImpersonationExtensions
{
public static IApplicationBuilder UseLocalDevelopmentImpersonation(this IApplicationBuilder app)
{
return app.UseMiddleware<LocalDevelopmentImpersonation>();
}
public static IApplicationBuilder UseLocalDevelopmentImpersonation(this IApplicationBuilder app, IConfiguration configuration)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
return app.UseMiddleware<LocalDevelopmentImpersonation>(Options.Create(configuration));
}
}
}
Calling instance
var app = builder.Build();
app.UseLocalDevelopmentImpersonation(configuration); //Instance of ConfigurationManager that implements IConfiguration
It isn't possible to pass objects to the factory-activated middleware with UseMiddleware,If you do want to pass argumentss to your middleware,try with Middleware activated by convention.
You could check the doc for more details
Our system is role based and makes a lot of use of the Authorize attribute
In our App Services we use the code below to set this up:
public void ConfigureAuth(IApplicationBuilder app)
{
if (Configuration.GetValue<bool>("UseLoadTest"))
{
app.UseMiddleware<ByPassAuthMiddleware>();
}
app.UseAuthentication();
}
How can I do this with an Azure function Http Trigger?
There is no UseAuthentication() method on IFunctionsHostBuilder.
public override void Configure(IFunctionsHostBuilder builder)
{
}
I'm using .NET Core 3.1.
AFAIK Functions doesn't have such an attribute currently. This issue tracks the work going in to support Filters for Functions but this is still in preview.
Based on the above preview feature, looks like this is one library - dark-loop/functions-authorize - that adds support for this.
I started using Middleware for this: Took inspiration from this article:
https://jinalkumarpatel.hashnode.dev/azure-functions-middleware-part-2-authentication-middleware
Create the middleware:
public class BearerAuthenticationMiddleware
: IFunctionsWorkerMiddleware
{
private readonly ILogger<BearerAuthenticationMiddleware> logger;
public BearerAuthenticationMiddleware(ILogger<BearerAuthenticationMiddleware> logger)
{
this.logger = logger;
}
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
if (context.IsHttpTriggerFunction())
{
var headers = context.BindingContext.BindingData["Headers"]?.ToString();
var httpHeaders = System.Text.Json.JsonSerializer.Deserialize<HttpHeaders>(headers);
if (httpHeaders?.Authorization != null &&
httpHeaders.Authorization.StartsWith("Bearer"))
{
//Validation logic for your token. Here If Bearer present I consider as Valid.
if (httpHeaders.Authorization.Contains("admin"))
{
// Originally based on token get user role.
// Put into context.Items so it will be used by next middleware.
context.Items.Add("UserRole", "Admin");
}
await next(context);
}
else
{
await context.CreateJsonResponse(System.Net.HttpStatusCode.Unauthorized, new { Message = "Token is not valid." });
}
}
else
{
await next(context);
}
}
}
Register the middleware:
public class Program
{
public static void Main()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults(configure=>
{
// other middleware also configure following way.
// It will execute in same order it is configured over here.
configure.UseMiddleware<SimpleMiddleware>();
})
.Build();
host.Run();
}
}
I want to authorize users to see only their own resources (e.g: Audits entity). So in the AuditController I have:
[MyAuthorize(Policy = nameof(ValidUserToSeeAuditAuthorizationHandler))]
[HttpGet]
public async Task<JsonResult<AuditView>> GetByIdAsync(Guid id)
{
// my business to fetch the audit info based by its id
// ...
return result;
}
Then I created my Requirement and AuthorizationHandler classes:
public class ValidUserToSeeAuditRequirment : IAuthorizationRequirement
{
public ValidUserToSeeAuditRequirment(Guid auditId)
{
auditId = auditId;
}
public Guid AuditId { get; }
}
public class ValidUserToSeeAuditAuthorizationHandler : AuthorizationHandler<ValidUserToSeeAuditRequirment>
{
private readonly AppUserManager _userManager;
private readonly IUnitOfWork _appDbContext;
public ValidUserToSeeAuditAuthorizationHandler(AppUserManager userManager, IUnitOfWork appDbContext)
{
_userManager = userManager;
_appDbContext = appDbContext;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidUserToSeeAuditRequirment requirement)
{
if (!context.User.IsAuthenticated())
{
context.Fail();
return Task.CompletedTask;
}
var theAudit = _appDbContext.Set<Audit>().SingleOrDefault(x => x.Id == requirement.AuditId);
var authenticatedUserId = Convert.ToInt32(context.User.GetSubjectId());
// If the authenticated user created the audit, then he/she is valid to see it
if (theAudit.SubjectauthenticatedUserId == authenticatedUserId)
{
// valid
context.Succeed(requirement);
return Task.CompletedTask;
}
// he/she is not authorized to see the resource (audit)
context.Fail();
return Task.CompletedTask;
}
}
But in the Startup class I want to configure authorization policies. How do I configure my Requirement class to get the user input parameters from the controller's action method?
services.AddAuthorization(options =>
{
// another policies
// ...
options.AddPolicy(name: nameof(ValidUserToSeeAuditAuthorizationHandler),
policy =>
{
policy.RequireAuthenticatedUser();
policy.AddRequirements(new ValidUserToSeeAuditRequirment( /****** HERE, how to pass the controller action method parameters ******/));
});
});
services.AddTransient<IAuthorizationHandler, ValidUserToSeeAuditAuthorizationHandler>();
You can customize a AuthorizationPolicy provider to get the parameter.
public class CustomAuthorizepolicyProvider: DefaultAuthorizationPolicyProvider
{
public CustomAuthorizepolicyProvider(IOptions<AuthorizationOptions> options):base(options)
{
}
public override Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (policyName=="[specified plicyname]")
{
var authorizePolicy = new AuthorizationPolicyBuilder();
authorizePolicy.AddRequirements(new ValidUserToSeeAuditRequirment(/* give the parameter*/)).Build();
return Task.FromResult(authorizePolicy);
}
return base.GetPolicyAsync(policyName);
}
}
Inject into startup. Note: it is a singleton.
services.AddSingleton<IAuthorizationPolicyProvider,CustomAuthorizepolicyProvider>();
I ended up with this solution:
/// <summary>
///
/// </summary>
public class ValidUserToSeeAuditRequirment : IAuthorizationRequirement
{
}
/// <summary>
/// Only an Admin and the authorized user can see the Audit
/// </summary>
public class ValidUserToSeeAuditAuthorizationHandler : AuthorizationHandler<ValidUserToSeeAuditRequirment>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ValidUserToSeeAuditAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidUserToSeeAuditRequirment requirement)
{
// If he has the Admin Role, then he can see the Audit
if (context.User.HasClaim(x => x.Type.ToUpperInvariant() == "ROLE" && x.Value.ToUpperInvariant() == "ADMIN"))
{
context.Succeed(requirement);
return;
}
// Get the audit id from the Routing
var auditIdFromRoute = _httpContextAccessor.HttpContext.GetRouteData()?.Values["id"].ToString();
if (auditIdFromRoute is null || !Guid.TryParse(auditIdFromRoute, out Guid requestingAuditId))
{
context.Fail();
return;
}
// get the authenticated user
var userId = Convert.ToInt32(context.User.GetSubjectId());
// check if the user has authorized to see the audit
if(isUserAllowToSeeAudit(userId, requestingAuditId))
{
context.Succeed(requirement);
return;
}
context.Fail();
}
private bool isUserAllowToSeeAudit(int userId, Guid auditId)
{
// ...
}
I would suggest you used the IAuthorizationRequirement and AuthorizationHandler approach. Instances of AuthorizationHandler (where T is the requirement) are registered as singletons in your startup. As such, you can inject an IHttpRequestAccessor into the handler, giving it the capability of accessing the request. This ends up being something like this
public class YourAuthorizationHandler : AuthorizationHandler<YourAuthorizationRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
YourAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context, YourAuthorizationRequriement requirement)
{
var userIdInClaim = context.User.Claims.Where(claim => claimType == NameIdentifier).FirstOrDefault();
var request = _httpContextAccessor.HttpContext.Request;
request.EnableBuffering(); // allows the request to be read again
// read the request from request.Body assuming an HTTP POST. It will depend.
// do your logic checking the content here.
return context.Succeed(requirement); // Assuming things are what you want.
}
}
This is less complicated than it seems.
Read this Introduction to authorization in ASP.NET Core
I want to get Windows logon of the users in GrantResourceOwnerCredentials method of Provider class and validate them. I tried all the possible ways like below, but no luck.
System.Security.Principal.WindowsIdentity.GetCurrent().Name --> It is returning the server name
Request.LogonUserIdentity --> null (It cant be accessed before authentication)
HttpContext.Current.User --> null
From what I understand, if you're only using Windows Authentication, you don't need to worry about GrantResourceOwnerCredentials. Are you trying to use the token authentication as well as the Windows authentication? you should only use Windows Authentication for a Web Api that is going to run on your intranet.
Forgive me if I say things you already know, but from the research I've done, and my thanks go to Dominick Baier on pluralsight, you need to:
Install the Microsoft.Owin and Microsoft.Owin.Security.OAuth nuget packages
Set Windows Authentication to "Enabled" on the Project (F4 properties window) or in the config file
Make sure you have the [Authorize] attribute on your controller and inherit from the ApiController
Specifically implement the Owin middleware (you will need to create three classes and make sure they are configured in the startup.cs class) Have a look at the following code:
1st Middleware class: declare the function
public class ClaimsTransformationOptions
{
public Func<ClaimsPrincipal, Task<ClaimsPrincipal>> ClaimsTransformation { get; set; }
}
2nd Middleware class: this is where the Invoke method is
public class ClaimsTransformationMiddleware
{
readonly ClaimsTransformationOptions _options;
readonly Func<IDictionary<string, object>, Task> _next;
public ClaimsTransformationMiddleware(Func<IDictionary<string, object>, Task> next, ClaimsTransformationOptions options)
{
_next = next;
_options = options;
}
public async Task Invoke(IDictionary<string, object> env)
{
// use Katana OWIN abstractions (optional)
var context = new OwinContext(env);
if (context.Authentication != null &&
context.Authentication.User != null)
{
var transformedPrincipal = await _options.ClaimsTransformation(context.Authentication.User);
context.Authentication.User = new ClaimsPrincipal(transformedPrincipal);
}
await _next(env);
}
}
3rd Middleware class: this is an extension class
public static class ClaimsTransformationMiddlewareExtensions
{
public static IAppBuilder UseClaimsTransformation(this IAppBuilder app,
Func<ClaimsPrincipal, Task<ClaimsPrincipal>> transformation)
{
return app.UseClaimsTransformation(new ClaimsTransformationOptions
{
ClaimsTransformation = transformation
});
}
public static IAppBuilder UseClaimsTransformation(this IAppBuilder app, ClaimsTransformationOptions options)
{
if (options == null)
{
throw new ArgumentNullException("options");
}
app.Use(typeof(ClaimsTransformationMiddleware), options);
return app;
}
}
In the startup class:
public void Configuration(IAppBuilder app)
{
app.UseClaimsTransformation(Transformation);
}
private async Task<ClaimsPrincipal> Transformation(ClaimsPrincipal incoming)
{
if (!incoming.Identity.IsAuthenticated)
{
return incoming;
}
var name = incoming.Identity.Name;
// go to a datastore - find the app specific claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, name),
new Claim(ClaimTypes.Role, "foo"),
new Claim(ClaimTypes.Email, "foo#foo.com")
};
var id = new ClaimsIdentity("Windows");
id.AddClaims(claims);
return new ClaimsPrincipal(id);
}
In the Controller (Make sure it has the [Authorize] attribute and inherits from ApiController
public IEnumerable<ViewClaim> Get()
{
var principal = User as ClaimsPrincipal;
return from c in principal.Claims
select new ViewClaim
{
Type = c.Type,
Value = c.Value
};
}
I have a really weird issue which I have spent an entire day debugging and nowhere close to solving. I am in the process of upgrading my application from ASP.NET Core 1.x to 2.1. As part of doing this, I am having to re-wire the Authentication and Authorization mechanism. We use JWTBearer Authentication, and I am using postman to fire an API call, which executes the pipeline and I can see the AuthHandler executing. However if I fire the same request again, the AuthHandler does not execute and debugger "steps-over" the "context.AuthenticateAsync" call and returns the previous result. For the sake of elaborating, I have written a custom auth handler which is a copy paste of JWTAuthHandler. The code to create a custom handler is based off the answer here.
using Microsoft.AspNetCore.Authentication.JwtBearer;
public class CustomAuthOptions : JwtBearerOptions
{
}
using Microsoft.AspNetCore.Authentication;
public static class CustomAuthExtensions
{
public static AuthenticationBuilder AddCustomAuth(this AuthenticationBuilder builder, Action<CustomAuthOptions> configureOptions)
{
return builder.AddScheme<CustomAuthOptions, CustomAuthHandler>("CustomScheme", configureOptions);
}
}
public class CustomAuthHandler : AuthenticationHandler<CustomAuthOptions>
{
private OpenIdConnectConfiguration _configuration;
public CustomAuthHandler(IOptionsMonitor<CustomAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
/// <summary>
/// The handler calls methods on the events which give the application control at certain points where processing is occurring.
/// If it is not provided a default instance is supplied which does nothing when the methods are called.
/// </summary>
protected new JwtBearerEvents Events
{
get => (JwtBearerEvents)base.Events;
set => base.Events = value;
}
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new JwtBearerEvents());
/// <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["Authorization"];
// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}
if (authorization.StartsWith("CustomAuth ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("CustomAuth ".Length).Trim();
}
// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
}
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
var validationParameters = Options.TokenValidationParameters.Clone();
if (_configuration != null)
{
var issuers = new[] { _configuration.Issuer };
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;
validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys)
?? _configuration.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;
}
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
var authResult = await HandleAuthenticateOnceSafeAsync();
var eventContext = new JwtBearerChallengeContext(Context, Scheme, Options, properties)
{
AuthenticateFailure = authResult?.Failure
};
// Avoid returning error=invalid_token if the error is not caused by an authentication failure (e.g missing token).
if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null)
{
eventContext.Error = "invalid_token";
eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure);
}
await Events.Challenge(eventContext);
if (eventContext.Handled)
{
return;
}
Response.StatusCode = 401;
if (string.IsNullOrEmpty(eventContext.Error) &&
string.IsNullOrEmpty(eventContext.ErrorDescription) &&
string.IsNullOrEmpty(eventContext.ErrorUri))
{
Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge);
}
else
{
// https://tools.ietf.org/html/rfc6750#section-3.1
// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"
var builder = new StringBuilder(Options.Challenge);
if (Options.Challenge.IndexOf(" ", StringComparison.Ordinal) > 0)
{
// Only add a comma after the first param, if any
builder.Append(',');
builder.Append(',');
}
if (!string.IsNullOrEmpty(eventContext.Error))
{
builder.Append(" error=\"");
builder.Append(eventContext.Error);
builder.Append("\"");
}
if (!string.IsNullOrEmpty(eventContext.ErrorDescription))
{
if (!string.IsNullOrEmpty(eventContext.Error))
{
builder.Append(",");
}
builder.Append(" error_description=\"");
builder.Append(eventContext.ErrorDescription);
builder.Append('\"');
}
if (!string.IsNullOrEmpty(eventContext.ErrorUri))
{
if (!string.IsNullOrEmpty(eventContext.Error) ||
!string.IsNullOrEmpty(eventContext.ErrorDescription))
{
builder.Append(",");
}
builder.Append(" error_uri=\"");
builder.Append(eventContext.ErrorUri);
builder.Append('\"');
}
Response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString());
}
}
private static string CreateErrorDescription(Exception authFailure)
{
IEnumerable<Exception> exceptions;
if (authFailure is AggregateException agEx)
{
exceptions = agEx.InnerExceptions;
}
else
{
exceptions = new[] { authFailure };
}
var messages = new List<string>();
foreach (var ex in exceptions)
{
// Order sensitive, some of these exceptions derive from others
// and we want to display the most specific message possible.
switch (ex)
{
case SecurityTokenInvalidAudienceException _:
messages.Add("The audience is invalid");
break;
case SecurityTokenInvalidIssuerException _:
messages.Add("The issuer is invalid");
break;
case SecurityTokenNoExpirationException _:
messages.Add("The token has no expiration");
break;
case SecurityTokenInvalidLifetimeException _:
messages.Add("The token lifetime is invalid");
break;
case SecurityTokenNotYetValidException _:
messages.Add("The token is not valid yet");
break;
case SecurityTokenExpiredException _:
messages.Add("The token is expired");
break;
case SecurityTokenSignatureKeyNotFoundException _:
messages.Add("The signature key was not found");
break;
case SecurityTokenInvalidSignatureException _:
messages.Add("The signature is invalid");
break;
}
}
return string.Join("; ", messages);
}
}
And then the Startup.cs to hook it up:
public class Startup
{
/// <summary>
/// Initializes a new instance of the <see cref="Startup"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
public Startup(IConfiguration configuration)
{
this.Configuration = configuration;
ConfigureLogging();
}
/// <summary>
/// Gets the configuration.
/// </summary>
/// <value>
/// The configuration.
/// </value>
public IConfiguration Configuration { get; }
/// <summary>
/// Gets or sets the Container
/// </summary>
private IUnityContainer Container { get; set; }
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var logger = Logger.For(this).ForAction(nameof(ConfigureServices));
services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Optimal);
services.AddResponseCompression();
logger.Info("Configuring JWT Bearer Token Authorization...");
services.AddAuthentication(options =>
{
// the scheme name has to match the value we're going to use in AuthenticationBuilder.AddScheme(...)
options.DefaultAuthenticateScheme = "CustomScheme";
options.DefaultChallengeScheme = "CustomScheme";
})
.AddCustomAuth(options => {
options.Audience = this.Configuration.ObtainConfiguredString(ConfigurationKeys.ValidAudienceId);
options.Authority = this.Configuration.ObtainConfiguredString(ConfigurationKeys.IssuerId);
options.SaveToken = false;
options.TokenValidationParameters = new TokenValidationParameters().WithConfiguredParameters(this.Configuration);
});
logger.Info("Adding Authorization policies to Services...");
services.AddAuthorization(
options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder("CustomScheme").RequireAuthenticatedUser().Build();
});
services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IAuthenticationHandler, CustomAuthHandler>();
EnableCors(services);
logger.Info("Adding MVC support to Services...");
services.AddMvc(config =>
{
var defaultPolicy = new AuthorizationPolicyBuilder(new[] { "CustomScheme" })
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(defaultPolicy));
});
Container = new UnityContainer();
logger.Info("Registering other Services with UnityContainer...");
Container.RegisterServices(Configuration);
// Configure Microsoft DI for Unity resolution
logger.Info("Configuring ASP.Net Core service resolution to use UnityContainer...");
return services.UseUnityResolution(Container, s => s.BuildServiceProvider());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// <summary>
/// The Configure
/// </summary>
/// <param name="app">The app<see cref="IApplicationBuilder"/></param>
/// <param name="env">The env<see cref="IHostingEnvironment"/></param>
/// <param name="loggerFactory">The loggerFactory<see cref="ILoggerFactory"/></param>
/// <param name="memoryCache">The memoryCache<see cref="IMemoryCache"/></param>
/// <param name="contextAccessor">The contextAccessor<see cref="IHttpContextAccessor"/></param>
/// <param name="authzClient">The authzClient<see cref="IAuthzClient"/></param>
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IMemoryCache memoryCache,
IHttpContextAccessor contextAccessor,
IAuthzClient authzClient)
{
var logger = Logger.For(this).ForAction(nameof(Configure));
logger.Info("Configuring ASP.Net Core logging framework...");
loggerFactory.AddConsole(this.Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
var corsEnabled = this.Configuration.ObtainConfiguredBooleanWithDefault(ConfigurationKeys.EnableCors, false);
if (corsEnabled)
{
app.UseCors("CorsPolicy");
}
logger.Info("Configuring ASP.Net Core custom status page...");
app.UseStatusCodePagesWithReExecute("/error/{0}");
if (env.IsDevelopment())
{
logger.Info("Configuring development middle-ware...");
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
logger.Info("Configuring standard ASP.Net Core behaviors...");
app.UseDefaultFiles();
app.UseStaticFiles();
////app.UseAuthentication();
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
var result = await context.AuthenticateAsync("CustomScheme");
if (result?.Principal != null)
{
context.User = result.Principal;
}
}
await next.Invoke();
});
app.UseMvc();
app.WithRequestLogging();
}
private void EnableCors(IServiceCollection service)
{
var logger = Logger.For(this).ForAction(nameof(EnableCors));
var corsEnabled = this.Configuration.ObtainConfiguredBooleanWithDefault(ConfigurationKeys.EnableCors, false);
if (corsEnabled)
{
logger.Verbose("Configuring ASP.Net Core CORS support...");
service.AddCors(
options =>
{
options.AddPolicy("CorsPolicy",
builder =>
{
builder.AllowAnyOrigin();
builder.AllowAnyHeader();
builder.AllowAnyMethod();
builder.AllowCredentials();
});
});
}
}
}
}
Can someone please tell me what I am doing wrong? First time around when I fire the postman request with the correct AuthorizationHeader with the access token this line executes the CustomAuthHandler:
var result = await context.AuthenticateAsync("CustomScheme");
However the second time around the debugger steps over that code? This is driving me up the wall. I must be missing something basic!
EDIT:
In the Core 1.x version, ConfigureServices was setup as
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var logger = Logger.For(this).ForAction(nameof(ConfigureServices));
logger.Verbose("Adding MVC support to Services...");
// Add framework services.
services.AddMvc();
logger.Verbose("Adding Authorization policies to Services...");
services.AddAuthorization(
options =>
{
options.AddPolicy(
"SomePermission",
policy => policy.RequireClaim("claimUrl", "Some Permission"));
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
container = new UnityContainer();
logger.Verbose("Registering other Services with UnityContainer...");
container.RegisterServices(Configuration);
// Configure Microsoft DI for Unity resolution
logger.Verbose("Configuring ASP.Net Core service resolution to use UnityContainer...");
return services.UseUnityResolution(container, s => s.BuildServiceProvider());
}
And Configure() was wired up as follows
app.UseAuth0JwtBearerAuthentication(
new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters =
new TokenValidationParameters().WithConfiguredParameters(this.Configuration)
});
if (env.IsDevelopment())
{
logger.Verbose("Configuring development middleware...");
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
logger.Verbose("Configuring standard ASP.Net Core behaviors...");
app.UseDefaultFiles();
app.UseMvc();
app.UseStaticFiles();
Using this version if I execute postman calls, then I get a new ClaimsPrincipal for every request. So what has changed in ASP.NET Core 2.1?
For anyone who faces the same issue; my problem turned out to be Unity. ASP.NET Core 2.0 does not support Unity out of the box, because the ConfigureServices() method in Startup.cs is a replacement for third party DI container like Unity or Autofac. However if you still want to use Unity, you need to Nuget Unity.Microsoft.DependencyInjection to your project. The Github repo has details on how to wire it up.
Additionally all of the other dependent projects were using Unity 4.0.1 which has IUnityContainer under Microsoft.Practices.Unity, whereas from Unity 5 upwards, the IUnityContainer has been moved to Unity namespace. This was an additional gotcha, whereby even after setting up the DI container as per Github repo's instructions, I was getting exceptions with failed dependency resolutions. The workaround was the create a new UnityContainer using Microsoft.Practices.Unity, let the dependent projects bootstrap it, and then copy those registrations to IUnityContainer under the Unity namespace.
Startup.cs
public void ConfigureContainer(IUnityContainer container)
{
container.RegisterServices(Configuration);
}
UnityRegistrations.cs
public static void RegisterServices(this IUnityContainer container, IConfiguration configuration)
{
// Microsoft.Practices.Unity
var currentContainer = new UnityContainer();
// Bootstrap this and register dependencies
// Then copy them over
foreach (var registration in currentContainer.Registrations)
{
container.RegisterType(registration.RegisteredType, registration.MappedToType);
}
}