I’m building the foundation for an ASP.Net Core 2 site with Cosmos DB as the back-end store and want to build in the idea of user-manageable API keys. In the past two posts, I’ve added interactive registration and login to the application using built-in Cookie and Twitter middleware on top of custom authorization logic and Cosmos DB. In this one, we’ll be adding endpoints that require API Keys that can be created and revoked by the user.

3 Authentication Scenarios: User/Pass, Twitter, API Keys

3 Authentication Scenarios: User/Pass, Twitter, API Keys

While I started out with credentials stored directly in the LoginUser Document, in the prior post I decided to start treating authentication mechanisms as separate documents (my house keys are not a property of me).

Defining People (Users) Separate from House Keys (User Authentication)

Defining People (Users) Separate from House Keys (User Authentication)

In this post, that separation will start paying off, as it will allow us to add API key entries for the user and easily “revoke” them by switching their authentication type from “APIKey” to “RevokedAPIKey”, keeping the data available for audits but ensuring it’s no longer valid for API authentication.

Breaking it down

In the last two posts, we laid the groundwork for authentication from the UI down to Cosmos DB. With this post, we’re going to build a minimal screen to let users generate API Keys, some protected API endpoints, and the logic to tie these into the LoginUserAuthentications DocumentCollection in Cosmos DB.

From UI down to Cosmos DB back up to Middleware

From UI down to Cosmos DB back up to Middleware

Most of the work is building on the previous post, the big difference is a new way to authenticate and some middleware to do the work.

I worked on this in two pieces:

  • User Interface: Create/Revoke API keys in the UI down to Cosmos DB
  • Making API Calls: Protected API endpoints, Middleware, Membership business logic

The source code through this set of changes is available here: github: Sample_ASPNetCore2AndCosmosDB, Post #4 Branch

Task 1: User-managed API Keys

In this set of changes, we’re going to add some very basic screens to show the list of API Keys, add a new one, and revoke an existing one.

Why Revoke instead of Delete? I chose to revoke API Keys to help support audit logs and instrumentation down the road. This may be a case of YAGNI, but it was easy enough to implement and also tries a pattern I may refactor to using for passwords and password history.

UI Screens

Let’s start with the screens and work down the stack. I’ve created a new (and poorly named) UserController for the new screens.

SampleCosmosCore2App/Controllers/UserController.cs

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
[HttpGet("")]
public async Task<IActionResult> IndexAsync(string error)
{
    var sessionId = _membership.GetSessionId(HttpContext.User);
    var user = await _persistence.Users.GetUserBySessionIdAsync(sessionId);
    var auths = await _persistence.Users.GetUserAuthenticationsAsync(user.Id);
 
    // ... grouping logic to create model ...
   
    return View("Index", model);
}
 
[HttpGet("addKey")]
public IActionResult AddKey()
{
    var model = new NewKeyModel();
    return View("AddKey", model);
}
 
[HttpPost("addKey")]
public async Task<IActionResult> PostAddKeyAsync(NewKeyModel model)
{
    if (!ModelState.IsValid)
    {
        return View("AddKey", model);
    }
 
    var sessionId = _membership.GetSessionId(HttpContext.User);
    var user = await _persistence.Users.GetUserBySessionIdAsync(sessionId);
 
    var generatedKey = _membership.GenerateAPIKey(user.Id);
    var result = await _membership.AddAuthenticationAsync(user.Id, "APIKey", generatedKey, model.Name);
    var resultModel = // ... create the model ...
 
    return View("ShowKey", resultModel);
}
 
