Implementing AbpIdentity cookies and JwtBearer - c#

I inherited an ASPnetzero application that uses both a Web API and a MVC front-end. The API authenticated via Bearer and the front-end via AbpIdentity (Cookies). A couple of days ago I got brave and decided to update my nuGet packages. The update came with an upgrade from .netCore v1 to v2. But I had some difficulties with the authentication after the JwtBearer middleware became obsolete. I could authenticate using the cookies, but not using Bearer Tokens.
I tried almost everything. Using multiple authentication methods meant that only one worked at a time.
In Startup.cs I had the following (snippets):
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddAbpIdentity<Tenant, User, Role>()
.AddUserManager<UserManager>()
.AddRoleManager<RoleManager>()
.AddSignInManager<SignInManager>()
.AddClaimsPrincipalFactory<UserClaimsPrincipalFactory>()
.AddDefaultTokenProviders();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
AuthConfigurer.Configure(app, _appConfiguration);
}
This is however a self-answer question and I'm hoping that I'll help anyone with similar or equal cases, since I've put together my own solution. The idea was to get the application working with a Bearer token (only when using the API) and cookies (only when using the MVC).
I also had a challenge as the MVC did XHR calls to the API to get data to display on the front-end. This meant that the API also needed to work with Cookies (but only for MVC users).

So I finally figured it out and it required quite a bit of transformation. The result was that:
API users only authenticated with a Bearer Token
MVC users authenticated with Cookies and the same authentication was used for the API calls in the application after they logged in.
All of the changes were made in Startup.cs, and I also commented out the reference to the AuthConfigure.cs file, which is now obsolete. I am open to any improvements or suggestions to the solution.
The important pieces in the Startup.cs file:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
// The application URL is used for the Issuer and Audience and is included in the appsettings.json
ValidIssuer = _appConfiguration["Authentication:JwtBearer:Issuer"],
ValidAudience = _appConfiguration["Authentication:JwtBearer:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appConfiguration["Authentication:JwtBearer:SecurityKey"]))
};
});
// Activate Cookie Authentication without Identity, since Abp already implements Identity below.
services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/Login");
// Add the Authentication Scheme Provider which will set the authentication method based on the kind of request. i.e API or MVC
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IAuthenticationSchemeProvider, CustomAuthenticationSchemeProvider>();
// Some of these extensions changed
services.AddAbpIdentity<Tenant, User, Role>()
.AddUserManager<UserManager>()
.AddRoleManager<RoleManager>()
.AddSignInManager<SignInManager>()
.AddClaimsPrincipalFactory<UserClaimsPrincipalFactory>()
.AddDefaultTokenProviders();
//…
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// app.UseAuthentication is critical here
app.UseAuthentication();
app.UseAbp(); //Initializes ABP framework.
app.UseCors("CorsPolicy");
//…
//AuthConfigurer.Configure(app, _appConfiguration);
//…
}
}
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.
var authHeader = httpContextAccessor.HttpContext.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader?.StartsWith("Bearer ") == true)
{
return await GetSchemeAsync(JwtBearerDefaults.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();
}

Related

Preventing AuthenticationState from removing claims added to ClaimsPrincipal

I am working on a Blazor Server application that uses Window Authentication through IIS and I'm having issues with updating the current Claims Principal with custom claims. Once the app has authenticated the user I have planned to use the authentication information to check against Active Directory and assign claims based on their AD Group Memberships.
I set up an IClaimsTransformation Service and while the TransformAsync Task is called successfully, each time it is called the ClaimsPrincipal has been reverted back to the original WindowsIdentity and any changes are lost. As long as everything is reapplied the claims do work after the last time transformAsync runs but I would like to see if there is a way to persist the changes to avoid the need to constantly reauthorize or manage a separate user object to store .
I have tried the following
Setting the service as both a scoped and a singleton
Modifying the WindowIdentity provided with the initial authentication
adding an additional claims identity to the ClaimsPrincipal
adding Claims as CI.CustomRoleType as well as just setting the Type and Value
My IClaimsTransormation Class:
public class UserAuthorizationService : IClaimsTransformation {
private readonly IConfiguration _configuration;
public UserAuthorizationService(IConfiguration configuration) {
_configuration = configuration;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) {
var claimsIdentity = new ClaimsIdentity();
if (!principal.HasClaim("TestClaim", "Test")) {
Claim customClaim = new Claim("TestClaim", "Test");
claimsIdentity.AddClaim(customClaim);
}
principal.AddIdentity(claimsIdentity);
return Task.FromResult(principal);
}
}
Related Startup class configuration:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = IISDefaults.AuthenticationScheme;
});
services.AddAuthorization(config => {
config.AddPolicy("TestPolicy", policy => policy.RequireClaim("TestClaim", "Test"));
});
services.AddScoped<IClaimsTransformation, UserAuthorizationService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
}

