I've setup oauth authorization in an MVC aspnet core 2 application.
It works as intended when I use the [Authorize] attribute but I can't get it to work with my middleware RequestHandler.
I tried creating a service that calls the context.ChallengeAsync() method but it fails when called from the middleware (the call never redirects).
If the user isn't already logged in the consent page is never shown and the token returned is null.
If the user was already logged in the call returns the token.
The service does work when called from inside a controller (instead of using [Authorize])
So how do I get it to work ? I want to make sure the user is authorized (so that i have access to a token) before continuing...
Here are the relevant code sections:
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OAuthOptionsDefaults.SchemeName;
})
.AddCookie()
.AddOAuth(OAuthOptionsDefaults.SchemeName,
options => { options.SaveTokens = true; /*other options*/ });
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IOAuthService, OAuthService>();
services.AddTransient<IRequestHandler, RequestHandler>();
}
public void Configure(IApplicationBuilder app, IRequestHandler requestHandler, ..)
{
app.UseExceptionHandler("/Home/Error");
app.UseStaticFiles();
app.UseAuthentication();
app.Map(new PathString("/myapi"), appbuilder =>
{
appbuilder.Run(async (context) =>
{
await requestHandler.Handle(context, "url to an api", "an api key");
});
});
app.UseMvcWithDefaultRoute();
}
}
RequestHandler.cs
public class RequestHandler : IRequestHandler
{
//[Authorize] doesn't work
public async Task Handle(HttpContext context, string apiUrl, string apiKey)
{
//injected IOAuthService _service;
//tried with and without context as parameter
var accessToken = await _service.GetAccesTokenAsync();
//do stuff ...
}
}
OAuthService.cs
public class OAuthService : IOAuthService
{
private async Task<string> GetAccesTokenAsyncHelper(HttpContext context)
{
var isAuthenticated = ((ClaimsIdentity)context.User.Identity)?.IsAuthenticated ?? false;
if (!isAuthenticated)
{
//tried with and without setting schemename
await context.ChallengeAsync(OAuthOptionsDefaults.SchemeName);
}
return await context.GetTokenAsync(OAuthOptionsDefaults.SchemeName, "access_token");
}
public async Task<string> GetAccesTokenAsync()
{
//injected IHttpContextAccessor _accessor;
var context = _accessor.HttpContext;
return await GetAccesTokenAsyncHelper(context);
}
public async Task<string> GetAccesTokenAsync(HttpContext context)
{
return await GetAccesTokenAsyncHelper(context);
}
}
Edit: made the question shorter and more to point in the hopes of someone answering it.
You need to add Identity to the user property of HttpContext
context.User.AddIdentity((ClaimsIdentity) principal.Identity);
I have added a tutorial here explaining how to authorize HttpContext in Middleware with JWT from the URL
hope this will be helpful :)
Related
I'm writing an integration test for my web service with JWT authentication. I'd like to test it with a token received from the real service. The problem is real tokens expire in 1 hour.
A possible way is to set options.TokenValidationParameters.ValidateLifetime inside AddJwtBearer of class Startup below.
However, Startup class is a also a code to be tested, so I don't want to change or replace it for testing.
Is there a neat way to test all JWT validation logic except expiration?
My project code:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication("JWT")
.AddJwtBearer("JWT", options =>
{
options.Authority = "https://my-real-identity-server.com/";
options.Audience = "...";
// I don't want to disable lifetime validation in real life
// options.TokenValidationParameters.ValidateLifetime = false;
});
// Other stuff
}
public void Configure(IApplicationBuilder app) => app
.UseRouting()
.UseAuthentication()
.UseAuthorization()
.UseEndpoints(endpoints => endpoints.MapControllers());
}
public class TestController : ControllerBase
{
[Authorize]
[HttpGet("/validate")]
public string Get() => "success";
}
My test code:
public class HostBuilderTests
{
private IHost testHost;
private CancellationTokenSource cancel;
private HttpClient client;
[SetUp]
public async Task ShouldReturnStatus()
{
testHost = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder =>
webBuilder
.UseStartup<Startup>()
.UseTestServer())
.ConfigureServices(services => services
.AddLogging(b => b.ClearProviders().AddNUnit()))
.Build();
cancel = new CancellationTokenSource(10000);
var server = testHost.Services.GetRequiredService<IServer>()
.ShouldBeOfType<TestServer>();
await testHost.StartAsync(cancel.Token);
client = server.CreateClient();
}
[TearDown]
public async Task TearDown()
{
await testHost.StopAsync(cancel.Token);
client.Dispose();
testHost.Dispose();
cancel.Dispose();
}
[Test]
[TestCase("<<JWT token copied from the real service>>")]
public async Task StatusShouldBeOk(string realToken)
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", realToken);
using var response = await client.GetAsync("/validate", cancel.Token);
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
}
Finally I found a simple way to manage options for authentication handlers:
An authentication scheme is a name which corresponds to:
An authentication handler.
Options for configuring that specific instance of the handler.
See MSDN.
So it suffices to post-configure JwtBearerOptions specifying the authentication scheme name "JWT" as options instance name in the test setup:
testHost = Host.CreateDefaultBuilder()
// other setup
.ConfigureServices(services => services
.PostConfigure<JwtBearerOptions>("JWT",
op => op.TokenValidationParameters.ValidateLifetime = false)
// other setup
).Build();
Also it is possible to pass null instead of "JWT" as it written in comments:
// Null name is used to configure all named options.
I have created a microservice using .Net 5 which has some endpoints which can only be called with a jwtBearertoken.
The ConfigureServices and Configure methods in the StartUp class look like this:
public void ConfigureServices(IServiceCollection services)
{
ConfigureDatabaseServices(services);
ConfigureMyProjectClasses(services);
services.AddVersioning();
services.AddControllers();
services.AddAuthentication(_configuration);
// Add framework services.
var mvcBuilder = services
.AddMvc()
.AddControllersAsServices();
ConfigureJsonSerializer(mvcBuilder);
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment webEnv,
ILoggerFactory loggerFactory,
IHostApplicationLifetime applicationLifetime)
{
_logger = loggerFactory.CreateLogger("Startup");
try
{
app.Use(async (context, next) =>
{
var correlationId = Guid.NewGuid();
System.Diagnostics.Trace.CorrelationManager.ActivityId = correlationId;
context.Response.Headers.Add("X-Correlation-ID", correlationId.ToString());
await next();
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
applicationLifetime.ApplicationStopped.Register(() =>
{
LogManager.Shutdown();
});
}
catch (Exception e)
{
_logger.LogError(e.Message);
throw;
}
}
AuthenticationExtensions:
public static class AuthenticationExtensions
{
public static void AddAuthentication(this IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.Authority = configuration["Authorization:Authority"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
}
I'm using an authorization server for the microservice to validate the token.
After adding an [Authorize] attribute above the controllers postman returns 401 Unauthorized and the integration tests I had created before adding Authentication also return Unauthorized as expected.
Now I am trying to figure out how I can change my integration tests by adding a JwtBearerToken and mocking the response from the authorization server so my tests will pass again.
How can I achieve this?
My answer is not 100% integrated, because we will add an extra auth scheme. TL;DR: You are not testing if your auth works, but working around it.
It would be best to use an ACTUAL token, but perhaps this solution is a nice middle ground.
You could create another auth scheme like DevBearer where you can specify an account, for example if you send the auth header DevBearer Customer-John, the application would recognize you as Customer John.
I use this approach during development because it is very easy to just test different users quickly. My code looks something like this:
Startup.Auth.cs
private void ConfigureAuthentication(IServiceCollection services)
{
services.AddHttpContextAccessor();
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = "Audience";
options.Authority = "Authority";
});
#if DEBUG
if (Environment.IsDevelopment())
{
AllowDevelopmentAuthAccounts(services);
return;
}
#endif
// This is custom and you might need change it to your needs.
services.AddAuthorization();
}
#if DEBUG
// If this is true, you can use the Official JWT bearer login flow AND Development Auth Account (DevBearer) flow for easier testing.
private static void AllowDevelopmentAuthAccounts(IServiceCollection services)
{
services.AddAuthentication("DevBearer").AddScheme<DevelopmentAuthenticationSchemeOptions, DevelopmentAuthenticationHandler>("DevBearer", null);
// This is custom and you might need change it to your needs.
services.AddAuthorization();
}
#endif
Custom Policies Hint
// Because my Policies/Auth situation is different than yours, I will only post a hint that you might want to use.
// I want to allow calls from the REAL flow AND DevBearer flow during development so I can easily call my API using the DevBearer flow, or still connect it to the real IDentityServer and front-end for REAL calls.
var policyBuilder = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser();
// The #IF adds an extra "security" check so we don't accidentally activate the development auth flow on production
#if DEBUG
if (_allowDevelopmentAuthAccountCalls)
{
policyBuilder.AddAuthenticationSchemes("DevBearer").RequireAuthenticatedUser();
}
#endif
return policyBuilder;
Auth handler
#if DEBUG
using System;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace NAMESPACE
{
public class DevelopmentAuthenticationHandler : AuthenticationHandler<DevelopmentAuthenticationSchemeOptions>
{
public DevelopmentAuthenticationHandler(
IOptionsMonitor<DevelopmentAuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Context.Request.Headers.TryGetValue("Authorization", out var authorizationHeader))
{
return AuthenticateResult.Fail("Unauthorized");
}
var auth = AuthenticationHeaderValue.Parse(authorizationHeader);
if (auth.Scheme == "Bearer")
{
// If Bearer is used, it means the user wants to use the REAL authentication method and not the development accounts.
return AuthenticateResult.Fail("Bearer requests should use the real JWT validation scheme");
}
// Dumb workaround for NSwag/Swagger: I can't find a way to make it automatically pass "DevBearer" in the auth header.
// Having to type DevBearer everytime is annoying. So if it is missing, we just pretend it's there.
// This means you can either pass "ACCOUNT_NAME" in the Authorization header OR "DevBearer ACCOUNT_NAME".
if (auth.Parameter == null)
{
auth = new AuthenticationHeaderValue("DevBearer", auth.Scheme);
}
IEnumerable<Claim> claims;
try
{
var user = auth.Parameter;
claims = GetClaimsForUser(user);
}
catch (ArgumentException e)
{
return AuthenticateResult.Fail(e);
}
var identity = new ClaimsIdentity(claims, "DevBearer");
var principal = new ClaimsPrincipal(identity);
// Add extra claims if you want to
await Options.OnTokenValidated(Context, principal);
var ticket = new AuthenticationTicket(principal, "DevBearer");
return AuthenticateResult.Success(ticket);
}
private static IEnumerable<Claim> GetClaimsForUser(string? user)
{
switch (user?.ToLowerInvariant())
{
// These all depend on your needs.
case "Customer-John":
{
yield return new("ID_CLAIM_NAME", Guid.Parse("JOHN_GUID_THAT_EXISTS_IN_YOUR_DATABASE").ToString(), ClaimValueTypes.String);
yield return new("ROLE_CLAIM_NAME", "Customer", ClaimValueTypes.String);
break;
}
default:
{
throw new ArgumentException("Can't set specific account for local development because the user is not recognized", nameof(user));
}
}
}
}
public class DevelopmentAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
public Func<HttpContext, ClaimsPrincipal, Task> OnTokenValidated { get; set; } = (context, principal) => { return Task.CompletedTask; };
}
}
#endif
With something like this, you could do an API call with an authorization header like DevBearer Customer-John and it would add the ID and role claim to the context, allowing auth to succeed :)
Here is Startup.cs from SignInWithAppleSample:
public void ConfigureServices(IServiceCollection services)
{
// ...
services
.AddAuthentication(options => options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/signin";
options.LogoutPath = "/signout";
})
.AddApple(options =>
{
options.ClientId = Configuration["AppleClientId"];
options.KeyId = Configuration["AppleKeyId"];
options.TeamId = Configuration["AppleTeamId"];
options.UsePrivateKey(
(keyId) =>
Environment.ContentRootFileProvider.GetFileInfo($"AuthKey_{keyId}.p8"));
});
// ...
}
public void Configure(IApplicationBuilder app)
{
// ...
app.UseAuthentication();
// ...
}
Here's controller's method, which uses for authentication:
public class AuthenticationController : Controller
{
[HttpPost("~/signin")]
public IActionResult SignIn()
=> Challenge(new AuthenticationProperties { RedirectUri = "/redirect/signin/apple" }, AppleAuthenticationDefaults.AuthenticationScheme);
[HttpGet("~/signout")]
[HttpPost("~/signout")]
public IActionResult SignOut()
=> SignOut(new AuthenticationProperties { RedirectUri = "/redirect/signout/apple" }, CookieAuthenticationDefaults.AuthenticationScheme);
}
It works, cookies set, I can see my claims.
In my project, I don't use built-in ASP.NET Core, I simply need to handle request on /redirect/signin/apple and return access token, identity token, etc. to my clients, and maybe check in DB that this user is already registered. Client's don't want cookies, they accept only Bearer tokens.
So, the question is, how can I get Bearer token instead of cookies?
I have a custom policy based authorization handler as defined below. Authentication is handled before the user hit this application so I only need authorization. I am getting the error:
No authenticationScheme was specified, and there was no DefaultForbidScheme.
If the authorization check succeeds then I do not get the error and all is well. This error only happens when the authorization check fails. I would expect that a 401 is returned on failure.
public class EasRequirement : IAuthorizationRequirement
{
public EasRequirement(string easBaseAddress, string applicationName, bool bypassAuthorization)
{
_client = GetConfiguredClient(easBaseAddress);
_applicationName = applicationName;
_bypassAuthorization = bypassAuthorization;
}
public async Task<bool> IsAuthorized(ActionContext actionContext)
{
...
}
}
public class EasHandler : AuthorizationHandler<EasRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EasRequirement requirement)
{
var mvcContext = context.Resource as ActionContext;
bool isAuthorized;
try
{
isAuthorized = requirement.IsAuthorized(mvcContext).Result;
}
catch (Exception)
{
// TODO: log the error?
isAuthorized = false;
}
if (isAuthorized)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
context.Fail();
return Task.FromResult(0);
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var easBaseAddress = Configuration.GetSection("EasBaseAddress").Value;
var applicationName = Configuration.GetSection("ApplicationName").Value;
var bypassAuthorization = bool.Parse(Configuration.GetSection("BypassEasAuthorization").Value);
var policy = new AuthorizationPolicyBuilder()
.AddRequirements(new EasRequirement(easBaseAddress, applicationName, bypassAuthorization))
.Build();
services.AddAuthorization(options =>
{
options.AddPolicy("EAS", policy);
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSingleton<IAuthorizationHandler, EasHandler>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
}
Authorization and authentication are closely linked in ASP.NET Core. When authorization fails, this will be passed to an authentication handler to handle the authorization failure.
So even if you don’t need actual authentication to identify your users, you will still need to set up some authentication scheme that is able to handle forbid and challenge results (403 and 401).
To do that, you need to call AddAuthentication() and configure a default forbid/challenge scheme:
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "scheme name";
// you can also skip this to make the challenge scheme handle the forbid as well
options.DefaultForbidScheme = "scheme name";
// of course you also need to register that scheme, e.g. using
options.AddScheme<MySchemeHandler>("scheme name", "scheme display name");
});
MySchemeHandler needs to implement IAuthenticationHandler and in your case, you especially need to implement ChallengeAsync and ForbidAsync:
public class MySchemeHandler : IAuthenticationHandler
{
private HttpContext _context;
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
public Task<AuthenticateResult> AuthenticateAsync()
=> Task.FromResult(AuthenticateResult.NoResult());
public Task ChallengeAsync(AuthenticationProperties properties)
{
// do something
}
public Task ForbidAsync(AuthenticationProperties properties)
{
// do something
}
}
For IIS/IIS Express, can just add this line instead of all of the above in the accepted answer to get the appropriate 403 response you're expecting;
services.AddAuthentication(IISDefaults.AuthenticationScheme);
I want to log stats for all calls to my .netcore webapi.
I have added a IAsyncActionFilter for this purpose and it picks up on all the actions.
But I also have Jwt Bearer Authentication enabled and am using the AuthorizeAttribute on my controller to limit access. When access is denied the Action filter will not be hit.
Whats the best way to add some custom logging (statsd) for authentication in general and failures in particular?
public void ConfigureServices(IServiceCollection services)
{
....
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// base-address of your identityserver
options.Authority = Configuration["Authority"]; ;
// name of the API resource
options.Audience = "myAudience";
});
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseAuthentication();
...
}
I Notice that JwtBearerOptions has JwtBearerEvents Events but I cant get this to work.
Edit : It looks like I am hitting the api with no token at all and the JWT Auth handler returns AuthenticateResult.NoResult() without calling the Events.AuthenticationFailed
https://github.com/aspnet/Security/blob/ba1eb281d135400436c52c17edc71307bc038ec0/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L63-L83
Edit 2 : Very frustrating. Looks like the correct place to log would be in Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter But this is automatically added when you use [Authorize] and is impossible to override, remove or replace?
The JwtBearerOptions.cs class exposes an JwtBearerEvents parameter where you can declare your events like this
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// base-address of your identityserver
options.Authority = Configuration["Authority"]; ;
// name of the API resource
options.Audience = "myAudience";
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
//Log failed authentications
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
//Log successful authentications
return Task.CompletedTask;
}
};
});
I don´t have any experience with Jwt Bearer Authentication - but we´ve had a similar situation. We´ve ended up with a new BaseController where we´re able to Log basic user information and distinguish between [AllowAnonymous] and [Authorize]:
public class NewBaseController : Controller
{
protected UserManager<MyUser> UserManager;
protected MyUser CurrentUser;
protected ILogger Logger;
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
UserManager = context.HttpContext.RequestServices.GetService<UserManager<MyUser>>();
var loggerFactory = context.HttpContext.RequestServices.GetService<ILoggerFactory>();
Logger = loggerFactory.CreateLogger(GetType());
// Check if Action is annotated with [AllowAnonymous]
var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
var anonymousAllowed = controllerActionDescriptor.MethodInfo
.GetCustomAttributes(inherit: true)
.Any(a => a.GetType().Equals(typeof(AllowAnonymousAttribute)));
if (!anonymousAllowed)
{
ApplicationUser = UserManager.GetUserAsync(User).Result;
if (ApplicationUser == null)
// do some stuff
Logger.LogInformation("User is {UserId}", CurrentUser.Id);
}
else
{
Logger.LogInformation("User is {User}", anonymous);
}
}
}
Bonus: Every Controller that derives from this base already has an instance of UserManager, ILogger and CurrentUser.
Hope that comes close to what you´re looking for...