[HttpGet("revoke")]
public async Task<IActionResult> Revoke(string id)
{
    var sessionId = _membership.GetSessionId(HttpContext.User);
    var user = await _persistence.Users.GetUserBySessionIdAsync(sessionId);
 
    var result = await _membership.RevokeAuthenticationAsync(user.Id, id);
    if (result.Failed)
    {
        return RedirectToAction("IndexAsync", new { error = result.Error });
    }
    else
    {
        return RedirectToAction("IndexAsync");
    }
}
[HttpGet("")]
public async Task<IActionResult> IndexAsync(string error)
{
	var sessionId = _membership.GetSessionId(HttpContext.User);
	var user = await _persistence.Users.GetUserBySessionIdAsync(sessionId);
	var auths = await _persistence.Users.GetUserAuthenticationsAsync(user.Id);

	// ... grouping logic to create model ...
   
	return View("Index", model);
}

[HttpGet("addKey")]
public IActionResult AddKey()
{
	var model = new NewKeyModel();
	return View("AddKey", model);
}

[HttpPost("addKey")]
public async Task<IActionResult> PostAddKeyAsync(NewKeyModel model)
{
	if (!ModelState.IsValid)
	{
		return View("AddKey", model);
	}

	var sessionId = _membership.GetSessionId(HttpContext.User);
	var user = await _persistence.Users.GetUserBySessionIdAsync(sessionId);

	var generatedKey = _membership.GenerateAPIKey(user.Id);
	var result = await _membership.AddAuthenticationAsync(user.Id, "APIKey", generatedKey, model.Name);
	var resultModel = // ... create the model ...

	return View("ShowKey", resultModel);
}

[HttpGet("revoke")]
public async Task<IActionResult> Revoke(string id)
{
	var sessionId = _membership.GetSessionId(HttpContext.User);
	var user = await _persistence.Users.GetUserBySessionIdAsync(sessionId);

	var result = await _membership.RevokeAuthenticationAsync(user.Id, id);
	if (result.Failed)
	{
		return RedirectToAction("IndexAsync", new { error = result.Error });
	}
	else
	{
		return RedirectToAction("IndexAsync");
	}
}

IndexAsync uses ICustomMembership to get the current logged in user’s information, then uses a new UserPersistence method to get all available LoginUserAuthentications from Cosmos DB (which will include Twitter, revoked API Tokens, and more as we go along).

AddKey and PostAddKeyAsync display a form to create a new API Token and receive the POST, respectively. When we receive the POST, we rely on a new method in ICustomMembership to generate a token then use the existing method (built while adding Twitter) to store that token.

Revoke uses a new ICustomMembership method to revoke a given key (and I only just noticed I left off the conventional Async suffix, oops).

The AddKey view is a basic 1-field form (included more to show you I don’t have anything up my sleeves):

SampleCosmosCore2App/Views/User/AddKey.cshtml

HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@model SampleCosmosCore2App.Models.User.NewKeyModel
@{
    ViewData["Title"] = "AddKey";
    Layout = "~/Views/Shared/Layout.cshtml";
}
 
<div class="box">
    <h2>AddKey</h2>
 
    <form asp-action="AddKey">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-group">
            <label asp-for="Name" class="control-label"></label>
            <input asp-for="Name" class="form-control" />
            <span asp-validation-for="Name" class="text-danger"></span>
        </div>
        <div class="form-group">
            <input type="submit" value="Create API Key" class="btn btn-default" />
        </div>
    </form>
 
</div>
<div>
    <a asp-action="IndexAsync">Back to List</a>
</div>
@model SampleCosmosCore2App.Models.User.NewKeyModel
@{
    ViewData["Title"] = "AddKey";
    Layout = "~/Views/Shared/Layout.cshtml";
}

<div class="box">
    <h2>AddKey</h2>

    <form asp-action="AddKey">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-group">
            <label asp-for="Name" class="control-label"></label>
            <input asp-for="Name" class="form-control" />
            <span asp-validation-for="Name" class="text-danger"></span>
        </div>
        <div class="form-group">
            <input type="submit" value="Create API Key" class="btn btn-default" />
        </div>
    </form>

</div>
<div>
    <a asp-action="IndexAsync">Back to List</a>
</div>

And once it’s created, we then show it to you with “ShowKey”:

SampleCosmosCore2App/Views/User/ShowKey.cshtml

HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@model SampleCosmosCore2App.Models.User.UserAuthenticationModel
@{
    ViewData["Title"] = "ShowKey";
    Layout = "~/Views/Shared/Layout.cshtml";
}
 
<div class="box">
    <h2>Your New API Key</h2>
    <p>
        You will need the API Key Id and API Key Secret to make an API call. Save your API Key Secret now, we won't show it again!
    </p>
 
    <div>
        Name: @Model.Name<br />
        API Key Id: @Model.Id<br />
        API Key Secret: @Model.Identity<br />
    </div>
</div>
<div>
    <a asp-action="IndexAsync">Back to List</a>
</div>
@model SampleCosmosCore2App.Models.User.UserAuthenticationModel
@{
    ViewData["Title"] = "ShowKey";
    Layout = "~/Views/Shared/Layout.cshtml";
}

<div class="box">
    <h2>Your New API Key</h2>
    <p>
        You will need the API Key Id and API Key Secret to make an API call. Save your API Key Secret now, we won't show it again!
    </p>

    <div>
        Name: @Model.Name<br />
        API Key Id: @Model.Id<br />
        API Key Secret: @Model.Identity<br />
    </div>
</div>
<div>
    <a asp-action="IndexAsync">Back to List</a>
</div>

We’ll be requiring the pair of API Key Id and API Key Secret to authorize API calls later. The Id is the generated Cosmos DB id and the Key is the ICustomMembership generated value.

Displaying the index is a little more complex, as there are potentially several types of LoginUserAuthentications records available, so we include areas for specific keys and ignore the ones we don’t currently recognize:

SampleCosmosCore2App/Views/User/Index.cshtml

HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@model SampleCosmosCore2App.Models.User.UserIndexModel
 
@{
    ViewData["Title"] = "Index";
    Layout = "~/Views/Shared/Layout.cshtml";
}
 
<h2>Your Account</h2>
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
 
Username: @Model.User.Username<br />
Registered: @Model.User.CreationTime<br />
<br />
 
Twitter Status: @if (Model.UserAuthentications.ContainsKey("Twitter"))
{
    var twitter = Model.UserAuthentications["Twitter"].Single();
    <text>@twitter.Name at @twitter.CreationTime</text>
}
else
{
    <text>Not Linked</text>
}
<br />
 
<h3>API Keys</h3>
<table>
    <tr>
        <th>Created</th>
        <th>Name</th>
        <th>API Key Id</th>
        <th>API Key Secret</th>
        <th></th>
    </tr>
    @if (Model.UserAuthentications.ContainsKey("APIKey"))
    {
        var keys = Model.UserAuthentications["APIKey"];
        foreach (var key in keys)
        {
            <tr>
                <td>@key.CreationTime</td>
                <td>@key.Name</td>
                <td>@key.Id</td>
                <td>@key.GetMaskedIdentity()</td>
                <td><a asp-action="Revoke" asp-route-id="@key.Id">Revoke</a></td>
            </tr>
        }
    }
    <tr>
        <td colspan="5">
            <a asp-action="AddKey">Add a key</a>
        </td>
    </tr>
</table>
@model SampleCosmosCore2App.Models.User.UserIndexModel

@{
    ViewData["Title"] = "Index";
    Layout = "~/Views/Shared/Layout.cshtml";
}

<h2>Your Account</h2>
<div asp-validation-summary="ModelOnly" class="text-danger"></div>

Username: @Model.User.Username<br />
Registered: @Model.User.CreationTime<br />
<br />

Twitter Status: @if (Model.UserAuthentications.ContainsKey("Twitter"))
{
    var twitter = Model.UserAuthentications["Twitter"].Single();
    <text>@twitter.Name at @twitter.CreationTime</text>
}
else
{
    <text>Not Linked</text>
}
<br />

