In the previous post in this series we examined at a high-level how responsibilities for authentication and authorisation are distributed in a micro-services architecture. In this post, the strategies and technologies that underpin the implementation of authentication and authorisation will be explored further, with the core access management services providing authentication services, which support individual services performing authorisation. This discussion is actually split across two posts, as authentication and authorisation are core parts of the access management services, and require extensive discussion, with this post focusing upon the core capabilities, and the following post focussing upon more advanced authentication and authorisation requirements.
Posts in this series
- Part 1: Overview
- Part 2: Authentication and Authorisation
- Part 3: Advanced Authorisation and Assurance
- Part 4: Enabling Other Teams and Inter-Service Authentication
Implementing Centralised Authentication
While authorisation may fall to individual services, authentication services are entirely the responsibility of the Access Management. In a distributed micro-services architecture, the requirements of the authentication services are that they are able to provide a token which allows for validation by individual services, without having to validate against the access management system. This requirement is driven by the fact that it is costly to make network hops when services are distributed and could be hosted anywhere. As with many things computer security related, this requirement is solved by asymmetric cryptography.
In a previous blog post, I have delved into some technology that offers an approach for tackling this, discussing this signing and validation process for JSON Web Tokens, or JWTs. While there are other token formats that could be used, JWTs are emerging as a very popular format, driven in part by their simplicity, but also by their relatively non-prescriptive nature. They can store any information in the payload, and flexibly store metadata about the approaches that can be taken to validate them, allowing changes to be made on the token issuing side with minimal disruption to dependant services (assuming they are appropriately configured, which is enabled by common Access Management SDKs, a concept that we will touch upon later).
The centrally issued JWTs are supported by the Access Management services providing the ability to obtain a JSON Web Key (JWK). The format of this is discussed in more detail in the previously mentioned blog post, but this key enables distributed services to validate user JWTs using the authentication provider’s public key, without needing to refer back to the provider on every call. This is demonstrated in the following diagram (again, from the previous blog post).
Another powerful tool for implementing centralised authentication is OAuth. Despite not specifying details around authentication, the flows utilised by OAuth are important concepts to understand, as they facilitate the secure transfer of information between systems. In this series, I will treat the OAuth 2.0 3-legged flow as the standard method that will be used for user authentication/consent management, though the client credentials and resource owner flows have a role as well. Services may need to authenticate themselves with the access management services (for instance to obtain JWKs, or authorisation information) as well as each other, and first party clients may desire a native log-in experience, which would warrant one of these other flows.
The OAuth standard doesn’t dictate authentication methods, and it is left intentionally vague here. While I assume that most implementations will perform some sort of form-based authentication against a user-store, there is no reason why it couldn’t be a Mutual SSL challenge, a FIDO flow or any other sort of mechanism that satisfactorily identifies the user. The authentication services could also dynamically determine an authentication challenge, in order to be able to accommodate step-up requests associated with particular scopes or services, or to handle context-based risk assessment.
Types of Consuming Clients
In the OAuth paradigm, ‘Client’ is fairly well defined, representing the party requesting access on behalf of a user (or for themselves).
While clients in the OAuth runtime are simply represented by an identifier, a secret, and some available scopes, I feel it is important to differentiate slightly more than this when designing a system. The most important design distinction is going to be that of first party clients versus third party clients. In the case of a first party client, there is a lot of lee-way in how authentication flows can be handled, and in how to handle consent/scopes. A first party client could be considered trusted enough to consume user credentials, and to be permitted access to a broad range of scopes.
The user credentials question is actually an interesting one, and a draft RFC takes a much more restrictive view of user credential handling than many users have become accustomed to, essentially barring any sort of native application from handling credentials itself. While this is absolutely appropriate for third party applications, forcing a user of your mobile app to jump out to the system browser to enter credentials into your web login screen may in fact seem more like a phishing attempt than simply having a native login screen. Given this focus upon fostering appropriately secure user behavior, while providing a compelling user experience, I will refrain from making any hard and fast statements about appropriate implementation, other than ‘Don’t let users enter credentials into third party applications’, but that should, I hope, be obvious.
The other distinction between types of clients is between ‘trusted’ and ‘untrusted’ clients in OAuth terminology, or more simply between clients which are capable of maintaining the secrecy of their client_secret and those that are not. This mostly differentiates applications which make client-side token requests from those which make server side requests, and an easy mechanism for determining the difference in most cases is whether the OAuth callback URL points to localhost or not. An untrusted client does not present credentials to identify itself to the endpoints, and so some care should be taken in granting sensitive scopes to these clients. It is relatively trivial for a malicious application to masquerade as any untrusted client, only requiring the client_id of the application, which is passed in the url of authorise requests, so ought to be considered insecure.
Implementing Distributed Authorisation
As previously mentioned, JWTs provide a powerful form of stateless token, which can present any arbitrary identity claims in a signed, verifiable form. As a result of a successful authentication flow, these claims are made available to consuming services, which are then responsible for making authorisation decisions. The implementation of these authorisation rules fall to the service developers, and I imagine most will implement them in code, perhaps using some external configuration file to simplify changes in policy. While there are a number of authorisation policy standards, such as XACML, I am not convinced that the productivity hit of mandating developers learn XACML, implement their authorisation rules, and develop an XACML parser/decision engine in whichever language they are using is justifiable compared to writing bespoke authorisation rules using whichever logical flow operators exist in their language of choice. For illustration, consider the following sample policy that makes an authorisation decision based upon working hours (taken from Wikipedia):
<xacml3:Rule RuleId="c01d7519-be21-4985-88d8-10941f44590a" Effect="Permit"> <xacml3:Description>Allow if time between 9 and 5</xacml3:Description> <xacml3:Target> <xacml3:AnyOf> <xacml3:AllOf> <xacml3:Match MatchId="urn:oasis:names:tc:xacml:1.0:function:time-greater-than"> <xacml3:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#time">09:00:00</xacml3:AttributeValue> <xacml3:AttributeDesignator Category="urn:oasis:names:tc:xacml:3.0:attribute-category:environment" AttributeId="urn:oasis:names:tc:xacml:1.0:environment:current-time" MustBePresent="false" DataType="http://www.w3.org/2001/XMLSchema#time"/> </xacml3:Match> </xacml3:AllOf> </xacml3:AnyOf> <xacml3:AnyOf> <xacml3:AllOf> <xacml3:Match MatchId="urn:oasis:names:tc:xacml:1.0:function:time-less-than"> <xacml3:AttributeValue DataType="http://www.w3.org/2001/XMLSchema#time">17:00:00</xacml3:AttributeValue> <xacml3:AttributeDesignator Category="urn:oasis:names:tc:xacml:3.0:attribute-category:environment" AttributeId="urn:oasis:names:tc:xacml:1.0:environment:current-time" MustBePresent="false" DataType="http://www.w3.org/2001/XMLSchema#time"/> </xacml3:Match> </xacml3:AllOf> </xacml3:AnyOf> </xacml3:Target> </xacml3:Rule>
Compared to the following equivalent javascript code:
var now = new Date(); if(now.getHours() >= 9 && now.getHours() < 17){ return true; } return false;
Yes, the XACML policy is beautifully externalised, and able to be updated, reloaded into the PDP, etc. but there is no reason why the javascript code couldn’t simply reference a policy config file to determine whether to evaluate it, and prevent this hard coding of hours.
If service developers do indeed develop their authorisation in this manner, the authorisation rules and their implementation will vary from service to service; but they will need to be based upon common identity claims presented in the user token.
The JWT specification includes a number of common claims, including ‘sub’ (subject), ‘aud’ (intended audience) and (from the OAuth specification) ‘scope’, these claims can also be complemented with any set of arbitrary claims, such as ‘role’. For most use cases, these will be adequate for making authorisation decisions, such as checking that the user principal in the subject claim is the owner of the data being accessed, or whether the role is appropriate for the action that is being performed.
One claim I do want to linger upon is ‘scope’, which ought to be added to the user token based upon the access that was originally requested in an OAuth flow. For authorisation, it can be considered a set of coarse grained entitlements, though the scope parameter itself does not have any rigid specification (RFC6749, Section 3.3). This gives the flexibility to define scopes in accordance with coarse grained business capabilities that may be valid across services, perhaps offering ‘manage_customers’, ‘promote_service’ or similar. These coarse grained entitlements can be combined with the service-specific authorisation logic discussed above, in addition to limiting access tokens issued for one application with particular required capabilities from being re-used on other service endpoints without invoking the authentication services.
This post has dealt with simple authentication and authorisation scenarios, or at least scenarios which only require a single stage of authentication and authorisation based solely upon the common claims encapsulated in the user’s JWT. Not all access management scenarios are going to be this straightforward though, and so in the next post in this series, we will examine more complex authentication and authorisation scenarios requiring additional sensitive information, token revocation, the concept of assurance levels and authentication challenges.
3 thoughts on “Access Management and Micro-services – Part 2: Authentication and Authorisation”