Asp.Net Core 3 Identity - Custom Claims not present in JWT from browser

Asp.Net Core 3.0
I am using the ASP.NET Core web application with Angular and Authentication (Individual User Accounts) template (from Visual Studio 2019).
My intention is to add some Custom Claims in the generated JWT and use them in browser.
In order to do that, I have extended the UserClaimsPrincipalFactory
public class MyCustomClaimsInjector : UserClaimsPrincipalFactory<ApplicationUser>
{
public MyCustomClaimsFactory(UserManager<ApplicationUser> userManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, optionsAccessor)
{
}
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
{
var id = await base.GenerateClaimsAsync(user);
id.AddClaim(new Claim("my_claim1", "AdditionalClaim1"));
id.AddClaim(new Claim("my_claim2", "AdditionalClaim2"));
return id;
}
}
As well, I have registered the extension in the Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddClaimsPrincipalFactory<MyCustomClaimsFactory>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddRazorPages();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
During Sign In phase, started from the SPA client, the debugger passes through MyCustomClaimsFactory and adds the claims to the ClaimsIdentity in the GenerateClaimsAsync method.
But, I find strange why the JWT received in browser does not contain the Claims added by the MyCustomClaimsFactory.
Is my expectation to see the Custom Claim in the JWT in browser OK ?
Can anyone suggest the direction to dig in... Why the claims isn't present in the JWT ?
Decoded JWT is:
The SPA app:
Will share my results. Hope that will help anyone else.
I have implemented IProfileService and piped the .AddProfileService<ProfileService>()implementation in ConfigureServices.
public class ProfileService : IProfileService
{
protected UserManager<ApplicationUser> _userManager;
public ProfileService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = await _userManager.GetUserAsync(context.Subject);
var claims = new List<Claim>
{
new Claim("my_FirstName", "user_FirstName"),
new Claim("my_LastName", "user_LastName")
};
context.IssuedClaims.AddRange(claims);
}
public async Task IsActiveAsync(IsActiveContext context)
{
var user = await _userManager.GetUserAsync(context.Subject);
context.IsActive = (user != null);
}
}
the Startup.cs file
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
.AddProfileService<ProfileService>();
With that, now the JWT contains my custom claims.
I am not sure why the override for UserClaimsPrincipalFactory was not able to solve that.
Will try to study deeper those areas.
So today I encountered the same problem. I haven't used IdentityServer before so I'm not totally sure if the following is the correct way of doing it, but it is certainly easier than creating your own IProfileService implementation.
The default profile service calls
context.AddRequestedClaims(principal.Claims);
The requested claims are defined on the IdentityServer ApiResource that you are accessing. This ApiResource configuration is created by calling
services.AddAuthentication()
.AddIdentityServerJwt();
This creates a default ApiResource configuration with the name "{Environment.ApplicationName}API".
This object also holds a collection of claim types to include when generating the JWT.
I couldn't find any documentation on whether this could also be set in the appsettings.json, but you can access it in the startup code.
TLDR; Change your startup code to something similar to this:
Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
{
Configuration = configuration;
Environment = environment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; }
// ...
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddIdentityServer()
.AddApiAuthorization<User, ApplicationDbContext>(options =>
{
// The options.ApiResources collection is automatically populated
// by services.AddAuthentication().AddIdentityServerJwt();
var apiResource = options.ApiResources[$"{Environment.ApplicationName}API"];
// Example: add the user's roles to the token
apiResource.UserClaims.Add(JwtClaimTypes.Role);
// Example: add another custom claim type
apiResource.UserClaims.Add("CustomClaimName");
});
services.AddAuthentication()
.AddIdentityServerJwt();
// ...
}
}

AspnetCore Multiple authentication and use of AddAuthorization/AddAuthentication/UseAuthentication