<h3>API Keys</h3>
<table>
    <tr>
        <th>Created</th>
        <th>Name</th>
        <th>API Key Id</th>
        <th>API Key Secret</th>
        <th></th>
    </tr>
    @if (Model.UserAuthentications.ContainsKey("APIKey"))
    {
        var keys = Model.UserAuthentications["APIKey"];
        foreach (var key in keys)
        {
            <tr>
                <td>@key.CreationTime</td>
                <td>@key.Name</td>
                <td>@key.Id</td>
                <td>@key.GetMaskedIdentity()</td>
                <td><a asp-action="Revoke" asp-route-id="@key.Id">Revoke</a></td>
            </tr>
        }
    }
    <tr>
        <td colspan="5">
            <a asp-action="AddKey">Add a key</a>
        </td>
    </tr>
</table>

Again, this HTML won’t win any awards, it’s here to let us functionally build out what we need. Later in a real application we would replace this with a better form or front-end templates and JSON.

Membership + Persistence

The screens above need some new capabilities in the membership logic (CosmosDBMembership) and queries to Cosmos DB (UserPersistence), so before we test we need to add those in.

The new methods are:

  • CosmosDBMembership.GenerateAPIKey
  • CosmosDBMembership.AddAuthenticationAsync
  • CosmosDBMembership.RevokeAuthenticationAsync
  • UserPersistence.GetUserAuthenticationAsync
  • UserPersistence.UpdateUserAuthenticationAsync
If you look at the commit details, you’ll also see an un-committed fix to UserPersistence.GetUserBySessionIdAsync from the prior post (oops).

SampleCosmosCore2App/Membership/CosmosDBMembership.cs

C#
1
2
3
4
5
6
7
8
9
public string GenerateAPIKey(string userId)
{
    var key = new byte[32];
    using (var generator = RandomNumberGenerator.Create())
    {
        generator.GetBytes(key);
    }
    return Convert.ToBase64String(key);
}
public string GenerateAPIKey(string userId)
{
    var key = new byte[32];
    using (var generator = RandomNumberGenerator.Create())
    {
        generator.GetBytes(key);
    }
    return Convert.ToBase64String(key);
}

GenerateAPIKey is starting with a simple random generator scheme to generate new keys.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public async Task<AuthenticationDetails> AddAuthenticationAsync(string userId, string scheme, string identity, string identityName)
{
    var userAuth = new LoginUserAuthentication()
    {
        // ... warning below ...
    };
 
    userAuth = await _persistence.Users.CreateUserAuthenticationAsync(userAuth);
 
    return new AuthenticationDetails()
    {
        // ... warning below ...
    };
}
public async Task<AuthenticationDetails> AddAuthenticationAsync(string userId, string scheme, string identity, string identityName)
{
    var userAuth = new LoginUserAuthentication()
    {
        // ... warning below ...
    };

    userAuth = await _persistence.Users.CreateUserAuthenticationAsync(userAuth);

    return new AuthenticationDetails()
    {
        // ... warning below ...
    };
}

AddAuthenticationAsync is just a CRUD method to write the new data and return the results.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public async Task<RevocationDetails> RevokeAuthenticationAsync(string userId, string identity)
{
    var userAuth = await _persistence.Users.GetUserAuthenticationAsync(identity);
    if (!userAuth.UserId.Equals(userId))
    {
        return RevocationDetails.GetFailed("Could not find specified API Key for your account");
    }
 
    if (userAuth.Scheme == Core.Users.AuthenticationScheme.RevokedAPIKey)
    {
        return RevocationDetails.GetFailed("APIKey has already been revoked");
    }
 
    if (userAuth.Scheme != Core.Users.AuthenticationScheme.APIKey)
    {
        return RevocationDetails.GetFailed("Could not find specified API Key for your account");
    }
 
    userAuth.Scheme = Core.Users.AuthenticationScheme.RevokedAPIKey;
    await _persistence.Users.UpdateUserAuthenticationAsync(userAuth);
 
    return RevocationDetails.GetSuccess();
}
public async Task<RevocationDetails> RevokeAuthenticationAsync(string userId, string identity)
{
    var userAuth = await _persistence.Users.GetUserAuthenticationAsync(identity);
    if (!userAuth.UserId.Equals(userId))
    {
        return RevocationDetails.GetFailed("Could not find specified API Key for your account");
    }

    if (userAuth.Scheme == Core.Users.AuthenticationScheme.RevokedAPIKey)
    {
        return RevocationDetails.GetFailed("APIKey has already been revoked");
    }

    if (userAuth.Scheme != Core.Users.AuthenticationScheme.APIKey)
    {
        return RevocationDetails.GetFailed("Could not find specified API Key for your account");
    }

    userAuth.Scheme = Core.Users.AuthenticationScheme.RevokedAPIKey;
    await _persistence.Users.UpdateUserAuthenticationAsync(userAuth);

    return RevocationDetails.GetSuccess();
}

