Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 150 additions & 22 deletions error_handler.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,174 @@
package jwtmiddleware

import (
"encoding/json"
"errors"
"fmt"
"net/http"

"github.com/auth0/go-jwt-middleware/v3/core"
)

var (
// ErrJWTMissing is returned when the JWT is missing.
ErrJWTMissing = errors.New("jwt missing")
// This is the same as core.ErrJWTMissing for consistency.
ErrJWTMissing = core.ErrJWTMissing

// ErrJWTInvalid is returned when the JWT is invalid.
ErrJWTInvalid = errors.New("jwt invalid")
// This is the same as core.ErrJWTInvalid for consistency.
ErrJWTInvalid = core.ErrJWTInvalid
)

// ErrorHandler is a handler which is called when an error occurs in the
// JWTMiddleware. Among some general errors, this handler also determines the
// response of the JWTMiddleware when a token is not found or is invalid. The
// err can be checked to be ErrJWTMissing or ErrJWTInvalid for specific cases.
// The default handler will return a status code of 400 for ErrJWTMissing,
// 401 for ErrJWTInvalid, and 500 for all other errors. If you implement your
// own ErrorHandler you MUST take into consideration the error types as not
// properly responding to them or having a poorly implemented handler could
// result in the JWTMiddleware not functioning as intended.
// JWTMiddleware. The handler determines the HTTP response when a token is
// not found, is invalid, or other errors occur.
//
// The default handler (DefaultErrorHandler) provides:
// - Structured JSON error responses with error codes
// - RFC 6750 compliant WWW-Authenticate headers (Bearer tokens)
// - Appropriate HTTP status codes based on error type
// - Security-conscious error messages (no sensitive details by default)
// - Extensible architecture for future authentication schemes (e.g., DPoP per RFC 9449)
//
// Custom error handlers should check for ErrJWTMissing and ErrJWTInvalid
// sentinel errors, as well as core.ValidationError for detailed error codes.
//
// Future extensions (e.g., DPoP support) can use the same pattern:
// - Add DPoP-specific error codes to core.ValidationError
// - Update mapValidationError to handle DPoP errors
// - Return appropriate WWW-Authenticate headers with DPoP scheme
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)

// DefaultErrorHandler is the default error handler implementation for the
// JWTMiddleware. If an error handler is not provided via the WithErrorHandler
// option this will be used.
// ErrorResponse represents a structured error response.
type ErrorResponse struct {
// Error is the main error message
Error string `json:"error"`

// ErrorDescription provides additional context (optional)
ErrorDescription string `json:"error_description,omitempty"`

// ErrorCode is a machine-readable error code (optional)
ErrorCode string `json:"error_code,omitempty"`
}

// DefaultErrorHandler is the default error handler implementation.
// It provides structured error responses with appropriate HTTP status codes
// and RFC 6750 compliant WWW-Authenticate headers.
func DefaultErrorHandler(w http.ResponseWriter, _ *http.Request, err error) {
// Extract error details
statusCode, errorResp, wwwAuthenticate := mapErrorToResponse(err)

// Set headers
w.Header().Set("Content-Type", "application/json")
if wwwAuthenticate != "" {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
}

// Write response
w.WriteHeader(statusCode)
_ = json.NewEncoder(w).Encode(errorResp)
}

// mapErrorToResponse maps errors to appropriate HTTP responses
func mapErrorToResponse(err error) (statusCode int, resp ErrorResponse, wwwAuthenticate string) {
// Check for JWT missing error
if errors.Is(err, ErrJWTMissing) {
return http.StatusUnauthorized, ErrorResponse{
Error: "invalid_token",
ErrorDescription: "JWT is missing",
}, `Bearer error="invalid_token", error_description="JWT is missing"`
}

// Check for validation error with specific code
var validationErr *core.ValidationError
if errors.As(err, &validationErr) {
return mapValidationError(validationErr)
}

// Check for general JWT invalid error
if errors.Is(err, ErrJWTInvalid) {
return http.StatusUnauthorized, ErrorResponse{
Error: "invalid_token",
ErrorDescription: "JWT is invalid",
}, `Bearer error="invalid_token", error_description="JWT is invalid"`
}

// Default to internal server error for unexpected errors
return http.StatusInternalServerError, ErrorResponse{
Error: "server_error",
ErrorDescription: "An internal error occurred while processing the request",
}, ""
}