I'm try to learn ASPNetCore 2.2
I don't understand how authentication chains works.
Example: Startup.cs
public void ConfigureServices(IServiceCollection services) {
services.AddMvcCore()
.AddAuthorization(); // Note 1
services.AddAuthentication(options => { // Note 2
options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme;
options.DefaultChallengeScheme = ApiKeyAuthenticationOptions.DefaultScheme;})
.AddJwtBearer(x => {
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = tvp;})
.AddApiKeySupport(options => { });
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
app.UseStatusCodePages()
.UseResponseCompression()
//.UseAuthentication() // NOTE 3
.UseMvc();
}
ApiKeyHandler.cs
public class ApiKeyHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
log.Debug("Checking API key");
// No API KeyProvided. Pass to next auth handler (JWT)
if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeaderValues)) {
return AuthenticateResult.NoResult();
}
if (checkAuthHeader(foo)) {
ticket = createTicket(foo);
return AuthenticateResult.Success(ticket);
}
}
TestController.cs
[Authorize]
[ApiController]
public class TestApiController : Controller {
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme + "," + ApiKeyAuthenticationOptions.DefaultScheme)]
[HttpGet("api-jwt")]
public IActionResult APIAndJWT() {
var message = $"API and JWT !!! {nameof(APIAndJWT)}";
return new ObjectResult(message);
}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet("only-jwt")]
public IActionResult OnlyJWT() {
var message = $"JWT {nameof(OnlyJWT)}";
return new ObjectResult(message);
}
[Authorize(AuthenticationSchemes = ApiKeyAuthenticationOptions.DefaultScheme)]
[HttpGet("only-api")]
public IActionResult OnlyAPI() {
var message = $" API {nameof(OnlyAPI)}";
return new ObjectResult(message);
}
}
Ok, TestApi is very simple. I want to call some endpoint using JWT auth or an API Key. Other endpoints can be called with both auth.
There are three methods. One use only JWT auth, one only API auth, and another that can use JWT or API auth.
I don't know how works those methods:
AddAuthorization() // Note 1
AddAuthentication(...) // Note 2
UseAuthentication() // Note 3
Well, without AddAuthorization() it seems that my [Authorize] attribute wont used, so I have free access to all API. BAD. On MSDN AddAuthorization() seems to enable policy that I don't use.
I must use AddAuthorization() for managing simple access to API?
AddAuthentication() it's easy. It just configure my auth handlers (JWT and a custom API handler)
UseAuthentication here is the problem. I think that enables the middlewares added with AddAuthentication(...) // Note 2
My question: it's correct that each route create a new instance of ApiKeyHandler?
For example in route /only-jwt I specify only JWT authschema, but ApiKeyHandler is always created/called.
REMOVING UseAuthentication() instead do same results, ie authenticated routes, but without creating useless ApiKeyHandler when not required (/only-jwt).
What's the correcy way?
I think that the order AddMvc() / AddAuthorization() - AddMvc / UseAuthentication() is correct.
Resolved by me.
AddAuthorization() is needed when you use UseMvcCore() because isn't automatically like UseMVC()
There was a bug in AspNetCore 2.2
UseAuthentication() use the standard auth pipeline so each AuthMiddleware is created and chained with others.

Azure AD Authorize Error of AuthenticationFailed: IDX10501: Signature validation failed. Unable to match 'kid'