RevokeAuthenticationAsync is a little more involved due to validation that the key you’re trying to revoke exists, is yours, isn’t already revoked, and so on, but then it updates the key type from APIKey to RevokedAPIKey and updates it in Cosmos DB.

So, ask me about class initializers…

There are times that using class initializers is an anti-pattern, like the case above. If the properties are required for the object to be valid, put it in the constructor and don’t use class initializers for it. Everything else is fair game.

I recognize that people really like them, but it misleads the next developer and makes it easy to introduce bugs when you’re adding or changing “required” fields.

Now that we have the new ICustomMembership behavior, we can add the UserPersistence requirements.

C#
1
2
3
4
5
6
public enum AuthenticationScheme
{
    Twitter = 1,
    APIKey = 2,
    RevokedAPIKey = 3
}
public enum AuthenticationScheme
{
    Twitter = 1,
    APIKey = 2,
    RevokedAPIKey = 3
}

First we add the two new Authentication types to the enum.

I really shouldn’t have named this enum “AuthenticationScheme”, since that has a specific meaning for ASP.Net Core 2 already, sorry about that. Future refactor opportunity.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ...
 
public async Task<LoginUserAuthentication> GetUserAuthenticationAsync(string id)
{
    var query = _client.CreateDocumentQuery<LoginUserAuthentication>(GetAuthenticationsCollectionUri(), new SqlQuerySpec()
    {
        QueryText = "SELECT * FROM UserAuthentications UA WHERE UA.id = @id",
        Parameters = new SqlParameterCollection()
        {
            new SqlParameter("@id", id)
        }
    });
 
    var result = await query.AsDocumentQuery()
                            .ExecuteNextAsync<LoginUserAuthentication>();
    return result.SingleOrDefault();
}
 
// ...
 
public async Task UpdateUserAuthenticationAsync(LoginUserAuthentication userAuth)
{
    await _client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(_databaseId, AUTHS_DOCUMENT_COLLECTION_ID, userAuth.Id), userAuth, new RequestOptions() { });
}
 
// ...
// ...

public async Task<LoginUserAuthentication> GetUserAuthenticationAsync(string id)
{
    var query = _client.CreateDocumentQuery<LoginUserAuthentication>(GetAuthenticationsCollectionUri(), new SqlQuerySpec()
    {
        QueryText = "SELECT * FROM UserAuthentications UA WHERE UA.id = @id",
        Parameters = new SqlParameterCollection()
        {
            new SqlParameter("@id", id)
        }
    });

    var result = await query.AsDocumentQuery()
                            .ExecuteNextAsync<LoginUserAuthentication>();
    return result.SingleOrDefault();
}

// ...

public async Task UpdateUserAuthenticationAsync(LoginUserAuthentication userAuth)
{
    await _client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(_databaseId, AUTHS_DOCUMENT_COLLECTION_ID, userAuth.Id), userAuth, new RequestOptions() { });
}

// ...

Once again, the Cosmos DB logic is pretty straightforward. I’m still getting used to the Query result handling, which feels a little convoluted, but everything else has been absurdly smooth because we’re just doing usual Document store logic (please take this JSON serialized object and give it back later) instead of having to map relational concepts to objects.

Now we can test out this process.

First we’ll add a new Key:

Adding an API Key

