Secure External APIs with API Keys - and Keep the Villains at Bay



Suppose you've written a web app and exposed an external REST API based on Swagger and Swashbuckle in an ASP.Net Core site.  Perhaps you blindly followed some rando's recent blog post about it, published it to production, and your customers are super happy except: Horrors, you've just realized you forgot to secure it and now it's open to all the villains of the Internet!  Worse, your customers don't want to use their existing credentials, they want something called API Keys.  

Good news: you've just stumbled on said rando's follow-up blog post about how to secure those external web API's, and even better it's got diagrams, detailed videos, and a whole working Pull Request in a sample GitHub repo.  

One caveat: this post is fairly specific to the security model in the ASP.Net Boilerplate framework, but even if you're not using that framework, this technique should be generally applicable.

What Are API Keys?

The idea of API Keys is fairly standard with systems that offer an external API.  They offer customers a way to connect to an API with credentials that are separate from their own.  More importantly API Keys offer limited access, a critical element of a good security model.  API Keys can't, for instance, log in to the site and create new API Keys, or manage users.  They can only perform the limited actions that the external Web API grants.  If the credentials become compromised, users can reject or delete them without affecting their own credentials.


Data Structure

The first step in implementing API Keys is to design the data structure and build out the CRUD for the UI.  Well, the CRD, anyway, updating API Keys doesn't make much sense.

Conveniently enough ASP.Net Boilerplate already provides a Users entity that ostensibly offers a superset of everything required.  ASP.Net Boilerplate Users can have multiple Roles and each of those Roles can have multiple Permissions.  

 

Developers can then restrict access to methods and classes via the AbpAuthorize("SomePermission") attribute, which takes a Permission as a parameter.  When an end user makes a request to an endpoint ABP determines the user from a bearer token in the request header, figures out their roles, and figures out which permissions belong to those roles.  If one of those permissions matches the requirement in the AbpAuthorize() attribute, the call is allowed through.

API Keys should be similar.  As far as fields they'll have an "API Key" instead of "Username", and a "Secret" instead of a "Password".  Unlike users they'll likely only need one permission for decorating the external API instead of many.  Thus, they'll have just a single Role to help link the single permission to the API Keys.

Therefore, implementing this should be as simple as having ApiKey inherit from User and pretend that Username is ApiKey and Password is Secret.



Crudy Keys

Last month this rando also published an ABP CRUD Cheat Sheet to help build out CRUD for new entities in ASP.Net Boilerplate apps.  Following that guide for creating API Keys would be a great place to start.  I'll include the original step and any customizations below.  Also, if you want to follow along visually this dude also posted a video of the process including explaining topics like Table-Per-Hierarchy inheritance and published it as an episode to something called Code Hour (to which you should totally subscribe):


A1. Add Entity

The ApiKey contains zero additional columns:

public class ApiKey : User
{
}

A2. Add to DB Context

A3. Add Migration

The addition of inheritance to the User entity means Entity Framework will add a discriminator column because Entity Framework Core prefers the  Table per Hierarchy inheritance strategy. It will take care of setting that discriminator column for all future data, but it won't for older data.  To address that add either a second migration or add this to the first migration:

migrationBuilder.Sql("UPDATE AbpUsers SET Discriminator = 'User'");

A4. Update Database

A5. Add DTO

The only field we want to expose when viewing API Keys is the key (username).  We want a custom mapping from Username to ApiKey when AutoMapper converts it.  Therefore we want a DTO like this:

public class ApiKeyDto : EntityDto<long>
{
    [Required]
    [StringLength(AbpUserBase.MaxUserNameLength)]
    public string ApiKey { get; set; }
}

And a mapping profile like this:

public class ApiKeyDtoProfile : Profile
{
    public ApiKeyDtoProfile()
    {
        CreateMap<ApiKey, ApiKeyDto>()
            .ForMember(i => i.ApiKey, opt => opt.MapFrom(i => i.UserName));
    }
}

A6. Register a Permission

context.CreatePermission(PermissionNames.Pages_ApiKeys, L("ApiKeys"), multiTenancySides: MultiTenancySides.Host);

A7. AppService

The AppService has three things of note.  

First, the Create DTO is different from the View DTO, because users send in a Secret but never get one back.  We can solve that by making a CreateApiKeyDto that inherits from the view DTO and customizes the final generic parameter

