OpenIddict - Authorization and authentication with Microservices - authentication

I have a mobile (native) and web app (SPA) that talks to backend microservices (developed in core 2.0) for authentication/authorization and other domain related functions, which has configured using Opendidict. Both apps are getting access token. What I'm struggling with is, all microservices should accept bearer access token and authentication/authorization logged in user (a central auth-service), access token generated in auth microservice (OpenIddict 2.*). So what changes I'm missing in microservices, where REST APIs are marked [Authorize]?
Code from Auth Microservice:
public void ConfigureServices(IServiceCollection services)
{
var connection = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<IdentityDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
options.UseOpenIddict();
});
services.AddAuthentication().AddOAuthValidation();
services.AddOpenIddict(options =>
{
options.AddEntityFrameworkCoreStores<IdentityDbContext>();
options.AddMvcBinders();
options.EnableTokenEndpoint("/connect/token");
// Enable the password flow.
options.AllowPasswordFlow().AllowRefreshTokenFlow();
options.SetRefreshTokenLifetime(TimeSpan.FromHours(1));
options.DisableHttpsRequirement();
});
services.AddDbContext<AuthDbContext>(options => options.UseSqlServer(connection));
services.AddScoped<IUserRepository, UserRepository>();
services.AddAuthentication(options =>
{
options.DefaultScheme = OAuthValidationDefaults.AuthenticationScheme;
});
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator"));
});
}
Existing code in Notification Microservice
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MastersDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddAuthentication().AddOAuthValidation();
services.AddAuthentication(options =>
{
options.DefaultScheme = OAuthValidationDefaults.AuthenticationScheme;
});
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator"));
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseCors(builder =>
builder.WithOrigins("*")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin()
);
//app.UseAntiforgeryToken();
app.UseMvc();
app.UseAuthentication();
}
Notification Controller:
// POST api/values
[HttpPost]
[Authorize]
public IActionResult Post(Notification notification)
{
//logic
return Ok();
}

For tokens to be correctly decrypted by all your micro-services, you need to make sure that the key ring containing the master keys (that are derived by ASP.NET Core Data Protection to create encryption and validation keys) is correctly synchronized. The procedure is described here: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview.
Here's an example of how it could be done using a shared folder:
public void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(#"\\server\share\directory\"))
}
You'll also need to configure the two applications to use the same "application discriminator":
public void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(#"\\server\share\directory\"))
.SetApplicationName("Your application name");
}

Related

How do I authorise the Hangfire Dashboard via Microsoft Single Sign-On with Angular 12 and ASP.Net Core 5

My application is an Angular 12 application running on ASP.Net Core 5.
I am currently trying to lock down Hangfire so that it will only work for people with the Admin role.
It uses Microsoft Identity to log in - specifically Single Sign-on, set up in Azure.
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHangfire(x =>
{
x.UseSqlServerStorage(sqlServerConnectionString);
});
...
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration);
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] {
new HangfireAuthorisationFilter()
},
AppPath = "/"
});
...
app.UseEndpoints(endpoints => {
...
});
app.UseSpa(spa=>{
...
});
}
This works in my dot net core controllers.
All I need to do to get it to work is add the Authorize attribute:
namespace MyAppName.Controllers
{
[Produces("application/json")]
[Route("api/MyRoute")]
[Authorize(Roles="Role1,Role2,Administrator")]
public class MyControllerController: MyBaseApiController
{
...
}
}
But when I want to Authorise in Hangfire, the User object is missing a whole lot of its properties.
Here is the HangfireAuthorisationFilter:
public class HangfireAuthorisationFilter : IDashboardAuthorizationFilter
{
public HangfireAuthorisationFilter()
{
}
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
// the next line always fails. The User object is set. The Identity object is set
// but there are no claims and the User.Name is null. There are also no roles set.
return httpContext.User.Identity.IsAuthenticated;
}
}
There is, however, cookie information, containing the msal cookie:
How can I pass authentication information into the Hangfire Authorize method? How can I access the role information so that I can lock it down to just the Admin role? Is there a way I can decode the msal cookie server-side?
Assuming you have an AzureAd configuration block that looks like below:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]",
"TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]",
"ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]"
}
I think a better approach to avoid manual validation of the token is to change your code to the following:
public void ConfigureServices(IServiceCollection services)
{
services.AddHangfire(x =>
{
x.UseSqlServerStorage(sqlServerConnectionString);
});
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration);
services.
.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.AddAuthorization(options =>
{
options.AddPolicy("Hangfire", builder =>
{
builder
.AddAuthenticationSchemes(AzureADDefaults.AuthenticationScheme)
.RequireRole("Admin")
.RequireAuthenticatedUser();
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHangfireDashboard("/hangfire", new DashboardOptions()
{
Authorization = Enumerable.Empty<IDashboardAuthorizationFilter>()
})
.RequireAuthorization("Hangfire");
});
}
To break this down, the following changes have been made:
Add authentication for AzureADDefaults.AuthenticationScheme so we can create a policy requiring the "Admin" role.
Add a policy named "Hangfire" that requires the "Admin" role against a user. See the AddAuthorization call.
Instead of calling UseHangfireDashboard we call MapHangfireDashboard inside UseEndpoints and protect the hangfire dashboard endpoint using our "Hangfire" policy through the call to RequireAuthorization("Hangfire")
Removal off the HangfireAuthorisationFilter which is not needed and instead we pass an empty collection of filters in the MapHangfireDashboard call.
The key takeaway is that we are now relying on the security provided by the middleware rather than the implementation of IDashboardAuthorizationFilter which comes with huge risk around the token being invalid and/or a mistake is made in the logic.
Ok I have figured out how to decode the msal cookie to get my list of claims and roles, and authorise successfully with Hangfire
using Hangfire.Dashboard;
using System.IdentityModel.Tokens.Jwt;
namespace MyApp.Filters
{
public class HangfireAuthorisationFilter : IDashboardAuthorizationFilter
{
public HangfireAuthorisationFilter()
{
}
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
var cookies = httpContext.Request.Cookies;
var msalIdToken = cookies["msal.{your app client id goes here}.idtoken"];
var token = new JwtSecurityTokenHandler().ReadJwtToken(msalIdToken);
foreach(var claim in token.Claims)
{
if (claim.Type=="roles" && claim.Value == "Admin")
{
return true;
}
}
return false;
}
}
}