Adding an API Key

And here it is:

New API Key, Fancy UI :)

New API Key, Fancy UI 🙂

And it shows up in our fancy API Key list like so:

Viewing created API Keys

Viewing created API Keys

And we can revoke one of the keys easily:

Revoked API Key disappears, success!

Revoked API Key disappears, success!

And here it is in Cosmos DB Data Explorer, with the poorly named “Scheme” property indicating “3”, which is “RevokedAPIKey”:

Viewing Revoked API Key in Cosmos DB Data Explorer

Viewing Revoked API Key in Cosmos DB Data Explorer

Now that our fictitious users can generate and revoke API keys and we’ve locked in those changes, it’s time to protect some API endpoints.

Task 2: Protected API Endpoints

What we’re going to be doing for this case is adding in a new AuthenticationHandler middleware.

Other resources

We’ll be adding a CustomMembershipAPIAuthHandler and an options object to hold the name of the AuthenticationScheme and Realm configured from startup.

The very short version of AuthenticationHandlers is that when the server receives a request, it will go through the whole list of available AuthenticationHandlers and ask each one if the request is: Pass, Fail, or NoResult:

  • NoResult: This request doesn’t have anything relevant to me, thank you drive through!
  • Fail: This request has relevant info for me and it’s wrong!
  • Success: This request has relevant info for me and it’s right, here’s a ClaimsPrincipal!

Add the AuthenticationHandler

For this case, we’re going to expect an Authorization header on requests formatted as: Scheme Id:Secret, similar to Basic and Bearer authentication methods. The Scheme comes from the Options that will be set in Startup, the Id and Secret are the API Token values we created above.

SampleCosmosCore2App/Membership/CustomMembershipAPIAuthHandler.cs (HandleAuthenticateAsync)

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class CustomMembershipAPIAuthHandler : AuthenticationHandler<CustomMembershipAPIOptions>
{
    // ...
 
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // Is this relevant to us?
        if (!Request.Headers.TryGetValue(HeaderNames.Authorization, out var authorization))
        {
            return AuthenticateResult.NoResult();
        }
 
        var actualAuthValue = authorization.FirstOrDefault(s => s.StartsWith(Options.Scheme, StringComparison.CurrentCultureIgnoreCase));
        if (actualAuthValue == null)
        {
            return AuthenticateResult.NoResult();
        }
 
        // Is it a good pair?
        var apiPair = actualAuthValue.Substring(Options.Scheme.Length + 1);
        var apiValues = apiPair.Split(':', 2);
        if (apiValues.Length != 2 || String.IsNullOrEmpty(apiValues[0]) || String.IsNullOrEmpty(apiValues[1]))
        {
            return AuthenticateResult.Fail($"Invalid authentication format, expected '{Options.Scheme} id:secret'");
        }
 
        var principal = await _membership.GetOneTimeLoginAsync("APIKey", apiValues[0], apiValues[1], Options.Scheme);
        if (principal == null)
        {
            return AuthenticateResult.Fail("Invalid authentication provided, access denied.");
        }
 
        var ticket = new AuthenticationTicket(principal, Options.Scheme);
        return AuthenticateResult.Success(ticket);
    }
 
    // ...
}
public class CustomMembershipAPIAuthHandler : AuthenticationHandler<CustomMembershipAPIOptions>
{
    // ...

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // Is this relevant to us?
        if (!Request.Headers.TryGetValue(HeaderNames.Authorization, out var authorization))
        {
            return AuthenticateResult.NoResult();
        }

        var actualAuthValue = authorization.FirstOrDefault(s => s.StartsWith(Options.Scheme, StringComparison.CurrentCultureIgnoreCase));
        if (actualAuthValue == null)
        {
            return AuthenticateResult.NoResult();
        }

        // Is it a good pair?
        var apiPair = actualAuthValue.Substring(Options.Scheme.Length + 1);
        var apiValues = apiPair.Split(':', 2);
        if (apiValues.Length != 2 || String.IsNullOrEmpty(apiValues[0]) || String.IsNullOrEmpty(apiValues[1]))
        {
            return AuthenticateResult.Fail($"Invalid authentication format, expected '{Options.Scheme} id:secret'");
        }

