An innovative approach to enhancing authentication, authorization, and tracing in microservices architectures using nested JWTs.
- Introduction
- Motivation
- Problems Addressed
- Solution Overview
- Key Concepts
- Implementation Guidelines
- Security Considerations
- Performance Considerations
- Limitations
- Future Work
- Contributing
- License
- Acknowledgments
The Matryoshka JWT Pattern is a novel approach to managing authentication, authorization, and tracing in microservices architectures. It involves nesting JSON Web Tokens (JWTs) within JWTs, much like Russian Matryoshka dolls, to create an authentication stack that can be passed along service calls.
This pattern enhances visibility into the call chain, improves security by verifying the entire chain of calls, aids in debugging by providing a built-in call stack within the tokens themselves, and helps prevent recursive or circular service calls.
In complex microservices architectures, services often call other services, leading to deep and sometimes circular call chains. Traditional methods may lack the ability to:
- Trace these calls effectively.
- Enforce security policies based on the entire call chain.
- Prevent recursion and circular dependencies.
- Provide services with context about their position in the call chain.
The Matryoshka JWT Pattern addresses these challenges by embedding the call history within the tokens themselves.
-
Lack of Call Chain Visibility:
- Problem: Services are unaware of how deep they are in a call chain or which services have been called before them.
- Solution: The pattern embeds an authentication stack within the JWT, allowing services to see the entire call chain up to that point.
-
Difficulty in Debugging and Monitoring:
- Problem: Tracing issues in microservices can be challenging without clear visibility into service interactions.
- Solution: By providing a built-in call stack, the pattern facilitates easier debugging and monitoring of service calls.
-
Unauthorized Service Access:
- Problem: Services may inadvertently call or be called by unauthorized services.
- Solution: The authentication stack allows services to verify the entire chain of callers, ensuring that only authorized services are involved.
-
Circular Dependencies and Recursion:
- Problem: Circular service calls can lead to infinite loops, stack overflows, or performance degradation.
- Solution: The pattern enables detection of recursion by inspecting the call chain for repeats and limits the call depth to prevent excessive recursion.
-
Inefficient Handling of Deep Call Chains:
- Problem: As call chains deepen, tokens may become large, and performance may suffer.
- Solution: Compression and encryption mechanisms reduce token sizes and protect sensitive information.
The Matryoshka JWT Pattern proposes that each service, upon receiving a request, generates a new JWT that includes a custom claim called auth_token
. This claim contains the previous token (from the caller), forming a chain of tokens representing the call stack.
Key features of the solution include:
- Embedding the Call Chain: Nesting tokens within tokens to build an authentication stack.
- Enhanced Security: Verifying the entire call chain to enforce security policies.
- Recursion Prevention: Detecting and preventing circular calls by analyzing the call chain.
- Compression and Encryption: Using compression to reduce token size and encryption to protect sensitive data.
The Auth Stack is the chain of nested JWTs representing the sequence of service calls. Each token in the stack contains metadata about the previous call, allowing services to trace back through the call chain and make informed decisions based on the entire sequence of calls.
The auth_stack
claim contains a JSON object with the following fields:
-
fmt
(Format):- Indicates the format of the token contained within the
container
field. - Possible values:
"JWT"
: Standard JSON Web Token."JWE"
: JSON Web Encryption token (encrypted JWT).
- Purpose:
- Allows services to know how to process the nested token.
- Enables the use of encrypted tokens (JWEs) for added security, especially across organizational boundaries.
- If a service cannot decrypt the JWE, it processes as much of the auth stack as it can, maintaining security boundaries.
- Indicates the format of the token contained within the
-
cmp
(Compression):- Specifies the compression algorithm used on the token container.
- Possible values:
"g"
: GZip compression (Good)"b"
: Brotli compression (Better)null
or omitted if no compression is used.
- Purpose:
- Reduces the size of the nested tokens.
- Addresses concerns about token size and performance impact.
- Compression is only recommended to help alleviate any compatibility issues with applications handling large HTTP header values
- Because the compressed results are Base64 encoded, only
auth_stack
containers with 3 or more entries should be compressed as shorter entries could result in a longer result than the uncompressed value
-
container
:- Contains the actual nested token (JWT or JWE), which itself may have an
auth_stack
claim. - If
cmp
is specified, contents will be a base64-encoded representation of the actual nested token (JWT or JWE), and will need to be decompressed first. - Purpose:
- Holds the previous JWT/JWE in the call chain.
- By recursively including the
auth_stack
in each token, the call stack is built.
- Contains the actual nested token (JWT or JWE), which itself may have an
-
sid
(Service ID):- Specifies the Service ID of the service that signed the current parent JWT, and is the top of the stack for the
auth_stack
- This can be an internal organizational identifier or a human readable namespace
- Purpose:
- Allows identification of services that are part of the
auth_stack
- Can be used to restrict access to services if a service in the chain is not permitted (crossing permissions boundaries)
- Allows identification of services that are part of the
- Specifies the Service ID of the service that signed the current parent JWT, and is the top of the stack for the
-
depth
:- Indicates the current
auth_stack
depth at the current point. - Purpose::
- Explicitly states the
auth_stack
depth at the given point - This is an optional, informational field for Services to provide to downstream consumers who lack access to decrypt the
auth_stack
token secured with JWE - Because this can potentially reveal information about your organizations service architecture and topography, it is not recommended for use beyond debugging
- Explicitly states the
- Indicates the current
To prevent recursion and circular dependencies:
-
Call Chain Inspection:
- Each service inspects the
auth_stack
chain to identify if it has already been called in the current request flow. - If the service's identifier appears in the call chain, it can abort the call to prevent recursion.
- Each service inspects the
-
Maximum Call Depth:
- Services can enforce a maximum call depth by checking the length of the
auth_stack
chain. - If the call depth exceeds a predefined limit, the service can deny the request to prevent excessive recursion.
- Services can enforce a maximum call depth by checking the length of the
-
Unique Identifiers:
- Including unique service identifiers in tokens helps in detecting repeats in the call chain.
-
Policy Enforcement:
- Organizations can define policies to handle recursion detection, such as logging occurrences, sending alerts, or blocking the request.
-
Compression:
- Purpose: Reduce token size to improve performance.
- Methods: Use algorithms like gzip.
- Implementation: Compressed tokens are base64-encoded and indicated by the
cmp
field.
Implementation of compression is only recommended in order to maintain compatibility with applications that run into issues with large HTTP headers. While the compression ratio increases as the size of the uncompressed token increases, the added compute overhead will negate any performance offsets in transmission times. The below table shows simulated examples of compression ratios for JWT tokens implementing the Matryoshka pattern at given depths:
Depth | Uncompressed Size | GZip+Base64 | Brotli+Base64 |
---|---|---|---|
1 | 529 | 572 (1.08x) | 556 (1.05x) |
2 | 1163 | 1132 (0.97x) | 1120 (0.96x) |
3 | 1903 | 1884 (0.99x) | 1868 (0.98x) |
4 | 2965 | 2768 (0.93x) | 2748 (0.92x) |
5 | 4288 | 3944 (0.91x) | 3957 (0.92x) |
6 | 6105 | 5496 (0.90x) | 5468 (0.89x) |
7 | 8528 | 7452 (0.87x) | 7256 (0.85x) |
8 | 11759 | 10076 (0.85x) | 9604 (0.81x) |
Note: Enabling HTTP Compression on your applciation endpoint will not compress the JWT Token, as HTTP compression only appplies to the payload body and not the headers.
-
Encryption (JWE):
- Purpose: Protect sensitive data within tokens, especially when crossing organizational boundaries.
- Methods: Use JSON Web Encryption standards.
- Implementation: Encrypted tokens are indicated by the
fmt
field set toJWE
.
(All examples use the shared secret example
for their HMAC-SHA256 signatures and hash)
JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhdXRoX3N0YWNrIjp7ImZtdCI6Imp3dCIsImhhc2giOiI0MGJhMTk4ODg3YzIyM2I4YmIzYzY1MTg3ZDYxMDgwMjFiMDQ4MjAyNWYxOTMyMzVhMjk3Yjk3MzVhMGNmMDE5IiwiZGVwdGgiOjEsImNvbnRhaW5lciI6ImV5SmhiR2NpT2lKSVV6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS5leUp6ZFdJaU9pSXhNak0wTlRZM09Ea3dJaXdpYm1GdFpTSTZJa3B2YUc0Z1JHOWxJaXdpYVdGMElqb3hOVEUyTWpNNU1ESXlmUS5pVUNST0h0NkpIQU5kdHpUNmFPdVVnT3FWRlJhbE9XMjBTYnpSc241U2tJIn19.ud_wwYeKCxj5AW5sRSopMBT2h-oZCnqT-BfLlgjHlmQ
JSON:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"auth_stack": {
"fmt": "jwt",
"sid": "Organization.Services.ServiceA"
"depth": 1,
"container": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.iUCROHt6JHANdtzT6aOuUgOqVFRalOW20SbzRsn5SkI"
}
}
In the above example, a client (Client1
) has called ServiceA
which has then generated a JWT token to send to a downstream service (ServiceB
), implementing the Matryoshka JWT Pattern by embedding the original JWT token from Client1
in the auth_stack
claim.
JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIzLCJhdXRoX3N0YWNrIjp7ImZtdCI6Imp3dCIsImNtcCI6ImciLCJoYXNoIjoiZWU4OGJiNjc1M2QyMjViY2U5NWRiYmNmNjMzN2NhZDNmNzgyMThjZmI1NjMxMzY0OGNhMDQ0M2NiYjZjMjYyOSIsInNpZCI6Ik9yZ2FuaXphdGlvbi5TZXJ2aWNlcy5TZXJ2aWNlQyIsImRlcHRoIjozLCJjb250YWluZXIiOiJINHNJQUFBQUFBQUFBMVZTeVphYk9CVDlvdVFBTnBYMnNsMGdESTdrc3RDRWRtSklBWkpzWE1iRjhQVXRWM0s2VDYra045OTczMnVXckMyVHFqdDFXVXJYMUVkZGVrOHZPS3hlMDVkVUQ0SzlacnZ2elpLdE5VOWRVanJESG5xSUZKdFRwS2UwbTdyU2dsSG1YOG1mS3RtKzQyUm5ubjdGZ1pmMjF4bVJPSEExSVl6UzllZHIxdFlDWDhVR2VRVkhIMmsvL0VpdEhPdm5NRHRzM0h0UExScS9odHVxYy9uWFFxQ3I2OU5KRGowWXdma1VJWXZzMlplUnRDZ3BKdFMvYjArY0xvZ3pMUWxjQ3lKYjJOTVpCZWxjckhwR050NGdJZzFhWlNjVDZxR0l6akQ2ZXlsNHVwWFBlUmMweUMreXUwVUc0S0hFME5hSitTeTdrRWlSQllvalV5My8vYzlQZ1N3MjFRRS9jUzBPWTE4R29lTmpIbEk4TmNwTVE0ZHJvZXRlMGQyUWE4TWFyV2RpVFZaNTVrRjcxcDg5SGVSOG5EaVFVZDVyVjJlT2pjVWk1K1pOVWJNbGZFQXdEaWtuNkEwbjQ4YjVONHBuSTc1Z1JmL1UxaFljVUZMbmVEWDMzM0VnY0wvUGxOMXRpY0VBYXY5R0NFTTROaThrUmtlVjRJTGFYUUc5N2NSWmZhcTB1VlZSblpWczhHU01NcVROV0VWSW56Mno1dHkvcWdRNlBaNTQ5RVJvZlZTeFQ5V0tIWjVxNVdMSUd1b3RPY0d5cE9iRjJSSlNuOG9WS1F3Y2Zvb3Z0ZWZmc0RVSWc5aHpmUFlORFFVWDdSdUwvWkNJZ2J0K043VWlxZXg5L1dNZmlQVWRYdXIvNWdPWE1obWZmQVlXb1dOcGpFQmliMHBhVEFXcndwck9kMkxudmpURHhFbkdHLzEvKzZ6TlVBZEdZVjl1WkZ4ZjNDNXVXS0NzcEdGUTJEQlhDUkNJejI1SFR6MTlqaGdhVldKeXFNT0pSZGs5OTkxRjlaS3dXTDRWak4wYmc0VExPVEV2ZEFpeHltMjhubzNoeWd0L0VoLzZaY0pZRlVtRkxUNFdqSVlWUUlXS3cxYXQrdzFMNXB4cmJ6cFIxc0xnR1c5MUJjd0lhYTN4WlUvLzdkZG5rbkU0OFI0YzgzZ25tbmhvZVlMZHZxVEduZy9LQStoZzREVFVqRFVNaXlvT1Q0d0FRUDMyWGpKR25EN2NqU1lzMmwvZC9YeFVzY0dNc0E4WnQ1UHpqOWpzTlRQeTRmUk1tTzg0MnJiRHhCUnVQNjhNbUlmRDNNUCtQRE9TZWU1dUg1RG9SOE5aU25QUE8ydDBsbjdzRllibEpXQjNUa0hHb2pweG5BQTNBNnBqZkNWSnV5Mk52TEZBYjNtZTdYNmR2KzgranAvTllxdXNlZTlQY042a2EzNVozci85QlE2aVNOOVoxRGF2MTdSc3Y4MHZ4VC9NWDNVSWl3UUFBQT09In19.DQRSDDZG2CUBNDwQxpaz79SFf2dDeGaUyw8HN9Aaqw4
JSON:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239023,
"auth_stack": {
"fmt": "jwt",
"cmp": "g",
"sid": "Organization.Services.ServiceC",
"depth": 3,
"container": "H4sIAAAAAAAAA1VSyZabOBT9ouQANpX2sl0gDI7kstCEdmJIAZJsXMbF8PUtV3K6T6+kN99732uWrC2Tqjt1WUrX1Eddek8vOKxe05dUD4K9ZrvvzZKtNU9dUjrDHnqIFJtTpKe0m7rSglHmX8mfKtm+42Rnnn7FgZf21xmROHA1IYzS9edr1tYCX8UGeQVHH2k//EitHOvnMDts3HtPLRq/htuqc/nXQqCr69NJDj0YwfkUIYvs2ZeRtCgpJtS/b0+cLogzLQlcCyJb2NMZBelcrHpGNt4gIg1aZScT6qGIzjD6eyl4upXPeRc0yC+yu0UG4KHE0NaJ+Sy7kEiRBYojUy3//c9PgSw21QE/cS0OY18GoeNjHlI8NcpMQ4droete0d2Qa8MarWdiTVZ55kF71p89HeR8nDiQUd5rV2eOjcUi5+ZNUbMlfEAwDikn6A0n48b5N4pnI75gRf/U1hYcUFLneDX333EgcL/PlN1ticEAav9GCEM4Ni8kRkeV4ILaXQG97cRZfaq0uVVRnZVs8GSMMqTNWEVInz2z5ty/qgQ6PZ549ERofVSxT9WKHZ5q5WLIGuotOcGypObF2RJSn8oVKQwcfoovteffsDUIg9hzfPYNDQUX7RuL/ZCIgbt+N7Uiqex9/WMfiPUdXur/5gOXMhmffAYWoWNpjEBib0paTAWrwprOd2LnvjTDxEnGG/1/+6zNUAdGYV9uZFxf3C5uWKCspGFQ2DBXCRCIz25HTz19jhgaVWJyqMOJRdk9991F9ZKwWL4VjN0bg4TLOTEvdAixym28no3hygt/Eh/6ZcJYFUmFLT4WjIYVQIWKw1at+w1L5pxrbzpR1sLgGW91BcwIaa3xZU//7ddnknE48R4c83gnmnhoeYLdvqTGng/KA+hg4DTUjDUMiyoOT4wAQP32XjJGnD7cjSYs2l/d/XxUscGMsA8Zt5Pzj9jsNTPy4fRMmO842rbDxBRuP68MmIfD3MP+PDOSee5uH5DoR8NZSnPPO2t0ln7sFYblJWB3TkHGojpxnAA3A6pjfCVJuy2NvLFAb3me7X6dv+8+jp/NYqusee9PcN6ka35Z3r/9BQ6iSN9Z1Dav17Rsv80vxT/MX3UIiwQAAA=="
}
}
In the above example, we're now three layers deep in our call stack (Client
-> ServiceA
-> ServiceB
-> ServiceC
), and the above token has been generated by ServiceC
to make calls further down the stack. The container
property in the auth_stack
payload has been compressed using the specified GZip compression and Base64 encoded.
-
Technology Stack:
- Modern Web Frameworks capable of processing JWT and JWE Tokens
- .NET 8 or later (for example prototype implementation).
- Familiarity with JWT, JWE, and microservices architectures.
-
Infrastructure:
- Secure key management system for handling encryption keys.
- Shared trust framework among services for token validation.
-
Create Initial Token:
- Service generates a JWT with standard claims and any necessary custom claims.
- No
auth_stack
claim is included if it's the initial token.
-
Nesting Tokens:
- When a service calls another service, it generates a new JWT.
- The previous token is embedded in the
auth_token
claim of the new token. - Apply compression and/or encryption as needed.
-
Metadata Inclusion:
- Include necessary metadata in the
auth_stack
claim fields (fmt
,cmp
,sid
).
- Include necessary metadata in the
-
Token Validation:
- Upon receiving a token, a service validates it according to standard JWT validation procedures.
- Verify signatures, issuer, audience, expiration, and other claims.
-
Extracting the Auth Stack:
- Parse the
auth_stack
claim to retrieve the nested token. - Decompress and decrypt the
container
as indicated by thecmp
andfmt
fields. - Recursively process nested tokens to build the full call chain.
- Parse the
-
Recursion Detection:
- Inspect the call chain for repeats of service identifiers.
- Compare the current service's identifier with those in the call chain.
- If recursion is detected, handle according to policy (e.g., deny the request).
-
Call Depth Limitation:
- Determine the depth of the call chain by counting the nested tokens.
- If the call depth exceeds the maximum allowed, deny the request.
-
Automated Processing:
- Implement middleware components to automate token processing.
- Middleware handles extraction, validation, and parsing of the
auth_stack
claim.
-
Integration with Frameworks:
- Extend existing authentication and authorization frameworks to support the pattern.
- Ensure compatibility with existing services by ignoring the
auth_stack
claim if not implemented.
-
Key Management:
- Securely store and manage encryption keys.
- Use key management services (e.g., AWS KMS, Azure Key Vault).
- Implement key rotation policies.
-
Encryption Algorithms:
- Use robust encryption algorithms for JWEs (e.g., RSA-OAEP, AES256-GCM).
- Ensure that only intended recipients can decrypt the tokens.
-
Access Control:
- Limit the information included in tokens to what is necessary.
- Avoid including sensitive data unless absolutely required.
-
Cross-Boundary Security:
- When crossing organizational boundaries, use JWEs to protect internal tokens.
- Ensure that external services cannot access internal call chain details unless authorized.
-
Token Size:
- Monitor token sizes to prevent exceeding header limits.
- Use compression to mitigate token size growth.
-
Processing Overhead:
- Be aware of the CPU overhead introduced by compression and encryption.
- Optimize performance by caching public keys and using efficient algorithms.
-
Network Latency:
- Larger tokens may impact network transmission times.
- Compression helps reduce latency caused by increased token sizes.
-
Scalability:
- Test the pattern under load to assess scalability.
- Ensure that services can handle the additional processing without significant degradation.
-
Complexity:
- Increased complexity in token generation and processing.
- Requires careful implementation and thorough testing.
-
Compatibility:
- May not be compatible with all JWT libraries or tools.
- Custom parsing logic may be necessary.
- Default Maximum Header Sizes within Nginx is 8k
-
Performance Impact:
- Potential performance impacts due to processing overhead.
- Needs optimization to minimize impact.
-
Key Distribution:
- Requires secure distribution and management of public keys across services.
- Key compromise can have widespread effects.
-
Tooling Support:
- Develop libraries or extensions to simplify implementation.
- Create middleware packages for popular frameworks.
-
Standardization:
- Propose the pattern as a standard or contribute to existing standards.
- Engage with the developer community for feedback and adoption.
-
Integration:
- Explore integration with service meshes or other architectural patterns.
- Leverage existing infrastructure for authentication and tracing.
-
Security Enhancements:
- Implement additional security features and conduct regular audits.
- Stay updated with the latest security best practices.
Contributions are welcome! Please open issues or pull requests for enhancements, bug fixes, or suggestions.
-
Fork the Repository:
- Click the "Fork" button at the top right of the repository page.
-
Create Your Feature Branch:
git checkout -b feature/YourFeature
-
Commit Your Changes:
git commit -m 'Add some feature'
-
Push to the Branch:
git push origin feature/YourFeature
-
Open a Pull Request:
- Navigate to your forked repository and click "New Pull Request".
This project is licensed under the MIT License - see the LICENSE file for details.
-
Inspiration:
- Derived from challenges faced in microservices architectures regarding authentication and tracing.
-
Community Support:
- Contributions and feedback from the developer community.
-
Technologies:
- Built upon the capabilities of JWT, JWE, and modern cryptographic practices.
Disclaimer: This pattern is experimental and should be thoroughly tested and reviewed before use in production environments. Security considerations are critical when handling authentication tokens.