...

Source file src/github.com/cybertec-postgresql/pgwatch/v6/internal/webserver/jwt.go

Documentation: github.com/cybertec-postgresql/pgwatch/v6/internal/webserver

     1  package webserver
     2  
     3  import (
     4  	"crypto/rand"
     5  	"errors"
     6  	"net/http"
     7  	"sync"
     8  	"time"
     9  
    10  	jsoniter "github.com/json-iterator/go"
    11  
    12  	"github.com/golang-jwt/jwt/v5"
    13  )
    14  
    15  type loginReq struct {
    16  	Username string `json:"user"`
    17  	Password string `json:"password"`
    18  }
    19  
    20  func (s *WebUIServer) IsCorrectPassword(lr loginReq) bool {
    21  	return (s.WebUser+s.WebPassword == "") ||
    22  		(s.WebUser == lr.Username && s.WebPassword == lr.Password)
    23  }
    24  
    25  func (s *WebUIServer) handleLogin(w http.ResponseWriter, r *http.Request) {
    26  	var (
    27  		err   error
    28  		lr    loginReq
    29  		token string
    30  	)
    31  
    32  	defer func() {
    33  		if err != nil {
    34  			http.Error(w, err.Error(), http.StatusInternalServerError)
    35  		}
    36  	}()
    37  
    38  	switch r.Method {
    39  	case "POST":
    40  		if err = jsoniter.ConfigFastest.NewDecoder(r.Body).Decode(&lr); err != nil {
    41  			return
    42  		}
    43  		if !s.IsCorrectPassword(lr) {
    44  			http.Error(w, "can not authenticate this user", http.StatusUnauthorized)
    45  			return
    46  		}
    47  		if token, err = generateJWT(lr.Username); err != nil {
    48  			return
    49  		}
    50  		_, err = w.Write([]byte(token))
    51  
    52  	default:
    53  		w.Header().Set("Allow", "POST")
    54  		http.Error(w, "only POST method is allowed", http.StatusMethodNotAllowed)
    55  		return
    56  	}
    57  }
    58  
    59  type EnsureAuth struct {
    60  	handler http.HandlerFunc
    61  }
    62  
    63  func (ea *EnsureAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    64  	if err := validateToken(r); err != nil {
    65  		http.Error(w, err.Error(), http.StatusUnauthorized)
    66  		return
    67  	}
    68  	ea.handler(w, r)
    69  }
    70  
    71  func NewEnsureAuth(handlerToWrap http.HandlerFunc) *EnsureAuth {
    72  	return &EnsureAuth{handlerToWrap}
    73  }
    74  
    75  var (
    76  	secretKeyOnce sync.Once
    77  	secretKey     []byte
    78  )
    79  
    80  // jwtSecretKey returns the HS256 signing key used for Web UI/API tokens.
    81  // The key is a cryptographically random 32-byte value generated once per
    82  // process at first use. Generating it at runtime (instead of hardcoding a
    83  // value in the source) prevents attackers from forging valid tokens.
    84  //
    85  // The key is intentionally not persisted: every process restart rotates it,
    86  // which invalidates any previously issued token. This is a security feature,
    87  // not a limitation - it bounds the lifetime of leaked tokens and forces users
    88  // to re-authenticate with the configured credentials rather than relying on
    89  // long-lived sessions.
    90  //
    91  // Operational note: because each instance generates its own key, tokens are
    92  // not portable across instances. Multi-replica/HA deployments should use
    93  // sticky sessions at the load balancer.
    94  func jwtSecretKey() []byte {
    95  	secretKeyOnce.Do(func() {
    96  		secretKey = make([]byte, 32)
    97  		if _, err := rand.Read(secretKey); err != nil {
    98  			// crypto/rand.Read should never fail; if it does there is no safe
    99  			// way to continue issuing/validating tokens, so fail loudly.
   100  			panic("webserver: unable to generate JWT secret key: " + err.Error())
   101  		}
   102  	})
   103  	return secretKey
   104  }
   105  
   106  func generateJWT(username string) (string, error) {
   107  	token := jwt.New(jwt.SigningMethodHS256)
   108  	claims := token.Claims.(jwt.MapClaims)
   109  
   110  	claims["authorized"] = true
   111  	claims["username"] = username
   112  	claims["exp"] = time.Now().Add(time.Hour * 8).Unix()
   113  
   114  	return token.SignedString(jwtSecretKey())
   115  }
   116  
   117  func validateToken(r *http.Request) (err error) {
   118  	var t string
   119  	if r.Header["Token"] == nil {
   120  		t = r.URL.Query().Get("Token")
   121  	} else {
   122  		t = r.Header["Token"][0]
   123  	}
   124  	if t == "" {
   125  		return errors.New("can not find token in header")
   126  	}
   127  
   128  	_, err = jwt.Parse(t,
   129  		func(_ *jwt.Token) (any, error) {
   130  			return jwtSecretKey(), nil
   131  		},
   132  		jwt.WithExpirationRequired(),
   133  		jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
   134  	return err
   135  }
   136