        var principal = await _membership.GetOneTimeLoginAsync("APIKey", apiValues[0], apiValues[1], Options.Scheme);
        if (principal == null)
        {
            return AuthenticateResult.Fail("Invalid authentication provided, access denied.");
        }

        var ticket = new AuthenticationTicket(principal, Options.Scheme);
        return AuthenticateResult.Success(ticket);
    }

    // ...
}

Notice we’ve added the concept of a “OneTimeLogin” to ICustomMembership. This will lead to a membership refactor to make it clearer which methods are relevant to “Interactive” logins and which are relevant to “OneTimeLogin” types like API request authentication.

On top of having logic to look at handle Authenticate requests, we also want to add logic to handle Challenge and Forbidden requests. In this case, we are going to add a nice WWW-Authenticate header before letting the base class send it back as a 401, which is consistent with how most APIs handle challenges. We’re also going to just let the base class handle the Forbidden cases as designed.

SampleCosmosCore2App/Membership/CustomMembershipAPIAuthHandler.cs (HandleAuthenticateAsync)

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CustomMembershipAPIAuthHandler : AuthenticationHandler<CustomMembershipAPIOptions>
{
    // ...
 
   protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{Options.Realm}\", charset=\"UTF-8\"";
        await base.HandleChallengeAsync(properties);
    }
 
    protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
    {
        return base.HandleForbiddenAsync(properties);
    }
 
    // ...
}
public class CustomMembershipAPIAuthHandler : AuthenticationHandler<CustomMembershipAPIOptions>
{
    // ...

   protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{Options.Realm}\", charset=\"UTF-8\"";
        await base.HandleChallengeAsync(properties);
    }

    protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
    {
        return base.HandleForbiddenAsync(properties);
    }

    // ...
}

After an addition of an extension method, we can register this in Startup.

SampleCosmosCore2App/Startup.cs

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
 
    // ... add persistence, membership ...
 
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        /* Custom Membership API Provider */
        .AddCustomMembershipAPIAuth("APIToken", "SampleCosmosCore2App")
        /* External Auth Providers */
        .AddCookie("ExternalCookie")
        .AddTwitter("Twitter", options => /* ... */ )
        /* 'Session' Cookie Provider */
        .AddCookie((options) => /* ... */);
 
    services.AddAuthorization(options => {
        options.AddPolicy("APIAccessOnly", policy =>
        {
            policy.AddAuthenticationSchemes("APIToken");
            policy.RequireAuthenticatedUser();
        });
    });
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    // ... add persistence, membership ...

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        /* Custom Membership API Provider */
        .AddCustomMembershipAPIAuth("APIToken", "SampleCosmosCore2App")
        /* External Auth Providers */
        .AddCookie("ExternalCookie")
        .AddTwitter("Twitter", options => /* ... */ )
        /* 'Session' Cookie Provider */
        .AddCookie((options) => /* ... */);

    services.AddAuthorization(options => {
        options.AddPolicy("APIAccessOnly", policy =>
        {
            policy.AddAuthenticationSchemes("APIToken");
            policy.RequireAuthenticatedUser();
        });
    });
}

First we make a one line addition to the AddCustomMembershipAPIAuth Extension method, passing in a Scheme and Realm. Next we add in a new “APIAccessOnly” policy that we will use to enforce API Access authentication only for API endpoints, which we can do next.

SampleCosmosCore2App/Controllers/ValuesController.cs

[Route("api/[controller]")]
[Authorize(Policy = "APIAccessOnly")]
public class ValuesController : Controller
{
    // GET api/values
    [HttpGet]
    public IEnumerable Get()
    {
        return new string[] { "value1", "value2" };
    }

    // ...
}