[AutoMapFrom(typeof(ApiKey))]
public class CreateApiKeyDto : ApiKeyDto
{
    [Required]
    [StringLength(AbpUserBase.MaxPlainPasswordLength)]
    public string Secret { get; set; }
}

[AbpAuthorize(PermissionNames.Pages_ApiKeys)]
public class ApiKeysAppService : AsyncCrudAppService<ApiKey, ApiKeyDto, long, PagedAndSortedResultRequestDto, CreateApiKeyDto>
{
   ...

Second, we need a custom method to retrieve unique API Keys and Secrets.  A quick naive version would look like this:

public CreateApiKeyDto MakeApiKey()
{
    return new CreateApiKeyDto
    {
        ApiKey = User.CreateRandomPassword(),
        Secret = User.CreateRandomPassword()
    };
}

Finally, when we create ApiKeys we need to put in some fake value in the various required user fields, but most importantly we need to hash the secret with an IPasswordHasher<User>.

public override async Task<ApiKeyDto> CreateAsync(CreateApiKeyDto input)
{
    var fakeUniqueEmail = input.ApiKey + "@noreply.com";

    var apiKey = new ApiKey
    {
        UserName = input.ApiKey,
        EmailAddress = fakeUniqueEmail,
        Name = "API Key",
        Surname = "API Key",
        IsEmailConfirmed = true,
        NormalizedEmailAddress = fakeUniqueEmail
    };

    apiKey.Password = _passwordHasher.HashPassword(apiKey, input.Secret);

    await _userManager.CreateAsync(apiKey);

    var apiRole = await _roleService.EnsureApiRole();
    await _userManager.SetRolesAsync(apiKey, new[] { apiRole.Name });

    await CurrentUnitOfWork.SaveChangesAsync();

    return new ApiKeyDto
    {
        Id = apiKey.Id,
        ApiKey = apiKey.UserName
    };
}

That call to _roleService.EnsureApiRole() basically just creates a Role and a Permission that we can decorate our external API calls with.

/// <summary>
/// ApiKeys should have the API permission.  Users/ApiKeys must get permissions by association with a Role.
/// This code finds and returns a role called API or if it doesn't exist it creates and returns a role
/// called API that has the API permission.  This code is called when an API Role is created.  Thus, the API
/// role is created the 1st time a user creates an API Key for a tenant.
/// </summary>
public async Task<Role> EnsureApiRole()
{
    var apiRole = await _roleRepository.GetAll().FirstOrDefaultAsync(i => i.Name == RoleNames.Api);
    if (apiRole != null) return apiRole;

    var permissions = _permissionManager.GetAllPermissions().Where(i => i.Name == PermissionNames.Api);
    apiRole = new Role
    {
        TenantId = CurrentUnitOfWork.GetTenantId(),
        Name = RoleNames.Api,
        DisplayName = "API"
    };
    await _roleManager.CreateAsync(apiRole);
    await _roleManager.SetGrantedPermissionsAsync(apiRole, permissions);
    return apiRole;
}

A8. Run App, See Swagger Update, Rejoice

Great, the API is finished:


B1. Update nSwag

B2. Register Service Proxy

B3. Update Left-Hand Nav

B4. Duplicate Tenant Folder and Find/Replace "Tenant" with "[Entity]" and "tenant" with "[entity]"

B5. Update Route

B6. Register New Components in app.module.ts

B7. Fix Fields, Customize ... Rejoice?

With the back-end work finished, the front-end is fairly straightforward.  Just delete the edit dialog and in the create dialog to set the DTO on init and pick up the random passwords:

public ngOnInit() {
    this.apiKey = new CreateApiKeyDto();
    this.apiKeyServiceProxy
        .generateApiKey()
        .first()
        .subscribe(apiKey => (this.apiKey = apiKey));
}

And add a "Copy To Clipboard" button:

<mat-form-field class="col-sm-12">
    <input matInput type="text" name="Key" [(ngModel)]="apiKey.apiKey" required placeholder="Key" readonly #keyInput />
    <button mat-icon-button matSuffix type="button" (click)="copyInputToClipboard(keyInput)">
        <mat-icon matTooltip="Copy to clipboard">content_copy</mat-icon>
    </button>
</mat-form-field>
public copyInputToClipboard(inputElement: HTMLInputElement) {
    inputElement.select();
    document.execCommand('copy');
    inputElement.setSelectionRange(0, 0);
}

And now users can add API Keys:



But API users still aren't authenticating with those API Keys.

ClientTokenAuthController

The existing SPA site authenticates a username, password and tenant by calling into the TokenAuthController class.  If the credentials check out that class returns an access token that the client appends to subsequent requests.

The Web API should do the same thing.  Mostly just copy and paste the existing TokenAuthController class into a ClientTokenAuthController and put it in the external API folder (\Client\V1 from my previous blog post).  The main customization is that the ClientTokenAuthController should take an ApiKey and Secret, and it should not take a tenant (if multi-tenancy is enabled) because the ApiKey is unique across tenants.

The full code for the ClientTokenAuthController is in this commit, but the relevant part looks like this:

/// <summary>
/// Authenticates an API Key and Secret.  If successful AuthenticateResultModel will contain a token that
/// should be passed to subsequent methods in the header as a Bearer Auth token.
/// </summary>
/// <param name="model">Contains the API Key and Secret</param>
/// <returns>The authentication results</returns>
[HttpPost("api/client/v1/tokenauth", Name = nameof(Authenticate))]
[ProducesResponseType(typeof(AuthenticateResultModel), 200)]
[DontWrapResult(WrapOnError = false, WrapOnSuccess = false, LogError = true)]
public async Task<IActionResult> Authenticate([FromBody] ClientAuthenticateModel model)
{
    /*
     * This 1st Authenticate() looks only in ApiKeys, which are assumed to be unique across Tenants (unlike Users),
     * thus we can pull back a TenantId on success and set the session to use it
     */
    var apiKeyAuthenticationResult = await _apiKeyAuthenticationService.Authenticate(model.ApiKey, model.Secret);

    if (!apiKeyAuthenticationResult.Success)
    {
        // this 401 is much cleaner than what the regular TokenAuthController returns.  It does a HttpFriendlyException which results in 500 :|
        return new UnauthorizedObjectResult(null);
    }

    using (_session.Use(apiKeyAuthenticationResult.TenantId, null))
    {
        /*
         * This 2nd Authenticate is almost entirely guaranteed to succeed except for a few edge cases like if the
         * tenant is inactive.  However, it's necessary in order to get a loginResult and create an access token.
         */
        AbpLoginResult<Tenant, User> loginResult = await GetLoginResultAsync(
            model.ApiKey,
            model.Secret,
            GetTenancyNameOrNull()
        );

        return new OkObjectResult(CreateAccessToken(loginResult));
    }
}

And after placing that class in the Client.V1 namespace, the SwaggerFileMapperConvention (from Mastering External API's) will expose it in Swagger


Authenticating The External API

Finally, the last step is to lock down the existing method and ensure (ala TDD) that client's can't get in.  If you're a visual learner this section along with details about password hashing and AutoMapper details is covered in Code Hour Episode 29:


With that shameful self-promotion out of the way, next register an API Permission.  Unlike the earlier permission this one is for accessing API endpoints, not managing API Keys:


context.CreatePermission(PermissionNames.Api, L("Api"), multiTenancySides: MultiTenancySides.Tenant);

Now restrict the external API's controller with that permission:

[AbpAuthorize(PermissionNames.Api)]
public class ProductController : LeesStoreControllerBase

Now if when clients hit the /api/client/v1/product endpoint, they get an HTTP 401.  Excellent!

To authenticate with an API Key and Password call into ClientTokenAuthController with an API Key and Secret and save the token onto the ClientApiController:

var authenticateModel = new ClientAuthenticateModel
{
    ApiKey = apiKey,
    Secret = secret
};
var authenticateResultModel = await clientApiProxy.AuthenticateAsync(authenticateModel);

clientApiProxy.AccessToken = authenticateResultModel.AccessToken;

var product = await clientApiProxy.GetProductAsync(productId);

What's that?  You're following along and it didn't compile?!  No AccessToken property on ClientApiProxy?!  Oh, that's easy!  Just use the fact that the NSwag generated proxy is partial and has a PrepareRequest partial method:

partial class ClientApiProxy
{
    public string AccessToken { get; set; }

    partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url)
    {
        if (AccessToken != null)
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
        }
    }
}

Huzzah!  Now check out that happy 200:


🎉 We Did It!!

If you made it to the end of this post I am deeply impressed.  I hope it was useful.  The code is at the Api Keys Pull Request in my ASP.Net Demo Site.  If you found this helpful or have a better approach please let me know @twitter or in the comments.  Happy coding!

Comments