go-chi / jwtauth

JWT authentication middleware for Go HTTP services
MIT License
547 stars 91 forks source link

How would I implement multiple secrets on the server side? #40

Open avpavp opened 5 years ago

avpavp commented 5 years ago

I'd like to have the possibility to have different clients use different secrets - how would I test against multiple secrets on the server side?

Thanks!

jess-belliveau commented 4 years ago

@avpavp, I had a similar requirement that I was playing around with. Support for each caller to have its own shared secret. I've ended up with the following as the starting function for my middleware, replacing the original "jwtauth.Verifier" call:

func verifyCaller(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("Executing Verifier")

        encodedToken := jwtauth.TokenFromHeader(r)

        if encodedToken != "" {
            encodedPayload := strings.Split(encodedToken, ".")
            decodedPayload, err := base64.StdEncoding.DecodeString(encodedPayload[1])
            if err != nil {
                log.Println("Payload decode error:", err)
                http.Error(w, http.StatusText(500), 500)
                return
            }

            payload := make(map[string]string)
            err = json.Unmarshal(decodedPayload, &payload)
            caller := payload["caller"]

            // Go off to secret vault, get secret for caller
            sharedSecret := determineCallerSecret(caller)

            if sharedSecret != "" {
                // Secret has been determined - build tokenAuth, verify and modify context
                log.Println("Caller: " + caller + ", Secret located.")
                tokenAuth = jwtauth.New("HS256", []byte(sharedSecret), nil)

                ctx := r.Context()
                token, err := jwtauth.VerifyRequest(tokenAuth, r, jwtauth.TokenFromHeader)
                ctx = jwtauth.NewContext(ctx, token, err)
                next.ServeHTTP(w, r.WithContext(ctx))
            } else {
                log.Println("Err: secret lookup failed")
                http.Error(w, http.StatusText(500), 500)
                return
            }
        } else {
            log.Println("Err: JWT token payload not found.")
            http.Error(w, http.StatusText(401), 401)
            return
        }
    })
}

This decodes the payload of the JWT and looks for a required entry "caller" - this value is then passed to a function which connects to how ever you are storing your secrets. I am yet to really test this, but some initial curl attempts with varying tokens look good.

I am going to try and find a better way to decode the payload and you may also want to support the ALG type changing based on caller. Also determineCallerSecret() should return an err, not just an empty string - but this was quick first pass.

EDIT: I'll also include what my testing router looks like (using gorilla/mux):

r := mux.NewRouter()
    api := r.PathPrefix("/api/v1").Subrouter()

    // Custom middleware to determine the caller, set the secret and verify the JWT
    api.Use(verifyCaller)
    // jwtauth middlware to authenticate based on the token
    api.Use(jwtauth.Authenticator)

    api.HandleFunc("", get).Methods(http.MethodGet)
pkieltyka commented 3 years ago

note, underlying lib in master has changed to https://github.com/lestrrat-go/jwx but jwtauth api is largely the same