// mapValidationError maps core.ValidationError codes to HTTP responses
// This function is extensible to support future authentication schemes like DPoP (RFC 9449)
func mapValidationError(err *core.ValidationError) (statusCode int, resp ErrorResponse, wwwAuthenticate string) {
// Map error codes to HTTP status codes and RFC 6750 Bearer token error types
// Future: Add DPoP-specific error codes and return appropriate DPoP challenge headers
switch err.Code {
case core.ErrorCodeTokenExpired:
return http.StatusUnauthorized, ErrorResponse{
Error: "invalid_token",
ErrorDescription: "The access token expired",
ErrorCode: string(err.Code),
}, `Bearer error="invalid_token", error_description="The access token expired"`

case core.ErrorCodeTokenNotYetValid:
return http.StatusUnauthorized, ErrorResponse{
Error: "invalid_token",
ErrorDescription: "The access token is not yet valid",
ErrorCode: string(err.Code),
}, `Bearer error="invalid_token", error_description="The access token is not yet valid"`

case core.ErrorCodeInvalidSignature:
return http.StatusUnauthorized, ErrorResponse{
Error: "invalid_token",
ErrorDescription: "The access token signature is invalid",
ErrorCode: string(err.Code),
}, `Bearer error="invalid_token", error_description="The access token signature is invalid"`

case core.ErrorCodeTokenMalformed:
return http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
ErrorDescription: "The access token is malformed",
ErrorCode: string(err.Code),
}, `Bearer error="invalid_request", error_description="The access token is malformed"`

case core.ErrorCodeInvalidIssuer:
return http.StatusForbidden, ErrorResponse{
Error: "insufficient_scope",
ErrorDescription: "The access token was issued by an untrusted issuer",
ErrorCode: string(err.Code),
}, `Bearer error="insufficient_scope", error_description="The access token was issued by an untrusted issuer"`

case core.ErrorCodeInvalidAudience:
return http.StatusForbidden, ErrorResponse{
Error: "insufficient_scope",
ErrorDescription: "The access token audience does not match",
ErrorCode: string(err.Code),
}, `Bearer error="insufficient_scope", error_description="The access token audience does not match"`

case core.ErrorCodeInvalidAlgorithm:
return http.StatusUnauthorized, ErrorResponse{
Error: "invalid_token",
ErrorDescription: "The access token uses an unsupported algorithm",
ErrorCode: string(err.Code),
}, `Bearer error="invalid_token", error_description="The access token uses an unsupported algorithm"`

case core.ErrorCodeJWKSFetchFailed, core.ErrorCodeJWKSKeyNotFound:
return http.StatusUnauthorized, ErrorResponse{
Error: "invalid_token",
ErrorDescription: "Unable to verify the access token",
ErrorCode: string(err.Code),
}, `Bearer error="invalid_token", error_description="Unable to verify the access token"`

switch {
case errors.Is(err, ErrJWTMissing):
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message":"JWT is missing."}`))
case errors.Is(err, ErrJWTInvalid):
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"message":"JWT is invalid."}`))
default:
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"message":"Something went wrong while checking the JWT."}`))
// Generic invalid token error for other cases
return http.StatusUnauthorized, ErrorResponse{
Error: "invalid_token",
ErrorDescription: "The access token is invalid",
ErrorCode: string(err.Code),
}, `Bearer error="invalid_token", error_description="The access token is invalid"`
}
}

Expand Down
Loading
Loading