Posts / Engineering in practice
Verifying JWTs across microservices with JWKS
How I separate token issuance, verification, and permissions across services, with Django examples from my work on inTalent.
Once authentication belongs to one service, every other service still has a decision to make: why should it trust the token attached to this request?
I worked on this boundary in inTalent, where identity, certification, and job applications have separate backend responsibilities. The identity service issues tokens. The services handling business operations verify them and apply their own access rules. Django is the implementation I use here, but the boundary is the same in a service written in another language.
The token does not get to choose who I trust
The identity backend signs access tokens using RS256 and holds the private signing key. It also publishes public verification keys as a JSON Web Key Set, or JWKS. A consuming service can verify a signature without receiving the ability to issue tokens itself.
That separation is useful, but it is only useful if the consumer already knows which issuer it trusts. I configure the issuer, audience, accepted algorithm, and key source on the service side. The token’s kid identifies a candidate key within that trusted set. It is a lookup hint, not permission to fetch keys from an arbitrary address supplied by the caller.
JWKS gives the consumer a standard key representation. It does not, on its own, implement a safe rotation policy. Caching, refresh failures, and how long an old key remains acceptable are separate operational decisions.
CONCEPTUAL TRUST BOUNDARY / RSA-SIGNED TOKEN EXAMPLE
- 01 / IDENTITY SERVICEPrivate key signs the token.
The configured issuer publishes public verification keys through its JWKS.
- 02 / RESOURCE SERVICEVerify, then validate claims.
Use an allowed key and algorithm. Check issuer, audience, and expiration.
- 03 / ACCESS DECISIONApply permissions.
Check whether this identity may act on this resource.
I want one clear path from token to identity
In the Django services, the authentication layer handles verification before the view performs its business operation. That keeps the same checks from being re-created, and gradually diverging, across individual endpoints.
The useful distinction is between reading a token and accepting it. A JWT payload can be decoded without proving who created it. My consumer needs a valid signature from an allowed key and algorithm, then claims that match this particular service: the expected issuer, the intended audience, an acceptable expiry, and the expected token type.
SIMPLIFIED VERIFICATION EXAMPLE
policy = service.authentication_policy
key = policy.trusted_jwks.find_key(token.header.kid)
claims = verify_signature_and_claims(
token,
key,
algorithms = policy.allowed_algorithms,
issuer = policy.issuer,
audience = policy.audience,
require = ["exp", "iss", "aud"]
)
require_token_type(claims, "access")
identity = resolve_identity(claims.subject)
return identityThis sketch leaves out library-specific errors and key-cache handling. In an implementation, malformed tokens and failed checks must stop authentication; they must not fall through to a partially trusted user. The library’s claim-validation settings need explicit review rather than an assumption that every check is enabled by default.
A valid signature can still end in rejection
An expired token can have a perfectly valid signature. So can a token intended for a different API. I find it easier to explain the difference by walking through a request than by treating “JWT valid” as one yes-or-no box.
Follow a request.
INTERACTIVE EXPLANATIONNO CRYPTOGRAPHY RUNS
Static synthetic fixtures, a fixed evaluation time, and predetermined outcomes. No real token, signing key, or network request is used.
- 01Token issuance
- 02Signature verification
- 03Claim validation
- 04Authorization
Step 1 of 4 · Issued / not yet trusted by the service
The issuer signs a token.
In this example, the identity service uses a private signing key. The token identifies a subject and intended audience. The resource service still has to verify it.
- Synthetic issuer (iss)
- identity.demo.invalid
- Audience (aud)
- certification-api
- Expiration / fixed evaluation time
- 13:00 UTC / 12:00 UTC
Same illustrative day - Permission / requested action
- certification:issue
Requested: certification:issue
The interactive example uses predetermined results. It illustrates the order of the decisions; it does not execute the authentication code or test a live inTalent service.
Access, refresh, and service tokens have different jobs
In the identity backend, an access token is used for ordinary authenticated requests. A refresh token is used with the issuer to obtain fresh access credentials. Accepting a refresh token at a normal business endpoint would blur that separation, so the consuming APIs check the token type as well as its signature.
I also worked on refresh rotation and revocation. A refresh can be consumed transactionally and replaced, while account eligibility and current identity claims are checked again. That is different from instantly invalidating every existing access token across every service. An already-issued access token may remain acceptable until expiry unless the system provides another revocation mechanism.
Service identities are another boundary. A backend calling another backend should receive explicitly granted service scopes. A trusted service is not automatically an administrator, and a service credential should not silently inherit the permissions of a human user.
Knowing the caller is the start of the access decision
After authentication, the service still owns authorization. Can this caller perform this operation? Does the record belong to the relevant organization? Is the operation valid in the record’s current state? Those answers depend on the service’s data and rules.
For example, a caller allowed to read a certification is not necessarily allowed to issue one. A claim naming the right API does not replace that permission check. I want the request’s identity and its requested action to remain separate inputs to the decision.
A signed JWT is not an encrypted payload. I avoid treating claims as a private place to store application data. Signing lets the consumer check integrity and origin; it does not hide the content from whoever holds the token.
The practical result is a boundary I can follow: the issuer creates identity credentials, the consumer verifies their applicability, and the business service decides what the caller may do.
References
The examples above are original explanations. These specifications describe the underlying formats and validation guidance.