Encore.Dev AuthHandler with Auth0 and VueJS

TL;DR: The documentation suggests that JWT validation is sufficient for authentication in Encore, but there seems to be a conflict with implementing RBAC, as Auth0 requires a handler to be returned and Encore doesnt seem to work with that.

Been stuck on this for about 2 days, i understand that based on the documentation all that should be done technically is JWTValidation which entails decoding, if it can decode than it is validated otherwise it is not: Adding authentication to APIs to auth users — Encore Docs However, the problem is for me that the documentation states:

Encore applications can designate a special function to handle authentication, by defining a function and annotating it with //encore:authhandler. This annotation tells Encore to run the function whenever an incoming API call contains an authentication token.

The auth handler is responsible for validating the incoming authentication token and returning an auth.UID (a string type representing a user id). The auth.UID can be whatever you wish, but in practice it usually maps directly to the primary key stored in a user table (either defined in the Encore service or in an external service like Firebase or Okta).

Which means i cannot return a handler. According to the documentation for Auth0 states when implementing RBAC that it must be a handler that is returned. Does anyone have any ideas? Here is the working code according to Auth0 Documentation:

package middleware
 
import (
    "context"
    "log"
    "net/http"
    "net/url"
    "time"
 
    "encore.app/auth/helpers"
    jwtmiddleware "github.com/auth0/go-jwt-middleware/v2"
    "github.com/auth0/go-jwt-middleware/v2/jwks"
    "github.com/auth0/go-jwt-middleware/v2/validator"
    "github.com/pkg/errors"
)
 
// Declare constants for error messages
const (
    missingJWTErrorMessage       = "Requires authentication"
    invalidJWTErrorMessage       = "Bad credentials"
    permissionDeniedErrorMessage = "Permission denied"
)
 
// CustomClaims is a struct representing additional permissions in the JWT
type CustomClaims struct {
    Permissions []string `json:"permissions"`
}
 
// Validate is a method for validating custom claims (currently a no-op)
func (c CustomClaims) Validate(ctx context.Context) error {
    return nil
}
 
// HasPermissions is a method for checking if the custom claims contain all expected permissions
func (c CustomClaims) HasPermissions(expectedClaims []string) bool {
    if len(expectedClaims) == 0 {
        return false
    }
    for _, scope := range expectedClaims {
        if !helpers.Contains(c.Permissions, scope) {
            return false
        }
    }
    return true
}
 
// ValidatePermissions is a middleware function for checking if the JWT has the expected permissions
func ValidatePermissions(expectedClaims []string, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)
        claims := token.CustomClaims.(*CustomClaims)
        if !claims.HasPermissions(expectedClaims) {
            errorMessage := ErrorMessage{Message: permissionDeniedErrorMessage}
            helpers.WriteJSON(w, http.StatusForbidden, errorMessage)
            return
        }
        next.ServeHTTP(w, r)
    })
}
 
// ValidateJWT is a factory function that returns a middleware for validating JWTs with a given audience and domain
func ValidateJWT(audience, domain string) func(next http.Handler) http.Handler {
    issuerURL, err := url.Parse("https://" + domain + "/")
    if err != nil {
        log.Fatalf("Failed to parse the issuer url: %v", err)
    }
 
    provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)
 
    //Setting up validator
    jwtValidator, err := validator.New(
        provider.KeyFunc,
        validator.RS256,
        issuerURL.String(),
        []string{audience},
        validator.WithCustomClaims(
            func() validator.CustomClaims {
                return &CustomClaims{}
            },
        ),
    )
    if err != nil {
        log.Fatalf("Failed to set up the jwt validator")
    }
 
    errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
        log.Printf("Encountered error while validating JWT: %v", err)
        if errors.Is(err, jwtmiddleware.ErrJWTMissing) {
            errorMessage := ErrorMessage{Message: missingJWTErrorMessage}
            helpers.WriteJSON(w, http.StatusUnauthorized, errorMessage)
            return
        }
        if errors.Is(err, jwtmiddleware.ErrJWTInvalid) {
            errorMessage := ErrorMessage{Message: invalidJWTErrorMessage}
            helpers.WriteJSON(w, http.StatusUnauthorized, errorMessage)
            return
        }
        ServerError(w, err)
    }
 
    middleware := jwtmiddleware.New(
        jwtValidator.ValidateToken,
        jwtmiddleware.WithErrorHandler(errorHandler),
    )
 
    return func(next http.Handler) http.Handler {
        return middleware.CheckJWT(next)
    }
}

Hey Colton, the example code provided is using net/http middleware to do JWT validation, which doesn’t map 1:1 with Encore’s authentication handlers. Fortunately the middleware is only a convenience method for plain net/http services; it’s equally easy (or arguably even easier) to use the underlying validation function, which should work great with Encore.

From the code snippet you provided, the core validation logic happens in jwtValidator.ValidateToken which is the validation function passed to jwtmiddleware.New. In the case of Encore, you can call that function directly from the Encore authentication handler, like so (rough sketch):

import (
    "encore.dev/beta/auth"
    "github.com/auth0/go-jwt-middleware/v2/validator"
)

//encore:authhandler
func AuthHandler(ctx context.Context, token string) (auth.UID, error) {
    validatedToken, err := jwtValidator.ValidateToken(ctx, token)
    if err != nil {
        return "", err
    }
    claims := validatedToken.(*validator.ValidatedClaims)

    // ... check claims for the permissions you care about ....
   
    // Finally return the subject as the Encore user id
    return auth.UID(claims.RegisteredClaims.Subject), nil
}

var jwtValidator *validator.Validator

func init() {
    // Set up jwtValidator in the same was in the example code provided, calling validator.New
}

Hope that helps. Let me know if you can’t get it to work and I’d be happy to jump on a video call to go through it together!

1 Like