Skip to main content

Distributed Trust with Service-to-Service Authentication

·1353 words·7 mins
A photo of a locked gate with padlock and chain.
Photo by Jose Fontano

In Service-oriented Architectures (SOA) the “services” are the fundamental building blocks of your application. Services are autonomous, loosely coupled and typically stateless. They are autonomous in that they are independent of the state or context provided by other services. Loosely coupled in that they are independent of the technologies used by other services. Stateless in that they do not maintain state between requests.

Services are the building blocks of your application and they are the building blocks of your security architecture.

Distributing Trust #

In a SOA, services are independent of one another. This means they are also independent of the security mechanisms used by other services.

This is a good thing.

It means that you can use different security mechanisms for different services. You can use different authentication mechanisms, different authorization mechanisms, different encryption mechanisms, etc.

This is also a bad thing.

It means that you have to manage different security mechanisms for different services. You have to manage different authentication mechanisms, different authorization mechanisms, different encryption mechanisms, etc.

Service-to-Service Authentication #

In a SOA, services are independent of one another. You can see the theme here.

Authenticating requests between services is a challenge. You can’t use the same mechanisms you use for authenticating users. You can’t use cookies, you can’t use sessions and you especially don’t want to rely on an Identity Provider (IdP).

With Platform-as-a-Service (PaaS) offerings like Cloudflare Workers, Heroku, Cloud Foundry and Vercel you can’t even rely on the underlying infrastructure to provide you with a secure channel between services.

You need a way to authenticate requests between services that is independent of the underlying infrastructure and independent of the security mechanisms used by other services.

This is where JWTs & JWKSs come in.

JSON Web Tokens (JWTs) #

JSON Web Tokens (JWTs) are a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.

JWT Structure #

A JWT is a string consisting of three parts: the header, the payload, and the signature. The header and payload are JSON objects. The signature is a Base64 encoded string.

<base64url-encoded header>.<base64url-encoded payload>.<base64url-encoded signature>

JWT Header #

The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.

{
  "alg": "HS256",
  "typ": "JWT"
}

JWT Payload #

The payload contains the claims. Claims are statements about an entity (typically, the user) and additional metadata. There are three types of claims: registered, public, and private claims.

  • Registered claims: These are a set of predefined claims which are not mandatory but recommended, to provide a set of useful, interoperable claims. Some of them are: iss (issuer), exp (expiration time), sub (subject), aud (audience), and others.
  • Public claims: These can be defined at will by those using JWTs. But to avoid collisions they should be defined in the IANA JSON Web Token Registry or be defined as a URI that contains a collision resistant namespace.
  • Private claims: These are the custom claims created to share information between parties that agree on using them.
{
  "sub": "1234567890",
  "iss": "https://my-experience-api.willhackett.xyz",
  "aud": "https://my-resource-api.willhackett.xyz",
}

The iss and aud claims are used to identify the issuer and audience of the JWT respectively. The sub claim is used to identify the subject of the JWT. In this case, the subject is the user ID the request is being made on behalf of. In a real world example, I would recommend re-packing the user’s requesting JWT into the new JWT. This would allow the receiving service to verify the user’s identity and to verify that the user has the appropriate permissions to perform the requested action at your resource layer.

The issuer & audience will come into play later when I discuss trust boundaries.

JWT Signature #

To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

The signature is used to verify the message wasn’t changed along the way, and, in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.

JSON Web Key Sets (JWKSs) #

JSON Web Key Sets (JWKSs) are a set of keys containing the public keys used to verify any JSON Web Token (JWT) issued by the authorization server and signed using the RS256 signing algorithm.

JWKS Structure #

A JWKS is a JSON object that represents a set of JWKs. The JSON object MUST have a “keys” member, which is an array of JWKs. This is the JWK Set format.

{
  "keys": [
    { "kty": "EC",
      "crv": "P-256",
      "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
      "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
      "use": "enc",
      "kid": "1" },
    { "kty": "RSA",
      "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFb.....<remainder of RSA key omitted>.....ZU3e_TURo9-F1bp-hqOY_BLPbEx-Rmc6XaXk",
      "e": "AQAB",
      "alg": "RS256",
      "kid": "2011-04-29" }
  ]
}

In our case, we are only interested in the public keys. The public keys are used to verify the signature of the JWTs.

Trust Model #

The trust model is simple. The service that issues the JWT is trusted to issue valid JWTs. The service that receives the JWT is trusted to verify the JWT. The service that receives the JWT is not trusted to issue valid JWTs on behalf of the calling service.

If Service A issues a JWT to Service B, Service B is trusted to verify the JWT against the JWKS provided by Service A. Service B cannot sign a JWT and expect Service C to trust it. This is where the trust boundaries come into play.

Trust Boundaries #

Trust boundaries are the boundaries between services that trust each other.

If Service A issues a JWT to Service B, Service B is trusted to verify the JWT against the JWKS provided by Service A.

Each JWT contains an iss claim and an aud claim. The aud is the intended audience of the JWT, this can be used to validate that the request is destined for the correct service. The iss is the issuer of the JWT, this can be used to validate that the JWT was issued by a trusted service.

Service B trusts Service A to issue valid JWTs. Assuming Service A’s hostname is service-a.willhackett.xyz and Service B’s hostname is service-b.willhackett.xyz, the issuer claim would be https://service-a.willhackett.xyz and the audience claim would be https://service-b.willhackett.xyz.

Service B would trust tokens issued with the issuer claim https://service-a.willhackett.xyz and the audience claim https://service-b.willhackett.xyz. Service B would call to Service A’s JWKS endpoint to retrieve the public keys used to verify the JWTs issued by Service A.

Service Mesh #

It’s probably worth asking, why not use a service mesh? Service meshes are great, but they are not a silver bullet. They unfortunately aren’t a good fit for all use cases. As developing for the web becomes more about developing for the edge, or developing on a platform, service meshes become less and less useful.

Internal to your infrastructure, a service mesh is a great solution. Deploying containers with a sidecar proxy is a fantastic way to standardize service-to-service communication. But what about when you’re deploying to a platform like Cloudflare Workers, Heroku, Cloud Foundry or Vercel?

With platforms, you need to rely on standards.

Other thoughts #

With this distributed trust model, you would need to ensure the RSA keys generated by your runtime are consistent as you horizontally scale. That way the JWKS call would always return the same public keys.

This is just one standard for HTTP service-to-service authentication. You could alternatively rely on mutual TLS (mTLS), shared secrets, etc. The important thing is that you have a standard that is independent of the underlying infrastructure and independent of the security mechanisms used by other services.

References #

Author
Will Hackett
I’m Will, a software engineer and architect based in Melbourne, Australia. I’m currently working at Blinq.