Deployed WebAssembly Blazor application doesn't route authentication properly, but locally working

I created a 'normal' WebAssembly Blazor client and server application.
I decided later on to add authentication, so I followed the steps at this address:
https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-identity-server?view=aspnetcore-3.1&tabs=visual-studio
ending up with this startup code in the server part of the Blazor WebAssembly application:
public class Startup
{
private readonly IWebHostEnvironment _environment;
private readonly IConfiguration _configuration;
public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
_environment = environment;
_configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
if (_environment.IsDevelopment())
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
_configuration.GetConnectionString("LocalEnvironment")));
}
else
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
_configuration.GetConnectionString("CloudEnvironment")));
}
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
options.IdentityResources["openid"].UserClaims.Add("name");
options.ApiResources.Single().UserClaims.Add("name");
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddRazorPages();
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier;
options.User.RequireUniqueEmail = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = true;
options.Password.RequireDigit = true;
});
services.AddTransient<IPasswordValidator<ApplicationUser>, CustomPasswordPolicy>();
services.AddTransient<IUserValidator<ApplicationUser>, CustomUsernameEmailPolicy>();
services.AddTransient<IProfileService, ProfileService>();
services.AddHttpContextAccessor();
services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(60);
});
}
public void Configure(IApplicationBuilder app, ApplicationDbContext db,
UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager)
{
if (_environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
db.Database.EnsureCreated();
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
IdentityDataInitializer.SeedTestData(userManager, roleManager);
}
}
The resulting application works perfectly when in the Development environment (both in Kestrel and IIS Express, but when I deploy it to an Azure Service App, the authentication part, and only that one, doesn't work properly.
For example: if I click the Login button in the home page, when I'm local there's a jump to the page:
https://localhost:5001/Identity/Account/Login?ReturnUrl=...
That's the correct path, because, moreover, after logging in, I'm redirected correctly to the home page.
But when I click the same button on the deployed application, I see the address becoming first
'.../authentication/login'
and after a few moments, going to
'.../connect/authorize?client_id=Test1.Client&redirect_uri=...'
that's a not existing page.
Personally, I don't even understand, at the moment, if it's a server or client problem, or just the configuration of the service app on Azure...
Please, feel free to ask for other code, or anything that can help.
Thank you in advance.
/connect/authorize is one of the endpoints that IdentityServer listens for and it is the first URL that the application/client should redirect to when the user is about to authenticate.
One way to tell if IdentityServer is up and running is to go to this URL https://yourdomain.com/.well-known/openid-configuration
This URL Should always succeed.
When you deploy to the cloud and Azure Service App, one thing is to make sure you understand where HTTPS is terminated, is it in your application or in Azure Service? If it is not terminated in the application it-self, then it might be that the public URL is HTTPS but what your application sees is HTTP.
Some links to follow:
https://securecloud.blog/2020/07/23/unobvious-stuff-about-azure-services-app-service-tls-termination/
https://www.hanselman.com/blog/SecuringAnAzureAppServiceWebsiteUnderSSLInMinutesWithLetsEncrypt.aspx

Problem in enabling CORS in asp net core web api v3.0

I am using asp net core 3.0 in my web API project. I have created various API's and all are accessible via Swagger or Postman. But when trying to access the same via any other client like React, Method not allowed (405 error code) is received. On investing further, I find out that at first, OPTION request is received from the React application and the net core web API application is giving the 405 status code. Further, I find out that I need to enable all the methods as well as origins from the net core application to accept all types of requests otherwise it will not accept OPTION request. To achieve this, I enabled CORS policy in startup.cs file but still had no luck. Following is my startup.cs file:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
var elasticUri = Configuration["ElasticConfiguration:Uri"];
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.WithExceptionDetails()
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri(elasticUri))
{
MinimumLogEventLevel = LogEventLevel.Verbose,
AutoRegisterTemplate = true,
})
.CreateLogger();
}
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)
{
services.Configure<IISServerOptions>(options =>
{
options.AutomaticAuthentication = false;
});
services.Configure<ApiBehaviorOptions>(options =>
{
//To handle ModelState Errors manually as ApiController attribute handles those automatically
//and return its own response.
options.SuppressModelStateInvalidFilter = true;
});
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
services.AddControllers(options =>
{
//To accept browser headers.
options.RespectBrowserAcceptHeader = true;
}).
AddNewtonsoftJson(options =>
{
// Use the default property (Pascal) casing
options.SerializerSettings.ContractResolver = new DefaultContractResolver();
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
}).
AddJsonOptions(options =>
{
//Not applying any property naming policy
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.IgnoreNullValues = true;
}).
AddXmlSerializerFormatters().
AddXmlDataContractSerializerFormatters();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCors("CorsPolicy");
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
//Configuring serilog
loggerFactory.AddSerilog();
}
}
I tried testing the same API with the OPTIONS method from POSTMAN. It is also giving the Http Status Code as 405. But when trying to access the same request using the POST method, I received the response successfully.
Is there anything wrong with the above code or something wrong with the order of middlewares being called in Configure().
Try to add extension method and modifying your startup class:
Extension method:
public static void AddApplicationError(this HttpResponse response, string
message)
{
response.Headers.Add("Application-Error", message);
response.Headers.Add("Access-Control-Expose-Headers", "Application-Error");
response.Headers.Add("Access-Control-Allow-Origin", "*");
}
Startup.cs :
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(builder =>
{
builder.Run(async context =>
{
context.Response.StatusCode = (int)
HttpStatusCode.InternalServerError;
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
context.Response.AddApplicationError(error.Error.Message);
await context.Response.WriteAsync(error.Error.Message);
}
});
});
}
P.S. in my case I had scenario also returning 405 status error, cause was, similar action methods I used and there are conflicted
For ex:
[HttpGet]
public ActionResult GetAllEmployees()
[HttpGet]
public ActionResult GetCustomers()
Hope this will help at least to show exact error message
You need to add Cors in Startup.cs file under your web api project
add this variable in Startup.cs
readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
add services.AddCors before services.AddControllers() in the method ConfigureServices in file Startup.cs:
services.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://localhost:4000",
"http://www.yourdomain.com")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddControllers();
*** You can pass only * to allow all instead of passing http://localhost:4000","http://www.yourdomain.com in the WithOrigins method
add app.UseCors before app.UseAuthentication() in the method Configure in file Startup.cs:
app.UseCors(MyAllowSpecificOrigins);
Check this Microsoft help
Try this:
app.UseCors(policy =>
policy.WithOrigins("https://localhost:PORT", "https://localhost:PORT")
.AllowAnyMethod()
.WithHeaders(HeaderNames.ContentType)
);

