You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Golang与Keycloak交互?解决Go应用认证集成无文档问题

Integrating a Go Application with Keycloak Using OpenID Connect

Hey there! I’ve worked through integrating Go apps with Keycloak many times, and while there’s no official Go adapter, you don’t need one—Keycloak speaks standard OpenID Connect (OIDC), which has great support in the Go ecosystem. Let me break this down step by step.

Core Idea: Use Standard OIDC Libraries

Forget the undocumented third-party adapters. The Go community has mature, well-maintained libraries that implement OIDC perfectly, and they play nicely with Keycloak. The go-to choice is github.com/coreos/go-oidc/v3/oidc, paired with the standard golang.org/x/oauth2 package.


Step 1: Configure a Keycloak Client

First, set up a client in your Keycloak realm to represent your Go app:

  • Log into your Keycloak admin console and create a new OpenID Connect client.
  • Set a meaningful Client ID (e.g., my-go-web-app or my-go-api).
  • Configure valid Redirect URIs (for web apps, use something like http://localhost:8080/callback for local testing; update this for production).
  • Enable the appropriate flow:
    • For web apps with user login: Turn on Standard Flow (Authorization Code Flow).
    • For backend APIs: Use Client Credentials Flow if the API authenticates other services, or just validate JWTs from frontend clients.
  • Save the client, then note down:
    • Client ID
    • Client Secret (if you enabled "Confidential" client type)
    • Keycloak’s issuer URL (e.g., http://your-keycloak-domain/auth/realms/your-realm)

Step 2: Integrate OIDC into Your Go App

First, install the required dependencies:

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

Scenario A: Web App with User Login (Authorization Code Flow)

This example handles user login via Keycloak, exchanges the authorization code for tokens, and protects routes:

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"

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

func main() {
    ctx := context.Background()

    // Initialize Keycloak OIDC provider
    provider, err := oidc.NewProvider(ctx, os.Getenv("KEYCLOAK_ISSUER"))
    if err != nil {
        panic(fmt.Errorf("failed to create provider: %w", err))
    }

    // Configure OAuth2 client
    oauth2Config := oauth2.Config{
        ClientID:     os.Getenv("KEYCLOAK_CLIENT_ID"),
        ClientSecret: os.Getenv("KEYCLOAK_CLIENT_SECRET"),
        RedirectURL:  "http://localhost:8080/callback",
        Scopes:       []string{oidc.ScopeOpenID, "profile", "email"},
        Endpoint:     provider.Endpoint(),
    }

    // Create a token verifier
    verifier := provider.Verifier(&oidc.Config{ClientID: os.Getenv("KEYCLOAK_CLIENT_ID")})

    // Login route: Redirect to Keycloak's login page
    http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
        // Generate a random state string (store this in the user's session for CSRF protection)
        state := "random-unique-state-123" // Replace with actual random state in production
        authURL := oauth2Config.AuthCodeURL(state)
        http.Redirect(w, r, authURL, http.StatusFound)
    })

    // Callback route: Exchange authorization code for tokens
    http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
        // Validate state against the one stored in the user's session (critical for CSRF protection)
        incomingState := r.URL.Query().Get("state")
        if incomingState != "random-unique-state-123" {
            http.Error(w, "invalid state parameter", http.StatusBadRequest)
            return
        }

        code := r.URL.Query().Get("code")
        token, err := oauth2Config.Exchange(ctx, code)
        if err != nil {
            http.Error(w, fmt.Sprintf("failed to exchange code: %v", err), http.StatusInternalServerError)
            return
        }

        // Extract and verify the ID Token
        rawIDToken, ok := token.Extra("id_token").(string)
        if !ok {
            http.Error(w, "no id_token found in response", http.StatusInternalServerError)
            return
        }

        idToken, err := verifier.Verify(ctx, rawIDToken)
        if err != nil {
            http.Error(w, fmt.Sprintf("failed to verify id_token: %v", err), http.StatusUnauthorized)
            return
        }

        // Parse user claims from the ID Token
        var userClaims struct {
            Email    string `json:"email"`
            Name     string `json:"name"`
            Username string `json:"preferred_username"`
            Roles    struct {
                RealmRoles []string `json:"roles"`
            } `json:"realm_access"`
        }
        if err := idToken.Claims(&userClaims); err != nil {
            http.Error(w, fmt.Sprintf("failed to parse claims: %v", err), http.StatusInternalServerError)
            return
        }

        // Store user info in session (use a proper session manager in production)
        fmt.Fprintf(w, "Welcome %s!\nYour email: %s\nRoles: %v", 
            userClaims.Name, userClaims.Email, userClaims.Roles.RealmRoles)
    })

    // Protected route: Verify Bearer token and check roles
    http.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
        // Extract Bearer token from Authorization header
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" || len(authHeader) < 7 || authHeader[:7] != "Bearer " {
            http.Error(w, "missing or invalid authorization header", http.StatusUnauthorized)
            return
        }
        rawToken := authHeader[7:]

        // Verify the token
        idToken, err := verifier.Verify(ctx, rawToken)
        if err != nil {
            http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized)
            return
        }

        // Parse roles from claims
        var claims struct {
            RealmAccess struct {
                Roles []string `json:"roles"`
            } `json:"realm_access"`
        }
        if err := idToken.Claims(&claims); err != nil {
            http.Error(w, fmt.Sprintf("failed to parse claims: %v", err), http.StatusInternalServerError)
            return
        }

        // Check if user has admin role
        isAdmin := false
        for _, role := range claims.RealmAccess.Roles {
            if role == "admin" {
                isAdmin = true
                break
            }
        }
        if !isAdmin {
            http.Error(w, "insufficient permissions", http.StatusForbidden)
            return
        }

        fmt.Fprintln(w, "Welcome to the admin panel!")
    })

    fmt.Println("Server running on http://localhost:8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(fmt.Errorf("server failed to start: %w", err))
    }
}

