The concept of a “user” in a system can mean different things, depending on the context, which is a great example of bounded contexts in action. A “user” represents an entity, which is simply a thing that can be uniquely identified. Given an identified users, different bounded contexts can be interested in different information about that user, in order to deliver different functionality and features. One of the things that can be difficult to grasp at first is this splitting of an entity into these sub-parts, where the connection between the parts is by identity only, and is as loose as possible – it is the looseness of this connection that opens up benefits, by creating small isolated components that can change independently.
Authentication is simply the question of identifying a user by some set of credentials, and providing them with a secured token that represents that identity. The most common mechanism for this is username/password; the user offers these credentials, which are checked against a credential store, and the user is given an encrypted token containing their unique identifier and other pertinent information. The token normally has an expiry and perhaps a refresh capability, as ways of securing the system. There is a cross-cutting element here, where all other services will need to be able to decrypt that token to read the user’s identity. The token is usually “signed” by private key by the authentication service, and can be decrypted with the public key by any other interested party. Many systems delegate authentication to third parties – perhaps login via Google, Facebook or Twitter account – which works in a very similar way – credentials (in this case credentials for this external service) are exchanged for a token. Authentication makes a neatly bounded context, in that it has a very well-defined role, and well-defined set of information that it needs to do this role. The domain of authentication does not need to be specialized for individual businesses, and so third-party implementations can be used quite easily – like the external Facebook/Google login, or an internally hosted solution like IdentityServer. Once again, the only job of Authentication is to verify the identity of a user and package that in a way that can be used elsewhere.
However, Authentication and Authorization are often mixed – which can lead to serious complexity problems down the line. While Authentication answers the question “who are you?”, Authorization is about the question “what can you do?”. Authentication makes a naturally bounded context (or logical service), in that it has a closed role and needs minimal other information, but Authorization is not that simple. The challenge with Authorization is that in order to answer the question “what can you do?”, the system must have knowledge of what things are possible to do. A very common anti-pattern is the creation of an “Authorization Service”, in an attempt to put Authorization concerns into a single place. This can be appealing on the grounds that all the information about a concept is grouped together, however this means that the “Authorization Service” must know about all the functionality of every other service, and the business rules around who can invoke this functionality and based on what conditions. This is a very common source of major architectural problems, as a result of individual services being unable to perform their job autonomously, and a single service (Authorization) having an intimate knowledge of the workings of other services. The purpose when designing services in a service-oriented or microservices architecture is to create independent logically bounded components, that can version independently, and function in isolation. At runtime, service boundaries form fault partitions, where if one service is offline, other services should be able to continue to work. Allowing a single service to hold knowledge about other services violates these principles.
While Authentication is normally business-agnostic – its role is simply to identify a user – authorization is normally very specific to the application context. There may be some reusable patterns – such as roles – but for the most part the logical rules about who can perform a function and when are very specific to the function in question. An important design principle for services is that they should form boundaries around logic, such that the logic is grouped closely together, and equally that these groups are isolated from each other, ideally resulting in a number of smaller and independent components rather than fewer larger and more intertwined components. A service should be able to, autonomously and independently, unpack the information provided about the user’s identity (the logic to unpack the information is a special case of shared knowledge), and determine how it should behave.
Where services need to know information that other services are responsible for authoring and managing, there is a robust mechanism for this, in the form of Domain Events. The idea behind domain events is that the domain that manages a piece of information that is shared, that domain publishes events describing changes to that information, which other interested domains can arbitrarily subscribe to. These other domains can make their own decisions about what to do with this information, perhaps manipulating it in some form and storing it alongside information that they manage, or alongside other information collected from subscribing to other services. Domain Events should richly describe the change, so that the event stands alone without need for further context, such that a subscribing service needs only to process these events, with no need to query for further information about the change.
In a system with Roles based authorization, one way to implement this is to have a very lightweight Roles service, that is responsible only for managing role membership – not for mapping the roles to allowed actions; this service could publish information about Role membership, which other services can collect to build their own view of membership of the roles they are interested in. This approach does give the advantage of each service being independent, but comes at the significant cost of every service having to build and maintain their own store of role membership. What this implementation has done is to simply reverse the problem – where the previous problem was having an Authorization service that had intimate knowledge of the rules about all actions all other services can perform, this implementation has the problem of all other services having an intimate knowledge of role membership.
A preferable approach for roles is that these roles are either owned by Authentication, as sub-facts of a user’s identity (this is Fred, he works for Acme Inc, his job title is Software Developer). This is sometimes known as “Claims” based authentication. Notice here that the pieces of information describe the user, not the system – these are merely facts about the user that other services can make use of in their decision to allow certain actions. The individual service may have a rule that “only software developers can edit system settings” – they are able to simply unbundle the claims, and apply this rule. The claims don’t know about the individual features (system settings), and the only requirement for this to work is that the Authentication service must have previously issued a valid token containing these claims. If the Authentication service is offline for whatever reason, if Fred is already authenticated, then other services can continue to interact with him freely until his token expires; if he is not authenticated then functions that allow unauthenticated users can carry on as normal, while secured functions are online but unavailable until he is able to authenticate. This approach works well for system-wide facts about a user, which should be very high-level. Also worth noting is that these facts should be considered publicly available, because they are stored in a token that can be decrypted by public key – so be sure not to put any sensitive information in here!
In some instances, the authorization rules are granular and specific to the functionality in question. The approach here is for the bounded context that owns this functionality to own the data needed. For example, an administrator may specify a list of individual users who can access a certain record. This is very specific, very detailed information, that implies a knowledge of the context in which it is used, and should not go into a system-wide claim. This is essentially business data, the same as any other, that is used as part of applying a business rule. The service should own this data, privately, with the full management capability provided.
Besides system-wide rules and these very specific feature-level rules, there is a common third category, which often applies when the billing model is that customers pay for access to certain features. In this case, there is a bounded context for managing these Entitlements, and connecting “feature packages” to customers. The way to implement this is to have an Entitlement service, that emits domain events describing changes to the features a customer has available to them. The “feature packages” describe what the customer has paid for – this is a piece of information owned by the Entitlement service. This may resemble features (functionality owned by other services), but is subtly and importantly different. The domain events will be something along the lines of “Customer 123 bought access to feature package Gold until 1/1/2018”. If for example the Gold package gives the customer free shipping, a Dispatch service might subscribe to this event, and store its own representation of this entitlement information. Note that the domain event is not describing whether the customer can perform an action, and has no details of which services are interested in the information – it merely states facts that are owned by the Entitlement service. This is not the same as an Authentication claim, because the fact is too specific, and merits its own logical context – it is nothing to do with “who the user is” – this type of data is merely published information about business entities.
In designing Authentication and Authorization in a system, thinking about the bounded contexts underneath can help us to come up with implementations that will not introduce coupling, and allow future maintenance and growth of the system to be far simpler. The job of Authentication is only to identify who a user is; the function of Authorization is to establish in a specific context whether a certain action is available to the identified user. Keep these cleanly separated, and keep the information about a specific context cleanly grouped even if there is an initial investment in doing so, and the overall complexity and lifetime costs can be kept much lower.