Unable to resolve service for type IEmailSender while attempting to activate RegisterModel

I'm using Identity and I have a problem that I make a new example project and with individual authentication and scaffold identity
InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Identity.UI.Services.IEmailSender' while attempting to activate 'MASQ.Areas.Identity.Pages.Account.RegisterModel'.
I am using ASP.NET Core 3.0 and had similar issue. I added the following .AddDefaultUI() to my Startup.cs & it worked.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders()
.AddDefaultUI()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddControllersWithViews();
services.AddRazorPages().AddRazorRuntimeCompilation();
}
There're two ways to do that :
remove the services.AddDefaultTokenProviders() in the ConfigurureServices() to disable two-factor authentication (2FA) :
// file: `Startup.cs` :
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
///.AddDefaultTokenProviders(); /// remove this line
Add your own IEmailSender and ISmsSender implementation to DI contianer if you would like to enable 2FA
// file: `Startup.cs`
services.AddTransient<IEmailSender,YourEmailSender>();
services.AddTransient<IEmailSender,YourSmsSender>();
Edit:
Both should work.
Both should work for ASP.NET Core 2.1. However, as of ASP.NET Core 3.0, the first approach doesn't work any more.
Add Default UI in the configuration service:
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddDefaultUI();
For ASP.NET Core 5.0 you can use the following code, instead of calling AddIdentity
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
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)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, ApplicationRole>(
option => {
option.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
option.Lockout.MaxFailedAccessAttempts = 5;
option.Lockout.AllowedForNewUsers = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
//services.AddDbContext<ApplicationDbContext>(options =>
// options.UseSqlServer(
// Configuration.GetConnectionString("DefaultConnection")));
//services.AddIdentity<ApplicationUser, IdentityRole>()
// .AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
services.AddTransient<Areas.Identity.Services.IEmailSender, AuthMessageSender>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// 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();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
maybe it's my time to be usefull. Since approach with green checkmark doesn't work in core 3.0 I have an idea. Basicaly you need to disable IEmailSender service. I think that not the best aproach, but if you dont need IEmailSender and you just want to quickly setup user functionality you can go to Register.cshtml.cs
and comment out (or delete) private readonly IEmailSender _emailSender; and everywhere it is used in this controller. Hope it helps.

How to ConfigureServices Authentication based on routes in ASP.NET Core 2.0

In ASP.NET Core 1.x I could use authentication methods in Configure but now in ASP.NET Core 2.0 I have to set everything in ConfigureServices and can't configure it in Configure method. For example
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
.AddCookie()
.AddXX();
}
and then in
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
....
app.UseAuthentication();
}
in the past, I could use something like
app.UseOpenIdConnectAuthentication();
and I can't configure it anymore like this.
so how I can use something like this now in ASP.NET Core 2.0?
app.Map(new PathString("/MyPath"), i => i.UseMyAuthMethod());
In 2.0, the best option to do per-route authentication is to use a custom IAuthenticationSchemeProvider:
public class CustomAuthenticationSchemeProvider : AuthenticationSchemeProvider
{
private readonly IHttpContextAccessor httpContextAccessor;
public CustomAuthenticationSchemeProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<AuthenticationOptions> options)
: base(options)
{
this.httpContextAccessor = httpContextAccessor;
}
private async Task<AuthenticationScheme> GetRequestSchemeAsync()
{
var request = httpContextAccessor.HttpContext?.Request;
if (request == null)
{
throw new ArgumentNullException("The HTTP request cannot be retrieved.");
}
// For API requests, use authentication tokens.
if (request.Path.StartsWithSegments("/api"))
{
return await GetSchemeAsync(OAuthValidationDefaults.AuthenticationScheme);
}
// For the other requests, return null to let the base methods
// decide what's the best scheme based on the default schemes
// configured in the global authentication options.
return null;
}
public override async Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultAuthenticateSchemeAsync();
public override async Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultChallengeSchemeAsync();
public override async Task<AuthenticationScheme> GetDefaultForbidSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultForbidSchemeAsync();
public override async Task<AuthenticationScheme> GetDefaultSignInSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultSignInSchemeAsync();
public override async Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultSignOutSchemeAsync();
}
Don't forget to register it in the DI container (ideally, as a singleton):
// IHttpContextAccessor is not registered by default
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IAuthenticationSchemeProvider, CustomAuthenticationSchemeProvider>();
The Microsoft docs say what to do if you want to use multiple authentication schemes in ASP.NET Core 2+:
The following example enables dynamic selection of schemes on a per
request basis. That is, how to mix cookies and API authentication:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
// For example, can foward any requests that start with /api
// to the api scheme.
options.ForwardDefaultSelector = ctx =>
ctx.Request.Path.StartsWithSegments("/api") ? "Api" : null;
})
.AddYourApiAuth("Api");
}
Example:
I had to implement a mixed-authentication solution in which I needed Cookie authentication for some requests and Token authentication for other requests. Here is what it looks like for me:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
// if URL path starts with "/api" then use Bearer authentication instead
options.ForwardDefaultSelector = httpContext => httpContext.Request.Path.StartsWithSegments("/api") ? JwtBearerDefaults.AuthenticationScheme : null;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o =>
{
o.TokenValidationParameters.ValidateIssuerSigningKey = true;
o.TokenValidationParameters.IssuerSigningKey = symmetricKey;
o.TokenValidationParameters.ValidAudience = JwtSignInHandler.TokenAudience;
o.TokenValidationParameters.ValidIssuer = JwtSignInHandler.TokenIssuer;
});
where the JWT Bearer authentication is implemented as described in this answer.
Tips:
One of the biggest 'gotchas' for me was this: Even though the Cookies Policy forwards requests with URLs that start with "/api" to the Bearer policy, the cookie-authenticated users can still access those URLs if you're using the [Authorize] annotation. If you want those URLs to only be accessed through Bearer authentication, you must use the [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] annotation on the API Controllers/Actions.