I’ve decorated the ValuesController with an Authorize(Policy = ...) attribute, but I could also have used Authorize(AuthenticationScheme = ...) and skipped the policy definition in the Startup file. However, I like the idea that all of my Authentication schemes are defined in Startup consistently, the policy for accessing API endpoints is defined in one place, and it looks more readable to me.

Membership Changes

To support the middleware above, we need a GetOneTimeLoginAsync method in membership. This method will accept the authentication details, verify them, and return a ClaimsPrincipal (or null).

SampleCosmosCore2App/Membership/CosmosDBMembership.cs

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public async Task<ClaimsPrincipal> GetOneTimeLoginAsync(string scheme, string userAuthId, string identity, string authenticationScheme)
{
    var authScheme = StringToScheme(scheme);
    var userAuth = await _persistence.Users.GetUserAuthenticationAsync(userAuthId);
 
    // are the passed auth details valid?
    if (userAuth == null)
    {
        return null;
    }
 
    if (userAuth.Scheme != authScheme || !userAuth.Identity.Equals(identity, StringComparison.CurrentCultureIgnoreCase))
    {
        return null;
    }
 
    // is the user allowed to log in? We don't have addtl checks yet
    var user = await _persistence.Users.GetUserAsync(userAuth.UserId);
 
    // create a claims principal
    var claimsIdentity = new ClaimsIdentity(authenticationScheme);
    claimsIdentity.AddClaim(new Claim("userId", userAuth.UserId));
    claimsIdentity.AddClaim(new Claim("userAuthId", userAuth.Id));
    return new ClaimsPrincipal(claimsIdentity);
}
public async Task<ClaimsPrincipal> GetOneTimeLoginAsync(string scheme, string userAuthId, string identity, string authenticationScheme)
{
    var authScheme = StringToScheme(scheme);
    var userAuth = await _persistence.Users.GetUserAuthenticationAsync(userAuthId);

    // are the passed auth details valid?
    if (userAuth == null)
    {
        return null;
    }

    if (userAuth.Scheme != authScheme || !userAuth.Identity.Equals(identity, StringComparison.CurrentCultureIgnoreCase))
    {
        return null;
    }

    // is the user allowed to log in? We don't have addtl checks yet
    var user = await _persistence.Users.GetUserAsync(userAuth.UserId);

    // create a claims principal
    var claimsIdentity = new ClaimsIdentity(authenticationScheme);
    claimsIdentity.AddClaim(new Claim("userId", userAuth.UserId));
    claimsIdentity.AddClaim(new Claim("userAuthId", userAuth.Id));
    return new ClaimsPrincipal(claimsIdentity);
}

Currently this method accepts an authentication scheme for the “OneTimeLogin”, later this will be refactored so that the membership class supports a separate “InteractiveAuthenticationScheme” and “OneTimeLoginScheme” that are configured in Startup and they will consistently set either userId and userAuthId claims or userId and sessionid claims.

That’s all we needed! Let’s try it out with postman.

Let’s start with the happy path, passing in good credentials:

Testing the successful path w/ good API Credentials

Testing the successful path w/ good API Credentials

Then we should try a number of failure situations, like no credentials, bad credentials, and invalidly formatted credentials:

Verifying Bad Token reports 401

Verifying Bad Token reports 401

Excellent! We now have working API Authentication!

Wrapping up, next steps

Moving right along. We now have an ASP.net Core 2 website that can perform really basic CRUD logic against Cosmos DB. We’ve then layered standard username/password authentication on it, with storage in Cosmos DB as well. Then we extended this to support a second interactive login method (Twitter), opening the door to adding as many of those as we need. Then in this post, we’ve added API authorization for per-request authorization. So far, so good.

Next up, we’re going to take a break from Cosmos DB for a bit to do some general refactoring (which won’t be a blog post) and add in error handling logic. From there, we’ll shift focus to front-end tooling to ensure we have a good developer and production experience with CSS and JavaScript. Once we’re done there, we’ll be back to add in API Rate Limiting with Cosmos DB as the backing store. See you soon!