Scenario B: Backend API (JWT Validation)

If your app is a backend API that only needs to validate JWT tokens from clients, use this simplified setup:

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"

    "github.com/coreos/go-oidc/v3/oidc"
)

func main() {
    ctx := context.Background()

    provider, err := oidc.NewProvider(ctx, os.Getenv("KEYCLOAK_ISSUER"))
    if err != nil {
        panic(fmt.Errorf("failed to create provider: %w", err))
    }

    verifier := provider.Verifier(&oidc.Config{ClientID: os.Getenv("KEYCLOAK_CLIENT_ID")})

    // Auth middleware to validate tokens
    authMiddleware := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            authHeader := r.Header.Get("Authorization")
            if authHeader == "" || len(authHeader) < 7 || authHeader[:7] != "Bearer " {
                http.Error(w, "unauthorized", http.StatusUnauthorized)
                return
            }
            rawToken := authHeader[7:]

            _, err := verifier.Verify(ctx, rawToken)
            if err != nil {
                http.Error(w, "invalid token", http.StatusUnauthorized)
                return
            }

            // Pass request to next handler
            next.ServeHTTP(w, r)
        })
    }

    // Protected API endpoint
    http.Handle("/api/data", authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "This is protected API data")
    })))

    fmt.Println("API server running on http://localhost:8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(fmt.Errorf("server failed to start: %w", err))
    }
}

Key Production Tips

  • CSRF Protection: Always generate a random state value for each login request, store it in the user’s session, and validate it in the callback. Never hardcode it.
  • Token Refresh: For long-lived sessions, store the refresh token and use it to get new access tokens when the old one expires.
  • Environment Variables: Don’t hardcode client secrets or Keycloak URLs—use environment variables (like in the examples above) or a secrets manager.
  • HTTPS: Always use HTTPS in production to protect tokens in transit.
  • Role Checks: Keycloak stores realm roles in realm_access.roles and client-specific roles in resource_access.{client-id}.roles. Adjust your claim parsing based on your needs.

内容的提问来源于stack exchange,提问作者setiabb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 08:58:39