I have 2 projects, one is for API and 2nd is for to view and get data by using 1st project's APIs. For both I have implemented Azure AD authentication but my issue is when try to call API from 2nd project back-end(C#) with Bearer token but in response i am getting below error.
Note: I have use [Authorize] Filter in each and every class and the method in both project.
Error
AuthenticationFailed: IDX10501: Signature validation failed. Unable to match 'kid': 'SSQdhI1cKvhQEDSJxE2gGYs40Q0',
token: '{"alg":"RS256","typ":"JWT","kid":"SSQdhI1cKvhQEDSJxE2gGYs40Q0"}.{"aud":"1e615ddb-ad4d-4e65-98de-c6f5db1ae08a","iss":"https://login.microsoftonline.com/5c58f0d9-2f98-4eb0-91f2-ec6afd4242f8/v2.0","iat":1518520450,"nbf":1518520450,"exp":1518524350,"aio":"Y2NgYHiknfZAIvPElucJpgeZzRa5AAA=","azp":"1e615ddb-ad4d-4e65-98de-c6f5db1ae08a","azpacr":"1","e_exp":262800,"oid":"159f0ec6-c5b9-4bfc-88d0-77924bd40b3f","sub":"159f0ec6-c5b9-4bfc-88d0-77924bd40b3f","tid":"5c58f0d9-2f98-4eb0-91f2-ec6afd4242f8","uti":"t8CU6YtsHE-5M9TbQm4aAA","ver":"2.0"}'.
Start class ConfigureServices Method
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAdB2C(options => Configuration.Bind("Authentication:AzureAdB2C", options))
.AddCookie();
// Add framework services.
services.AddMvc();
// Adds a default in-memory implementation of IDistributedCache.
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(1);
options.CookieHttpOnly = true;
});
}
Start class Configure method
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseSession();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Class for to get Bearer Token
public class ServicePrincipal
{
static string authority = "https://login.microsoftonline.com/{TenantID}/{Policy}/v2.0/";
static string clientId = "XXX";
static string clientSecret = "XXX";
static string resource = "XXX";
static public async Task<Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult> GetS2SAccessTokenForProdMSAAsync()
{
return await GetS2SAccessToken(authority, resource, clientId, clientSecret);
}
static async Task<Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult> GetS2SAccessToken(string authority, string resource, string clientId, string clientSecret)
{
var clientCredential = new Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential(clientId, clientSecret);
AuthenticationContext context = new AuthenticationContext(authority, false);
Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult authenticationResult = await context.AcquireTokenAsync(resource,clientCredential);
return authenticationResult;
}
}
Controller method from where I am trying to calling 1st project's API
public async Task<IActionResult> GetCustomerGroupAsync()
{
try
{
var token = await ServicePrincipal.GetS2SAccessTokenForProdMSAAsync();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
HttpResponseMessage response = await _client.GetAsync("http://localhost:49942/api/customermodule/v0.3/customergroup");
response.EnsureSuccessStatusCode();
string receiveStream = await response.Content.ReadAsStringAsync();
return null;
}
catch (Exception e)
{
return Json(new { Error = e.Message });
}
}
Please let me know if I am missing anything or am i doing wrong?, thank you.
Based on your GetCustomerGroupAsync action, you are using the client credentials flow in your website backend to access the secured resource (Web API) with Azure AD B2C. As Azure Active Directory B2C: Types of applications mentions under the Current limitations section as follows:
Daemons/server-side apps
Apps that contain long-running processes or that operate without the presence of a user also need a way to access secured resources such as web APIs. These apps can authenticate and get tokens by using the app's identity (rather than a user's delegated identity) and by using the OAuth 2.0 client credentials flow.
This flow is not currently supported by Azure AD B2C. These apps can get tokens only after an interactive user flow has occurred.
Moreover, you could follow the git samples below to implement your requirement:
An ASP.NET Core 2.0 web API with Azure AD B2C
An ASP.NET Core web app with Azure AD B2C
Because you need to retrive access token, it must be a call to OAuth endpoint. Try with the below one to get Azure AD access token
https://login.microsoftonline.com/{TenantID}/oauth2/token
More over, just need to make sure the authorization header is constructed in valid format: Bearer

How do you Authorize a Web API Controller in ASP Net Core

