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.
DefaultPolicy
orPolicy
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).