Nowadays, the HTTP APIs act as gateways for petabytes of data and some chunk of it might actually require enhanced access rules. For example, you could create a link that allows the user to download the file only once, and within such link you would find a token.
I was in a need of creating such solution for my open source project Warden – a specialized, one-time link that can be used fetch the configuration object from the API.
It turned out to be fairly straightforward to implement the most basic version of such behavior.
Before I put there any code, let me warn you, that for the sake of simplicity there will be no specialized design patterns involved (like repositories, command handlers etc.) and all of the stuff will be held in memory (static List
1 2 3 4 5 6 7 8 9 10 11 |
public class Item { public int Id { get; protected set; } public string Name { get; protected set; } public Item(int id, string name) { Id = id; Name = name; } } |
This is quite a sophisticated entity called Item which we would like to fetch via our API controller:
1 2 3 4 5 6 7 8 |
[Route("api/items")] public class ItemsController : Controller { private static readonly IItemService ItemService = new ItemService(); [HttpGet("{id}")] public Item Get(int id) => ItemService.Get(id); } |
And the “business logic”:
1 2 3 4 5 6 7 8 9 10 11 |
public class ItemService { private static readonly List<Item> Items = new List<Item> { new Item(1, "Item 1"), new Item(2, "Item 2"), new Item(3, "Item 3") }; public Item Get(int id) => Items.FirstOrDefault(x => x.Id == id); } |
As you can see it’s as simple as possible – I’m not even using interfaces or DTOs not to mention the IoC containers. Alright then, let’s try to secure our API:
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 |
public enum ResourceType { Item = 1 } public class SecuredRequest { private static readonly string[] ReplaceableCharacters = { "+", "?", "&", "%" }; public ResourceType ResourceType { get; protected set; } public int ResourceId { get; protected set; } public string Token { get; protected set; } public DateTime CreatedAt { get; protected set; } public DateTime? UsedAt { get; protected set; } protected SecuredRequest() { } public SecuredRequest(ResourceType resourceType, int resourceId) { ResourceType = resourceType; ResourceId = resourceId; CreatedAt = DateTime.UtcNow; Token = CreateToken(); } public void Consume(string token) { if (UsedAt.HasValue) throw new InvalidOperationException("Token has been already used."); if (!Token.Equals(token)) throw new InvalidOperationException("Invalid token."); UsedAt = DateTime.UtcNow; } private static string CreateToken() { using (var rng = RandomNumberGenerator.Create()) { var tokenData = new byte[32]; rng.GetBytes(tokenData); var token = Convert.ToBase64String(tokenData); foreach (var replaceableCharacter in ReplaceableCharacters) { token = token.Replace(replaceableCharacter, string.Empty); } return token; } } } |
So, what’s going on here? At first, we create an enum for the resource types. There’s just Item and you could use something totally different here like a string based on class type name or some more fancy stuff, but that’s not the point of this article. Then, we define the SecuredRequest which takes the resource type and id as parameters and creates a secured token. In case you were wondering what the heck are ReplaceableCharacters – it’s just a set of characters that I’d rather exclude from the token used within a link in order not to mess with HTTP parser. Now we’re ready for the almost final step:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class SecuredRequestService { private static readonly List<SecuredRequest> SecuredRequests = new List<SecuredRequest>(); public void Create(ResourceType resourceType, int resourceId) { var securedRequest = new SecuredRequest(resourceType, resourceId); SecuredRequests.Add(securedRequest); } public void Consume(ResourceType resourceType, int resourceId, string token) { var securedRequest = SecuredRequests.FirstOrDefault(x => x.ResourceType == resourceType && x.ResourceId == resourceId && x.Token == token); if(securedRequest == null) throw new ArgumentException("Resource not found or not secured."); securedRequest.Consume(token); } } |
This guy above will be responsible for creating and validating access tokens for the particular resources. In order to make this sample testable, let’s add a simple endpoint for generating the tokens:
1 2 3 4 5 6 7 |
[HttpPost("{id}/tokens")] public object Post(int id) { var securedRequest = SecuredRequestService.Create(ResourceType.Item, id); return new {token = securedRequest.Token}; } |
And finally, we can secure the access to the items, so that the controller would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[Route("api/items")] public class ItemsController : Controller { private static readonly ItemService ItemService = new ItemService(); private static readonly SecuredRequestService SecuredRequestService = new SecuredRequestService(); [HttpGet("{id}")] public Item Get(int id, string token) { SecuredRequestService.Consume(ResourceType.Item, id, token); return ItemService.Get(id); } [HttpPost("{id}/tokens")] public object Post(int id) { var securedRequest = SecuredRequestService.Create(ResourceType.Item, id); return new {token = securedRequest.Token}; } } |
And that’s all – try to run the application now and access the item without token or with the invalid one. Create a new token, fetch the item and try to fetch in one more time – voilĂ , you have just limited your requests to be one-time only. You can download the sample application by clicking here.
Before I finish this post, here are these possible extensions and things to consider:
- Move the implementation to specialized attributes or so.
- Store more data like headers (e.g. User-Agent), IP address etc.
- Allow only authenticated users to access the resource.
- Include dates to limit the usage of the token within the given period.
- Restrict the access via IP address or other techniques.
Pingback: One-time secured API requests - How to Code .NET
What if someone would like to regenerate the token for specific item? You create new SecuredRequest just by adding a new one to the collection, so can happen that you will have 2 SecureRequests for Item 1 in collection, but while consuming you find them by FirstOrDefault so it will be always comparing to the first SecuredRequest ever created == it won’t work in this scenario. Maybe that was the point no to let people to regenerate the link for a single Item so maybe it would be good not to allow creating multiple SecureRequests for Item 1 in the collection?
Good point, seems that I’ve made a mistake here, as you should be able to create more than one SecuredRequest for a single Item. Let me fix the examples and source code :).
Pingback: Dew Drop - September 6, 2016 (#2322) - Morning Dew
This is really helpful post, very informative there is no doubt about it. Keep it up.