1

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?

Rush B
  • 29
  • 1
  • 9

2 Answers2

1

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.

Dominik Holland
  • 368
  • 3
  • 6
1

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.

  1. Override the DefaultPolicy or
  2. 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).

Nico
  • 12,493
  • 5
  • 42
  • 62
  • This looks very promising I will definitely give it a shot. Why is this not a good design? I've come across many API's that use key's to validate the url based on a user. – Rush B Aug 21 '17 at 05:05
  • @RushB You are correct there are quite a few (or used to be) applications that embedded Api Keys into the Url. There has been a large shift to token authentication (such as JWT etc) that provide a more secure way of exchanging this data. Typically this is used in headers as opposed to query strings. Now by placing this in a URL this is a public place (yes SSL may hide) but is open to be distributed. If you use an authorization header then you can be sure that this information cannot be accidentally copy and pasted. – Nico Aug 21 '17 at 05:13
  • @RushB Just stumbled on this thread which makes some additional points. https://stackoverflow.com/questions/5517281/place-api-key-in-headers-or-url – Nico Aug 21 '17 at 05:14
  • I understand it is bad practice and you don't want the Key to be exposed in the url but what if I was only calling the url through network requests from an iOS application and the key would be hidden in my code. Would the network requests say through Alamofire still leave a url trail or some sort? – Rush B Aug 21 '17 at 05:25
  • Also I read the link you posted. Can I possibly use Core Identity to authorize the routes and provide username and password credentials in the header to valid the route? I already tried using it and it took me to the log-in page everytime instead of letting me access the data directly. – Rush B Aug 21 '17 at 05:27
  • What it sounds like is you are actually using a mixed set of Authentication and Authorization methods. So lets say you use forms authentication for your front end site then you invoke the Authorization Policies for directly for forms, then you can add another Authentication system (such as JWT) for your mobile application and use the policies for JWT for your api methods. .netcore will allow you to use multiple methods in one stack. IMO i would look at a some sort of JWT header authorization for your mobile app and keep the user credentials secured. – Nico Aug 21 '17 at 05:36