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)
}
}