Professional Software Development Outsourcing
Not all companies have the luxury of owning an in-house tech team. That is to say, the luxury of having a skilled team they can consult when looking to achieve tech-related business goal...
When building an API, sooner or later you’ll have to secure it with some kind of authentication schema. There are many industry-standard solutions available - JWTs, sessions, OpenID...
Then, after implementing one you'll feel satisfaction - your API is secured!
That is until one of your customers gains access to another's data. Uh-oh. You forgot the authorization - again!
API authorization is the function of restricting access to different resources based on a user's identity. This is different from API authentication, which determines user identity based on information previously provided.
For example, taking a user's session would be authentication, whereas checking for user admin and restricting or denying access to some endpoint is the authorization.
Choosing the right way to implement the authorization is hugely dependent on your particular application, business logic and requirements.
One of the simplest ways to restrict users is to assign one of the predefined roles to every user, then check on every request if a given endpoint requires a role and whether the user has one.
An example sketch of its implementation would look something like this:
Roles are neat and easy to implement, but this relative ease can also cause inflexibility in potential configurations. What if we want dynamic roles a customer can configure? Enter scopes.
Scopes (or permissions, depending on nomenclature) are strings assigned to each user or role which allow us to define a cascading access system. There are multiple methods of creating scopes, one of which being domain.resource.target.action:
domain represents one microservice or business domain
resource represents one resource (not necessarily one endpoint!)
target represents whom this action concerns. Example classification would be user and team all, where each next also contains the previous one.
action represents what will be done to the resource. Example classification would be read, write, manage, where read gives read-only access, write gives read + create + update and manage allows also to also delete or perform any custom actions on the resource.
Each endpoint would then be given one or more required scopes which would then be checked on each endpoint.
So far, we've only discussed restricting which endpoint the user can access. However, that endpoint still leaves a lot of potentially unwanted access, especially to what data is returned to the user. What we need is data ownership.
Data ownership is assigning ownership to all data kept in your application - and it doesn't have to be restricted to just one owner. We can keep an ownerId as well as a relation (or array of IDs if your database supports some form of arrays) teamIds and any other unit of access your application may require. We then mirror that to our user and allow him access if the ids match (for example, when the owner's id matches the user’s id or if any team id matches any of client’s team id).
Data ownership done this way integrates neatly with scopes. This is to say that when you have only user target in your scope, you can only access data that matches direct ownership (ownerId with your id), but if you have team target, team access is also checked. All target means is that ownership is not checked at all.
The previously mentioned methods work wonders when your application only handles customers. But sometimes you might also want to give access to other APIs and applications. OAuth 2.0 offers a perfect solution.
OAuth 2.0 enables an external application to act as a user from your application after his or her consent, achieving two goals simultaneously: access for others to the information provided by the API whilst restricting them from what is accessible by the given user and what he consented to.
Since OAuth 2.0 only gives use of the framework for managing external access, internal authorization must be done in other ways. Since consent is also working per scope, a scope-based authorization is a perfect counterpart to OAuth 2.0.
But we won’t get into implementation here, given its general complexity and pre-existing solutions. In the meantime, however, you can consult node-oauth2-provider and OAuth2orize, both of which are officially approved libraries.
But there don’t have to be. If you are using JWT, all initial authorization data (roles, scopes) can be stored in the token. But as data ownership must be checked on a database level, try storing it as ownership data in the same table as the data it’s guarding. This will minimise joins in your queries and do away with any need for NoSQL.
For really expensive data-fetching, you can keep a cache that has just access information. For each piece of data, keep its id in a form that can be easily traced back to the user. When you want to check access, query your cache. If the ID you want to fetch is in the cache for this user, he can access it.
Find your own way to better authorization
If none of the solutions mentioned above fully suits you, then there's no obligation! Good authorization API security mechanisms are those which fulfil your specific requirements. At Startup Development House, we’ll help you choose and implement the perfect one for your application.
Don't hesitate to reach out to hello@start-up.house