I have an api build in .net core 2.1. To restrict access to various endpoints, I use IdentityServer4 and [Authorize] attribute. However, my goal during development is to expose the api swagger documentation to our developers so that they may use it no matter where they work from. The challenge that I face is how do I protect the swagger index.html file so that only they can see the details of the api.
I have created a custom index.html file in the wwwroot/swagger/ui folder and that all works, however, that file uses data from /swagger/v1/swagger.json endpoint which is not protected. I would like to know how can I override the return value for that specific endpoint so that I may add my own authentication to it?
EDIT:
Currently, I have achieved the above with the following middleware:
public class SwaggerInterceptor
{
private readonly RequestDelegate _next;
public SwaggerInterceptor(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
var uri = context.Request.Path.ToString();
if (uri.StartsWith("/swagger/ui/index.html"))
{
var param = context.Request.QueryString.Value;
if (!param.Equals("?key=123"))
{
context.Response.StatusCode = 404;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("{\"result:\" \"Not Found\"}", Encoding.UTF8);
return;
}
}
await _next.Invoke(context);
}
}
public class Startup
{
//omitted code
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<SwaggerInterceptor>();
//omitted code
}
}
What I don't like about this approach as it will inspect every single request. Is there a better way to achieve this? The above only protects the index.html file, but I can adjust it to protect the json endpoint in the similar fashion.
You can choose some options:
basic authorization
OpenId Connect authorization using identity server
Basic Authorization
In this case you just close your swagger endpoints.
// Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication()
.AddScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>("Basic", _ => {});
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseEndpoints(endpoints =>
{
...
var pipeline = endpoints.CreateApplicationBuilder().Build();
var basicAuthAttr = new AuthorizeAttribute { AuthenticationSchemes = "Basic" };
endpoints
.Map("/swagger/{documentName}/swagger.json", pipeline)
.RequireAuthorization(basicAuthAttr);
endpoints
.Map("/swagger/index.html", pipeline)
.RequireAuthorization(basicAuthAttr);
});
}
}
// BasicAuthenticationHandler.cs
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
...
}
OIDC Authorization Using IdentityServer4
I have written the article for this case: https://medium.com/dev-genius/csharp-protecting-swagger-endpoints-82ae5cfc7eb1
Here it is using OpenIdConnect and Swashbuckle in Asp.Net Core 3.1. Now, if I type https://myurl.com/swagger I get routed to my normal login page and after logging in successfully, I can see the swagger.
public class Startup
{
//<snip/>
public void Configure(IApplicationBuilder app)
{
//<snip/>
app.UseAuthentication();
app.UseAuthorization();
app.UseSwagger();
app.UseSwaggerUI(c => { c.SwaggerEndpoint("v1/swagger.json", "Some name"); });
app.UseEndpoints(routes =>
{
var pipeline = routes.CreateApplicationBuilder().Build();
routes.Map("/swagger", pipeline).RequireAuthorization(new AuthorizeAttribute {AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme});
routes.Map("/swagger/index.html", pipeline).RequireAuthorization(new AuthorizeAttribute {AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme});
routes.Map("/swagger/v1/swagger.json", pipeline).RequireAuthorization(new AuthorizeAttribute { AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme });
routes.Map("/swagger/{documentName}/swagger.json", pipeline).RequireAuthorization(new AuthorizeAttribute { AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme });
routes.MapDefaultControllerRoute();
});
}
}
EDIT:
Somehow I thought the below was working, but when I retested it later it turned out that actually it was giving error: The request reached the end of the pipeline without executing the endpoint. So, I changed to include a fixed set of endpoints under /swagger that contain afaik the key data.
routes.Map("/swagger/{**any}", pipeline).RequireAuthorization(new AuthorizeAttribute {AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme});
Note: this {**any} part of the route template protects all files under /swagger as well, so for example /swagger/index.html, /swagger/v1/swagger.json etc etc.
I believe your best option is what you already did. Build your own middleware, as I don't know any middleware for validate authentication on static files. You could add the basePath to avoid enter at this specific middleware when its not necessary. Like the code below
app.Map("/swagger", (appBuilder) =>
{
appBuilder.UseMiddleware<SwaggerInterceptor>();
});
Also this article could help you to build a more generic middleware for validate authentication on static files.
https://odetocode.com/blogs/scott/archive/2015/10/06/authorization-policies-and-middleware-in-asp-net-5.aspx
After some search I solved and it works for me.
you must define a middle-ware class to authenticate the user whom wants to visit your swagger-ui view page like this.
public class SwaggerBasicAuthMiddleware
{
private readonly RequestDelegate next;
public SwaggerBasicAuthMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/swagger"))
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic "))
{
// Get the credentials from request header
var header = AuthenticationHeaderValue.Parse(authHeader);
var inBytes = Convert.FromBase64String(header.Parameter);
var credentials = Encoding.UTF8.GetString(inBytes).Split(':');
var username = credentials[0];
var password = credentials[1];
// validate credentials
if (username.Equals("Test") && password.Equals("Test"))
{
await next.Invoke(context).ConfigureAwait(false);
return;
}
}
context.Response.Headers["WWW-Authenticate"] = "Basic";
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
await next.Invoke(context).ConfigureAwait(false);
}
}
}
For simplicity I am using hardcoded credentials but same can enhanced to use it from database also.
Also I have to create a extension method in a static class like this
public static class AuthorizedSampleClass
{
public static IApplicationBuilder UseSwaggerAuthorized( this IApplicationBuilder builder )
{
return builder.UseMiddleware<SwaggerBasicAuthMiddleware>( );
}
}
in startup.cs remove the env.IsDevelopment() part and add app.UseSwaggerAuthorized( ); right before app.UseSwagger( ); because authentication middleware will be called before accessing swagger ui.
app.UseSwaggerAuthorized( );
app.UseSwagger( );
app.UseSwaggerUI( c => c.SwaggerEndpoint( "/swagger/v1/swagger.json", "Broker.Rest v1" ) );
now press F5 and rout to swagger view, it has done.
press "Test" and "Test" as username and password to enter.
Related
I am testing a self-hosted Asp Net Core Web server (Kestrel), and I am struggling with the client authentication using self-signed certificates.
This is my startup code
WebApplicationBuilder webBuilder = WebApplication.CreateBuilder();
var webHostBuilder = builder.WebHost;
X509Certificate2 rootCert = new X509Certificate2(hostCertFilePath, hostCertPassword);
webHostBuilder.ConfigureKestrel(o =>
{
o.ConfigureHttpsDefaults(o =>
{
o.ServerCertificate = rootCert;
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
});
webHostBuilder.UseKestrel(o =>
{
o.Listen(IPAddress.Parse(myHttpsEndPointIpAddr), myHttpsEndPointPort,
listenOptions =>
{
listenOptions.UseHttps();
});
o.Listen(IPAddress.Parse(myHttpEndPointIpAddr), myHttpEndPointPort);
});
var services = webBuilder.Services;
services.AddTransient<MyCustomCertificateValidationService>();
services
.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.SelfSigned;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService = context.HttpContext.RequestServices
.GetService<MyCustomCertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate))
{
context.Success();
}
else
{
context.Fail("invalid cert");
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Fail("invalid cert");
return Task.CompletedTask;
}
};
});
...
var app = webBuilder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
This my custom certification class
public class MyCustomCertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate)
{
// todo: check certificate thumbnail
return false;
}
}
But even if MyCustomCertificateValidationService has a method ValidateCertificate() that returns false, the controller method is still called when a client accesses the url with the route to the controller method.
This is what is displayed in the log:
...
AspNetCore.Routing.EndpointRoutingMiddleware : Request matched endpoint ‘GetMyData…‘
AspNetCore.Authentication.Certificate.CertificateAuthenticationHandler : Certificate was not authenticated. Failure message: invalid cert
AspNetCore.Routing.EndpointMiddleware : Executing endpoint ‘GetMyData…‘
...
Any clue why the controller method is still called?
"There is a use-case for the application that in some test environment
also unauthorized calls (over http://...) should be allowed. I would
prefer to use, if possible, a settings parameter to decide dynamically
if http access is allowed or not, instead of "hardcode" it as
[Authorize] attribute"
Of course you can do that. There is a handy way to implement your requirement using middleware for sure. Please try the code snippe below:
Http/Https Request Middleare Based On Environment:
public class CustomHttpHttpsRequestMiddleware
{
private readonly RequestDelegate next;
public CustomHttpHttpsRequestMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
//env = "Production";
if (env == "Development")
{
await next(context);
}
else
{
if (!context.Request.IsHttps)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("HTTPS required!");
}
}
}
}
Note: In application request context we are checking two important value, first if the request is secure means cIsHttps and the application environment, in Development environment we will allow http request. Therefore, other than, dev or any env based on our requirement we will reject http request.
Register Middleware on Program.cs:
app.UseMiddleware<CustomHttpHttpsRequestMiddleware>();
Note: Make sure you have followed the correct middleware order. In order to avoid short circuiting, you could place this middleware way down of your all current middleware.
Output:
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 :)
I have a .Net 5 Web API that is protected by Azure AD. What I want to accomplish is to provide custom error message when authorization fails. For example,
If a user forgets to add Authorization header in the request, I want to tell the user that this header is required.
If the token has expired, I want to tell the user exactly that rather than simply returning 401 status code with no details.
Simply putting [Authorize] attribute before controller and/or action does not work as it returns 401 status code and does not include any details about this error.
I searched for it and found that I have to write a custom authorization filter and using that it should be possible to accomplish what I am looking for.
Using this blog post, I was able to write a custom authorization filter however I am still not able to find out how to validate the token and return appropriate error messages.
Here's the code I wrote so far:
Startup.cs
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)
{
AuthorizationPolicy policy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build();
_ = services.AddAuthorization(options =>
{
options.DefaultPolicy = policy;
});
_ = services.AddControllers(options =>
{
options.Filters.Add(new CustomTokenAuthorizationFilter(policy));
});
_ = services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" });
});
_ = services
.AddMicrosoftIdentityWebApiAuthentication(Configuration, "ApiSettings");
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
_ = app.UseDeveloperExceptionPage();
_ = app.UseSwagger();
_ = app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"));
}
_ = app.UseConfigureRequestHeaders();
_ = app.UseHttpsRedirection();
_ = app.UseRouting();
//_ = app.UseAuthentication();
_ = app.UseAuthorization();
_ = app.UseEndpoints(endpoints =>
{
_ = endpoints.MapControllers();
});
}
}
CustomAuthorizationFilter.cs
public class CustomTokenAuthorizationFilter : AuthorizeFilter
{
public CustomTokenAuthorizationFilter(AuthorizationPolicy policy) : base(policy)
{
}
public override async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Allow Anonymous skips all authorization
if (context.Filters.Any(item => item is IAllowAnonymousFilter))
{
return;
}
var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService<IPolicyEvaluator>();
var authenticateResult = await policyEvaluator.AuthenticateAsync(Policy, context.HttpContext);
var authorizeResult = await policyEvaluator.AuthorizeAsync(Policy, authenticateResult, context.HttpContext, context);
return;
}
}
With this code, when there's an issue with my token authenticateResult.Succeeded property is false and authorizeResult.Challenged property is true.
How can I find out what caused the authorization to fail? Any help would be highly appreciated.
I am using Microsoft.Identity.Web NuGet Package as suggested by Microsoft.
What you could possibly do here and user an error controller and catch your unauthorized exception and then output the message from that exception.
This is a good Stackoverflow post about how to configure an error controller
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'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 :)