Search code examples
gooauth-2.0oauthkeycloak

GO validate access token (keycloak)


I am trying to implement access token validation with GO. But the examples I see online only seem to validate it with the TOKEN_SECRET. however I am used to programming in Java spring and there I dont need to use TOKEN_SECRET. I just provide the jwk-set-uri and it checks the validity (automatically - security filters and such) I know that it comunicates with the oauth server and makes this validation.

Is there no lib in Go that checks that the token is valid with a request to the oauth server?

Right know I know that I can make it manually with a request to the user info endpoint of the oauth server:

http://localhost:8080/auth/realms/<your_realm>/protocol/openid-connect/userinfo

(include token in the headers with key Authorization)

but I don't know if this is exacly following standards.

What would be the correct way to do it?


Solution

  • Short answer: use go-oidc

    Long answer: First, let's understand how Spring Security automatically validates access tokens that are JWTs. By convention, if you define OAuth 2.0 or OIDC client properties in the application.yaml config file, Spring will automatically wire a filter in the security filter chain that fetches jwk-set from Keycloak which is a set of public keys corresponding to the secret keys used by Keycloak to sign the tokens. This filter will applied to all protected routes and spring will use the public key to check if the signature of the token is valid and make additional checks if applicable (audience, time-out, etc...)

    So how do we do this in Go? We can write a simple middleware that accepts the jwk-set and use it to validate the tokens. You will need to check the alg claim to see which signing algorithm was used, pick the corresponding public key from jwk-set and check that the signature is valid. Then, decode the token, confirm the iss and sub claims to ensure it's from a trusted issuer and for the intended audience, and check that it hasn't expired. Check the nbf (Not Before) and iat (Issued At) claims for correct timing. Finally, inject relevant info from the token to the request context if needed downstream.

    func JWTMiddleware(jwkSet map[string]*rsa.PublicKey) func(http.Handler) http.Handler {
        return func(next http.Handler) http.Handler {
            return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                authHeader := r.Header.Get("Authorization")
                if authHeader == "" {
                    http.Error(w, "Authorization header is required", http.StatusUnauthorized)
                    return
                }
    
                tokenString := strings.TrimPrefix(authHeader, "Bearer ")
                token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
                    if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
                        return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
                    }
    
                    alg := token.Method.Alg()
                    publicKey, ok := jwkSet[alg]
                    if !ok {
                        return nil, fmt.Errorf("no key found for signing method: %v", alg)
                    }
                    return publicKey, nil
                })
                
                if err != nil || !token.Valid {
                    http.Error(w, "Invalid token", http.StatusUnauthorized)
                    return
                }
                // Other checks, ISS, Aud, Expirey, etc ...
                // If needed, store the user principal 
                // and other relevant info the request context  
                next.ServeHTTP(w, r)
            })
        }
    }