This article discusses how machine clients interact with each other in a secure way. Specifically it will dive into:
- A refresher on how JWTs work
- What a machine service is
- Usage of API Keys
- Token generation and how it differs from user JWT access tokens
- How generated JWTs are secured
- How to validate service client JWTs
Throughout the article we'll refer to our clients as
service clients. A service client is a machine or service entity that needs access to another service. Examples of service clients might be one of your users that wants to interact with your APIs, or one of your services that interacts with the Slack or Google Workspace APIs. Alternatively, one service in your platform that needs to communicate with another one--such as an
Orders Service that wants to fetch user related information from a
User Profile Service. These services will talk to each other. And they do that by creating and securing HTTP requests between each other. This is known as
Every client and user needs to identify who they are so that we can verify that identity and ensure that client is authorized to only be able to perform the actions they are allowed to perform.
The working setup is:
- End users that log into UIs
- Backend services that receive calls from these UIs
- These same services may also call each other
- A centralized authentication service such as Authress that enables verification of tokens
We'll remember that every token that is created in a platform must be verifiable. If these tokens can't be verified then any one can create any token, even one that has admin privileges. To guard against this, all tokens will be JWTs. JWTs have two important components:
- A user ID - known as the
- The issuer - the source that created the token found in the
This is an example JWT:
These JWTs can be verified by using a standard library such as an Authress SDK.
In the case of your end users, you have a login portal that users are directed to in order to log in. Through the Auth provider, users are forwarded to a provider of their choice, such as Google, to log in. Once returning, your auth provider will verify the user identity and generate a JWT that represents that user.
However, in the case of service clients, there is no user interaction, there is no password, so how do these service clients get valid tokens to call other services?
End user tokens are created by an Auth service. Tokens are JWTs. They are verified by using data saved in your Auth service.
Creating Service Client tokens
Just as we have end user tokens we'll want to have service tokens as well. To make security in the platform simple and consistent, these tokens should have the exact same form as the user tokens. That means they should look exactly like this:
We'll notice here that instead of the user ID present in the
sub claim from above, we want to see the service client ID.
Users get tokens by navigating through the authentication login flow. That flow is:
- Users register with a username, email, biometrics, WebAuthN, Face ID, etc...
- Then later, users navigate to the authentication service and use the same strategy as selected during registration.
- The user receives back a JWT that contains their username in the
We need a similar process for service clients as well:
- Register a service client and receive some credentials.
- Service client calls the authentication service with the credentials.
- The service client is returned a JWT that contains the service client ID in the
The credential generation options
Step (3) is the same as with the user login case. This means as long as the service client interacts with the same Auth service, they'll get back a valid service client JWT access token that can be easily verifiable. Step (2) can be accomplished if the auth service has an endpoint that accepts service client credentials and returns JWTs. The real question is how does step (1) happen, and what are credentials really?
Credentials are the strategy in which service clients identify themselves. What's important is that these credentials provide the service client a way to do that. Further, we need the Auth service here, because we need some way of verifying the credentials that are generated. Without that, any service client could generate any credentials and impersonate both your users and other services. That means we need some service that is trustworthy.
There are many ways for service clients to identify themselves. The core component is that the client can convey to the receiving service who they are. Some ways to do this are:
1. A plain text string that says I'm Service X
The service passes a string that literally says I am service X. The problems with this should be obvious. It means that any service can pretend to be any other service. However, when you have only a couple of services, this might not be a problem. But it would require that all these services are protected behind some complex firewall, because if they are public, your services will not be able to distinguish between one of your valid services and a malicious attacker attempting to impersonate your services. Since your services are probably handling requests from users as well this doesn't work in real production environments.
curl -XGET https://example.servire.com -H"Authorization: Service X"
2. An api key
When we say an API Key, we usually refer to a plain text string that is a generated by the Auth service, and is treated very similar to a password. When a service client wants a valid JWT it presents the API key, for which the Auth service can verify it. Often the API key is coupled to the specific service client. When you register the client in the Auth service, usually using the UI, you'll get back an API key. Many services with low security concerns out there will allow you to generate API keys for service client to interact with. Using API keys are susceptible to database vulnerabilities as well as potentially timing attacks. This is reviewed further in other academy articles, and won't be discussed here.
In OAuth2.0 this is the client_credentials grant.
3. Generated x509 certificate
x509 certificates are a complex strategy that enable the client to encrypt requests using that certificate. That are usually used in a scheme called mTLS. The problem with mTLS is that it requires a trusted certificate exchange in order to even generate the certificate.
If you can't guarantee the certificate exchange is secure then this opens opportunities for vulnerabilities, making it worse than an plain text api key. Additionally the generation of these certificates not easily done. Lastly, they often don't provide a meaningful level of security. That is, while they are secure, their generation is difficult, it is difficult to keep it secure, and they probably won't help at all.
Version: 3 (0x2)
Serial Number: 04:00:00:00:00:01:15:4b:5a:c3:94
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=BE, O=GlobalSign nv-sa, OU=Root CA, CN=GlobalSign Root CA
Subject: C=BE, O=GlobalSign nv-sa, OU=Root CA, CN=GlobalSign Root CA
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Exponent: 65537 (0x10001)
Signature Algorithm: sha1WithRSAEncryption
After the key exchange, payloads can be directly decrypted by the API, and verified that way.
4. An attestation provided by a third party service
Another way to provide assurances that a request is coming from the right service client, is to use yet another system that provides some sort of attestation for these requests. Systems such as WS-Federation, Kerberos tickets, and others, exist in the world. Most of these are no longer used prevalently because they lack the sophisticated configurability, are unnecessarily complicated to set up, lack integrations with other tools, do not provide managed or cloud native solutions, or just did not follow a standard such as OAuth.
5. Ed25519 public/private key pairs
The last option available is using asymmetric public/private key pars to sign and verify requests. This is the most secure option available. While some providers offer public/private key signatures, they use the weaker RS256 encryption method, however, this is still better than any of the other above alternatives.
EdDSA signatures created via Ed25519 is the norm. These pairs are created either by the Auth Service or the Service client and then exchanged. The Auth Service gets the public key, the Service Client gets the private key. From that point on, for every HTTP interaction request the service client will sign or create JWTs which the Auth service can verify. If the Auth service can verify the request from the signature that means other services can use the Auth service to verify these requests as well.
For the remainder of this academy article, we will assume that service clients use a
private key in the Ed25519 form. There are many reasons for this, but most of them boil down to alternatives are either unnecessarily complicated or unsafe.
In OAuth2.0 this is the urn:ietf:params:oauth:grant-type:jwt-bearer grant.
Securing request chains using JWT tokens
Now that we know how Credentials are created for our service client, we'll need to start using them. Here will review a few different request flows.
End user request flow
Here we'll review the flow when one of your users using your UIs makes a request to your service API.
- The User gets a JWT access token by logging and now has it available in your UI.
- From there your UI makes an API request to
Service Aneeds data from
Service Bso it makes a subsequent request.
In this circumstance, Service A can actually pass along the user's JWT access token to Service B. If the resources in Service B are actually owned by the User, then Service A doesn't need to generate its own token, it can utilize the User's.
This is the same flow that happens when a service asks for access to use your Google Drive. The token generated in the UI is passed through your service back to Google Drive to authenticate.
When Service A and Service B receive the User's JWT access token in the request, they must verify that token and then it must also check the valid authorization. We have to do this to make sure the user actually has access to the resource they are asking for.
Both services can verify the token the same way.
To verify the token, we will open it, grab the
iss, ask the
iss for the public keys associated with this token. Once we get back the public keys can we use them to verify the signature of the token.
Examples of how to authorize requests are available in the Authress Knowledge Base.
Direct service authorization
Sometimes however we can't use the end user's JWT. That's because one of these resources:
- The end user doesn't own the resources in Service B. They could be Service A's private resources, the security to access a database is one example. That means the user's JWT won't have access.
- The resources don't exist yet, and we need to create them. Sometimes resources can only be created by service clients and then granted to users. Resources before they are created are either claimable by anyone using something called Resource Claims, or are owned by Service A. In the case they are owned by Service, then Service A needs to call Service B as itself.
- The service isn't owned by you, but is actually owned by a third party developer who develops apps or plugins for your platform. This means your third party won't necessarily know what to do with your user's JWT AND even if they did, you would not want to grant them access by giving them a valid user token. (See more about this in Platform Extensions.)
In these cases,
Service A, will need to use its credentials to generate a valid JWT and then call
Using the credentials,
Service A can sign a request asking for a JWT, and then send that signed request to the
Auth Service. It will then get back a JWT that contains it's client ID as the
Alternatively, this same service client can perform what is known as Offline authentication by using their private key to generate a service client minted JWT:
We'll notice here, the
issuer has changed to be one that identifies
Service A as the issuer. Whether or not you choose offline or online authentication for your service clients is an implementation detail. It is more consistent to perform online, but offline offers a huge number of benefits.
Optional: Bring your own keys (BYOK)
Now with every integration between services secure we can technically move on to more important things. However, the secure storage of credentials is also not a trivial problem. More details are in how to securely store credentials. We'll remember from above, the critical components are:
- A public/private key pair to sign and verify JWT tokens
- An Auth service to store the public keys.
That means Auth services, including Authress don't necessarily care where the public/private key pair comes from. Any pair can be used, so long as it provides modern asymmetric cryptography. For Authress, this means you can generate your own EdDSA keys or even bring your AWS Key Management Service (KMS) keys to use with Authress. For this, only updating the public key is required and can be done as part of your CI/CD process or via the the Authress Service Client API.
Why not send the service client credentials on every request?
It works the same as with user passwords in browsers. You only send your password on the login page, then the site generates a session credential. For every subsequent request only the session credential is used and not the password. This is so that the password is not present in every request. Ideally, the login page is a separate website that has more security around dependency management and development workflows to prevent password <=> session token attacks. Now, the login page specifically has to be compromised instead of any one of a numerous number of front end applications. That's easy to control for. Additionally, the more services that have access to credential generation processes, such as the password, the larger your attack surface is, and the more places the password can end up in logs.
Further, other services have no idea what do with the service client credentials.
Service B can't handle
Service A credentials only the
Auth service knows they are valid. Worse still is that if the credentials are sent, then
Service B could impersonate
Service A and get access
Service A's private resources. Giving someone else your credential, is the same as giving them your password, they can impersonate you. Services are no exception to this rule.