diff --git a/.gitignore b/.gitignore index 06f54b6..89eea8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .env ./listener/tmp/* -./listener/bin/* \ No newline at end of file +./listener/bin/* +jwt +private.pem +public.pem \ No newline at end of file diff --git a/jwt-generator/README.md b/jwt-generator/README.md new file mode 100644 index 0000000..a57510e --- /dev/null +++ b/jwt-generator/README.md @@ -0,0 +1,48 @@ +# JWT Token Generator + +This is a simple JWT token generator written in Go. It creates a JWT token signed with an RSA private key and includes a custom expiration date. + +## Prerequisites + +- Go 1.15 or later +- RSA key pair (private key in PEM format) + +## Installation + +1. **Clone the repository (or download the jwt_generator.go file):** + + ```sh + git clone https://github.com/yourusername/jwt-generator.git + cd jwt-generator + ``` + +2. **Install dependencies:** + + Ensure you have the `golang-jwt/jwt/v5` library installed: + + ```sh + go get github.com/golang-jwt/jwt/v5 + ``` + +3. **Generate an RSA key pair:** + + Use OpenSSL or a similar tool to generate an RSA key pair (in PEM format): + + ```sh + openssl genrsa -out private.pem 2048 + openssl rsa -in private.pem -pubout -out public.pem + ``` + +4. **Compile the program:** + + ```sh + go build -o jwt_generator main.go + ``` + +## Usage + +Run the compiled binary to generate a JWT token: + +```sh +./jwt_generator -private-key=private.pem -sub=user@example.com -exp=24h -kid=key1 +``` diff --git a/jwt-generator/go.mod b/jwt-generator/go.mod new file mode 100644 index 0000000..2974a51 --- /dev/null +++ b/jwt-generator/go.mod @@ -0,0 +1,5 @@ +module github.com/dappnode/validator-monitoring/jwt-generator + +go 1.22.3 + +require github.com/golang-jwt/jwt/v5 v5.2.1 diff --git a/jwt-generator/go.sum b/jwt-generator/go.sum new file mode 100644 index 0000000..f56d3e6 --- /dev/null +++ b/jwt-generator/go.sum @@ -0,0 +1,2 @@ +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= diff --git a/jwt-generator/main.go b/jwt-generator/main.go new file mode 100644 index 0000000..193687b --- /dev/null +++ b/jwt-generator/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +func main() { + // Define flags for the command-line input + privateKeyPath := flag.String("private-key", "private.pem", "Path to the RSA private key file") + subject := flag.String("sub", "user@example.com", "Subject claim for the JWT") + expiration := flag.String("exp", "24h", "Expiration duration for the JWT (e.g., '24h' for 24 hours)") + kid := flag.String("kid", "key1", "Key ID (kid) for the JWT") + flag.Parse() + + // Read the private key file + privateKeyData, err := os.ReadFile(*privateKeyPath) + if err != nil { + log.Fatalf("Failed to read private key file: %v", err) + } + + // Parse the RSA private key + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyData) + if err != nil { + log.Fatalf("Failed to parse private key: %v", err) + } + + // Parse the expiration duration + duration, err := time.ParseDuration(*expiration) + if err != nil { + log.Fatalf("Failed to parse expiration duration: %v", err) + } + + // Create a new token object, specifying signing method and claims + token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ + "sub": *subject, + "exp": time.Now().Add(duration).Unix(), + }) + + // Set the key ID (kid) in the token header + token.Header["kid"] = *kid + + // Sign the token with the private key + tokenString, err := token.SignedString(privateKey) + if err != nil { + log.Fatalf("Failed to sign token: %v", err) + } + + // Output the token + fmt.Println(tokenString) +} diff --git a/listener/internal/api/middleware/loadSecrets.go b/listener/internal/api/middleware/loadSecrets.go index 08d6e21..708f701 100644 --- a/listener/internal/api/middleware/loadSecrets.go +++ b/listener/internal/api/middleware/loadSecrets.go @@ -2,112 +2,82 @@ package middleware import ( "encoding/json" - "errors" "fmt" "net/http" "os" "strings" - "time" - "github.com/dappnode/validator-monitoring/listener/internal/logger" "github.com/golang-jwt/jwt/v5" ) -var allowedPublicKeys []string - -// Runs automatically in main.go when the package is imported -func init() { - // Load public keys from a JSON file - data, err := os.ReadFile("/app/jwt/public_keys.json") - if err != nil { - logger.Fatal("Failed to load public keys: " + err.Error()) - } - - var keys struct { - AllowedPublicKeys []string `json:"allowedPublicKeys"` - } - - err = json.Unmarshal(data, &keys) - if err != nil { - logger.Fatal("Failed to unmarshal public keys: " + err.Error()) - } - - allowedPublicKeys = keys.AllowedPublicKeys - logger.Info("Loaded public keys: " + fmt.Sprintln(allowedPublicKeys)) -} - -// CustomClaims defines the custom claims in the JWT token -type MyCustomClaims struct { - PubKey string `json:"pubkey"` - jwt.RegisteredClaims +type PublicKeyEntry struct { + PublicKey string `json:"publicKey"` + Roles []string `json:"roles"` } -// JWTMiddleware is a middleware that checks the Authorization header for a valid JWT token -// and verifies the signature using the public key of the user contained in the token. -// The public keys are loaded from a JSON file. -// The JWT token must be in the format "Bearer " +// JWTMiddleware dynamically checks tokens against public keys loaded from a JSON file func JWTMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { - http.Error(w, "Missing Authorization header", http.StatusUnauthorized) + http.Error(w, "Authorization header is required", http.StatusUnauthorized) return } - tokenString := strings.TrimPrefix(authHeader, "Bearer ") - if tokenString == authHeader { - http.Error(w, "Invalid Authorization header format", http.StatusUnauthorized) + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + http.Error(w, "Authorization header format must be Bearer {token}", http.StatusUnauthorized) return } - // Parse the token - token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) { - // Validate the algorithm - if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok { - return nil, errors.New("unexpected signing method") - } + tokenString := parts[1] - // Extract the claims - claims, ok := token.Claims.(*MyCustomClaims) - logger.Info("token:" + fmt.Sprintln(token)) - if !ok { - return nil, errors.New("invalid token claims") - } + // Load public keys from JSON file + publicKeys, err := loadPublicKeys("/app/jwt/users.json") + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load public keys: %v", err), http.StatusInternalServerError) + return + } - // Verify the expiration time - if claims.ExpiresAt != nil && !claims.ExpiresAt.Time.After(time.Now()) { - return nil, errors.New("token has expired, it expires at " + claims.ExpiresAt.Time.String() + " now is " + time.Now().String()) + // Parse and verify the token + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } - // Verify that the public key is allowed - isAllowed := false - for _, allowedKey := range allowedPublicKeys { - logger.Info("allowedKey:" + allowedKey) - logger.Info("claims.PubKey:" + claims.PubKey) - if allowedKey == claims.PubKey { - isAllowed = true - break - } + kid, ok := token.Header["kid"].(string) + if !ok { + return nil, fmt.Errorf("kid not found in token header") } - if !isAllowed { - return nil, errors.New("public key not allowed") + // Load the public key for the given kid + entry, exists := publicKeys[kid] + if !exists { + return nil, fmt.Errorf("public key not found for kid: %s", kid) } - // Parse and return the public key - pubKey, err := jwt.ParseECPublicKeyFromPEM([]byte(claims.PubKey)) - if err != nil { - return nil, err - } - logger.Info("pubKey:" + fmt.Sprintln(pubKey)) - return pubKey, nil + return jwt.ParseRSAPublicKeyFromPEM([]byte(entry.PublicKey)) }) if err != nil || !token.Valid { - http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized) + http.Error(w, fmt.Sprintf("Invalid token or claims: %v", err), http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } + +func loadPublicKeys(filePath string) (map[string]PublicKeyEntry, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + var keys map[string]PublicKeyEntry + if err := json.Unmarshal(data, &keys); err != nil { + return nil, err + } + + return keys, nil +}