Keycloak’s OIDC integration with Go isn’t about blindly trusting tokens; it’s about a sophisticated dance of verification, ensuring the digital handshake is legitimate and secure.

Let’s see this in action. Imagine a simple Go web server that needs to protect a /protected endpoint.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/coreos/go-oidc/v3/oidc"
	"golang.org/x/oauth2"
)

var (
	oauth2Config *oauth2.Config
	oidcProvider *oidc.Provider
	verifier     *oidc.IDTokenVerifier
)

const (
	keycloakRealm      = "myrealm"
	keycloakClientID   = "myclient"
	keycloakClientSecret = "myclientsecret"
	keycloakIssuerURL  = "http://localhost:8080/auth/realms/myrealm"
)

func init() {
	var err error
	oidcProvider, err = oidc.NewProvider(http.DefaultClient, keycloakIssuerURL)
	if err != nil {
		log.Fatalf("Failed to initialize OIDC provider: %v", err)
	}

	verifier = oidcProvider.Verifier(&oidc.Config{
		ClientID:               keycloakClientID,
		SkipIDTokenMap:         true,
		SkipSignatureVerification: false, // Crucial for security
	})

	oauth2Config = &oauth2.Config{
		ClientID:     keycloakClientID,
		ClientSecret: keycloakClientSecret,
		RedirectURL:  "http://localhost:8080/callback",
		Scopes:       []string{"openid", "profile", "email"},
		Endpoint:     oidcProvider.Endpoint(),
	}
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
	url := oauth2Config.AuthCodeURL("random-state-string")
	http.Redirect(w, r, url, http.StatusFound)
}

func callbackHandler(w http.ResponseWriter, r *http.Request) {
	query := r.URL.Query()
	authCode := query.Get("code")
	state := query.Get("state")

	if state != "random-state-string" {
		http.Error(w, "Invalid state parameter", http.StatusBadRequest)
		return
	}

	token, err := oauth2Config.Exchange(r.Context(), authCode)
	if err != nil {
		http.Error(w, fmt.Sprintf("Failed to exchange token: %v", err), http.StatusInternalServerError)
		return
	}

	rawIDToken, ok := token.Extra("id_token").(string)
	if !ok {
		http.Error(w, "No id_token found in token response", http.StatusInternalServerError)
		return
	}

	idToken, err := verifier.Verify(r.Context(), rawIDToken)
	if err != nil {
		http.Error(w, fmt.Sprintf("Failed to verify ID token: %v", err), http.StatusInternalServerError)
		return
	}

	// Successfully verified. Now extract claims.
	var claims struct {
		Email    string `json:"email"`
		Verified bool   `json:"email_verified"`
		Name     string `json:"name"`
	}
	if err := idToken.Claims(&claims); err != nil {
		http.Error(w, fmt.Sprintf("Failed to parse claims: %v", err), http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "Welcome, %s! Your email is %s (verified: %t)", claims.Name, claims.Email, claims.Verified)
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
	// In a real app, you'd check for a valid session or a validated token here.
	// For this example, we'll assume authentication happened in callbackHandler.
	fmt.Fprintln(w, "This is a protected resource!")
}

func main() {
	http.HandleFunc("/login", loginHandler)
	http.HandleFunc("/callback", callbackHandler)
	http.HandleFunc("/protected", protectedHandler) // This endpoint would typically be protected
	log.Println("Server starting on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

This code sets up a basic OIDC flow. When a user hits /login, they’re redirected to Keycloak. After authenticating with Keycloak, they’re sent back to /callback with an authorization code. The callbackHandler then exchanges this code for an ID token and an access token, crucially verifies the ID token’s signature and claims against Keycloak’s public keys, and finally extracts user information from the verified token.

The mental model here is that your Go application is a relying party (RP) and Keycloak is the identity provider (IdP). The OIDC protocol defines how the RP and IdP communicate to authenticate a user without the RP ever seeing the user’s password. The oauth2 Go package handles the OAuth2 flow (like exchanging codes for tokens), and the go-oidc package specifically adds the OIDC layer to verify the identity token’s integrity and contents.

The verifier.Verify call is where the magic happens. It doesn’t just take the token at face value. Internally, it:

  1. Fetches Keycloak’s public keys from the .well-known/openid-configuration endpoint (usually at http://localhost:8080/auth/realms/myrealm/.well-known/openid-configuration).
  2. Uses the alg (algorithm) specified in the token’s header to select the correct public key.
  3. Verifies the token’s signature using that public key and the token’s header and payload.
  4. Checks standard OIDC claims like iss (issuer), aud (audience, which should match your keycloakClientID), and exp (expiration time).

The most surprising thing that many developers miss is that the oidc.Config struct has a SkipSignatureVerification field. While it’s invaluable for development or testing scenarios where you might be mocking responses or dealing with self-signed certificates, leaving this as true in production completely defeats the purpose of OIDC’s security guarantees. The token could be forged by anyone with malicious intent, and your application would happily accept it as legitimate.

The next problem you’ll likely encounter is managing user sessions after the initial OIDC authentication, deciding how to keep the user logged in across requests without forcing them to re-authenticate with Keycloak every time.

Want structured learning?

Take the full Keycloak course →