The most surprising thing about Keycloak JWT validation is that you’re probably doing it wrong, and Keycloak is letting you.
Let’s see this in action. Imagine a simple microservice, product-service, that needs to know who’s asking for product details. It’s written in Go.
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"github.com/dgrijalva/jwt-go" // Deprecated but illustrates the point
)
const (
// This should be fetched dynamically from Keycloak's .well-known endpoint
// For demonstration, we use a hardcoded value.
keycloakPublicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApN4k+4eFfN2yq1j6u6lJ
... (rest of the public key) ...
-----END PUBLIC KEY-----`
)
func main() {
http.HandleFunc("/products", func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header missing", http.StatusUnauthorized)
return
}
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect!
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// This is the crucial part: using the correct public key
return jwt.ParseRSAPublicKeyFromPEM([]byte(keycloakPublicKey))
})
if err != nil {
http.Error(w, fmt.Sprintf("Invalid token: %v", err), http.StatusUnauthorized)
return
}
if !token.Valid {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// In a real app, you'd extract claims, check roles, etc.
// For now, we just confirm it's a valid JWT.
fmt.Fprintf(w, "Token is valid! Welcome, user!\n")
})
log.Println("Starting server on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
This Go service expects a Bearer token in the Authorization header. It then attempts to parse and validate it using a hardcoded public key. The jwt.Parse function, when configured correctly, will verify the signature against the public key. If the signature is valid and the token hasn’t expired (though expiration isn’t explicitly checked in this simplified example), the token is considered valid.
The problem this solves is authentication and authorization in a distributed system. Instead of every service needing to know about user passwords or directly query Keycloak for every request, Keycloak issues a JWT. This JWT acts as a verifiable credential. The product-service only needs Keycloak’s public key to verify the authenticity of the token. What it does with the information inside the token (like user ID, roles, scopes) is its own business.
Internally, Keycloak signs JWTs using private keys. When a service receives a JWT, it uses the corresponding public key (which Keycloak makes available) to verify that the token was indeed issued by Keycloak and hasn’t been tampered with. The JWT itself contains claims – pieces of information about the user or the token. Common claims include iss (issuer), sub (subject), aud (audience), exp (expiration time), and custom claims like realm_access (roles).
The exact levers you control are primarily in how you configure Keycloak’s client settings and how your services retrieve and use the public key. Keycloak publishes its public keys, along with other OIDC/OAuth2 discovery information, at a .well-known/openid-configuration endpoint. For example, http://your-keycloak-host:8080/realms/your-realm/.well-known/openid-configuration. From this, you can find the jwks_uri, which points to a JSON Web Key Set (JWKS) containing the public keys. Your services should fetch these keys from this URI dynamically, rather than hardcoding them.
Most people don’t realize that the aud (audience) claim in a JWT is a list. This is incredibly powerful for microservices. When Keycloak issues a token, you can specify which clients (your microservices) are allowed to consume that token. If a token is issued with aud: ["product-service", "order-service"], then product-service should check if "product-service" is present in the aud claim. If it’s not, the token is invalid for that service, even if the signature is correct. This prevents a token intended for one service from being accepted by another, a common security oversight.
The next concept you’ll run into is managing token revocation and session invalidation across distributed services.