Skip to content

Commit

Permalink
jwt-generator & improve jwt middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Marketen committed May 23, 2024
1 parent e2e36bc commit 593f630
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 73 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.env
./listener/tmp/*
./listener/bin/*
./listener/bin/*
jwt
private.pem
public.pem
48 changes: 48 additions & 0 deletions jwt-generator/README.md
Original file line number Diff line number Diff line change
@@ -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 [email protected] -exp=24h -kid=key1
```
5 changes: 5 additions & 0 deletions jwt-generator/go.mod
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions jwt-generator/go.sum
Original file line number Diff line number Diff line change
@@ -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=
56 changes: 56 additions & 0 deletions jwt-generator/main.go
Original file line number Diff line number Diff line change
@@ -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", "[email protected]", "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)
}
114 changes: 42 additions & 72 deletions listener/internal/api/middleware/loadSecrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token string>"
// 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
}

0 comments on commit 593f630

Please sign in to comment.