I have created an API for a back-end in C# ASP Net Core. I am trying to figure our a way to authorize the Routes so that It will take in a API Key in the url such as "https://mywebsite.com/api/data/first?key=VX4HCOjtMQ6ZF978a245oLw00SfK0ahm" to authenticate the Route and present the data in JSON.
I know in ASP NET Core identity there is a way to authenticate the route but that requires the user to login first. How can I secure my API Routes with an API Key?
From the sounds of it what you are trying to achieve is an alternative Authentication system and a custom Authorization system that uses this key query string parameter (which is probably not the best design).
The first step would be to authenticate the user based on this QueryString parameter. Now the best way (IMO) is to roll your own authentication handler. Reviewing the code For Aspnet Security reveals the inner workings of some of their existing authentication systems.
Effectively what we will do is intercept the request early on validate the existence of this key and then authenticate the request.
Something below shows this basic system.
public class QueryStringAuthOptions : AuthenticationOptions
{
public const string QueryStringAuthSchema = "QueryStringAuth";
public const string QueryStringAuthClaim = "QueryStringKey";
public QueryStringAuthOptions()
{
AuthenticationScheme = QueryStringAuthSchema;
}
public string QueryStringKeyParam { get; set; } = "key";
public string ClaimsTypeName { get; set; } = "QueryStringKey";
public AuthenticationProperties AuthenticationProperties { get; set; } = new AuthenticationProperties();
}
public class QueryStringAuthHandler : AuthenticationHandler<QueryStringAuthOptions>
{
/// <summary>
/// Handle authenticate async
/// </summary>
/// <returns></returns>
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Request.Query.TryGetValue(Options.QueryStringKeyParam, out StringValues value) && value.Count > 0)
{
var key = value[0];
//..do your authentication...
if (!string.IsNullOrWhiteSpace(key))
{
//setup you claim
var claimsPrinciple = new ClaimsPrincipal();
claimsPrinciple.AddIdentity(new ClaimsIdentity(new[] { new Claim(Options.ClaimsTypeName, key) }, Options.AuthenticationScheme));
//create the result ticket
var ticket = new AuthenticationTicket(claimsPrinciple, Options.AuthenticationProperties, Options.AuthenticationScheme);
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
return Task.FromResult(AuthenticateResult.Fail("Key not found or not valid"));
}
}
Now the above is pretty straight forward we have created a custom AuthenticationOptions class that we will use in our custom AuthenticationHandler. As you see this is very straight forward but in the end we are creating a valid Authentication Ticket (ClaimsPrinciple) and responding with a Success result or Fail().
Next we need to get the Authentication system working within the .Net pipeline (note this is 1.2 as 2.0 has changed see Auth 2.0 Migration). This is done through AuthenticationMiddleware so as before we create our simple implementation of the middleware.
public class QueryStringAuthMiddleware : AuthenticationMiddleware<QueryStringAuthOptions>
{
public QueryStringAuthMiddleware(RequestDelegate next, IOptions<QueryStringAuthOptions> options, ILoggerFactory loggerFactory, UrlEncoder encoder)
: base(next, options, loggerFactory, encoder)
{
}
protected override AuthenticationHandler<QueryStringAuthOptions> CreateHandler()
{
return new QueryStringAuthHandler();
}
}
This is really basic but just creates a new QueryStringAuthHandler() to handle the Authenticate request. (The one we created earlier). Now we need to get this middleware into the pipeline. So following the .Net convention a static extensions class can do this with the ability to manage the options.
public static class QueryStringAuthMiddlewareExtensions
{
public static IApplicationBuilder UseQueryStringAuthentication(this IApplicationBuilder appBuilder)
{
if (appBuilder == null)
throw new ArgumentNullException(nameof(appBuilder));
var options = new QueryStringAuthOptions();
return appBuilder.UseQueryStringAuthentication(options);
}
public static IApplicationBuilder UseQueryStringAuthentication(this IApplicationBuilder appBuilder, Action<QueryStringAuthOptions> optionsAction)
{
if (appBuilder == null)
throw new ArgumentNullException(nameof(appBuilder));
var options = new QueryStringAuthOptions();
optionsAction?.Invoke(options);
return appBuilder.UseQueryStringAuthentication(options);
}
public static IApplicationBuilder UseQueryStringAuthentication(this IApplicationBuilder appBuilder, QueryStringAuthOptions options)
{
if (appBuilder == null)
throw new ArgumentNullException(nameof(appBuilder));
if (options == null)
throw new ArgumentNullException(nameof(options));
return appBuilder.UseMiddleware<QueryStringAuthMiddleware>(Options.Create(options));
}
}
Right so far thats alot of code to get the Authentication system in place, however this is following many of the examples provided by the .net core team.
Final step for the Authentication middleware to work is to modify the startup.cs file and add the authentication systems.
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(); //adds the auth services
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseQueryStringAuthentication(); //add our query string auth
//add mvc last
app.UseMvc();
}
We are almost there, to this point we have our mechanisms for authenticating the request, and best we are creating claims (which can be extended) to hold more information if required. The final step is to Authorize the request. This is the easy bit, all we need to do is tell the default Authorization Handlers which sign in schema you are using, and in addition we will also require the claim we applied earlier on. Back in the ConfigureServices method in your startup.cs we simply AddAuthorization with some settings.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(o =>
{
//override the default policy
o.DefaultPolicy = new AuthorizationPolicy(new[] { new ClaimsAuthorizationRequirement(QueryStringAuthOptions.QueryStringAuthClaim, new string[0]) }, new[] { QueryStringAuthOptions.QueryStringAuthSchema });
//or add a policy
//o.AddPolicy("QueryKeyPolicy", options =>
//{
// options.RequireClaim(QueryStringAuthOptions.QueryStringAuthClaim);
// options.AddAuthenticationSchemes(QueryStringAuthOptions.QueryStringAuthSchema);
//});
});
services.AddAuthentication(o =>
{
o.SignInScheme = QueryStringAuthOptions.QueryStringAuthSchema;
}); //adds the auth services
services.AddMvc();
}
In the above snippet we have two options.
Override the DefaultPolicy or
Add a new Policy to the authorization system.
Now which option you use is up to you. Using the later option requires you to explicitly tell the Authorization handler which AuthorizationPolicy to use.
I suggest you read Custom Policy-Based Authorization to understand how these work.
To use this Authorization system (depending on your options above) you can simply decorate your controllers with the AuthorizeAttribute() (with policy name if you used the second option).
What you are trying to do will not secure the web api. I would recommend that you look into OAuth/OpenID. There is an open source .net core implementation called Identity Server 4.
However to answer your question you could create a custom attribute to validate the key being passed to your actions, or you could simply handle the validation in each action. There is no built in way to do this in .net core, you will have to manually handle the api key like any other value being passed to your web api.

Categories