Skip to content

Commit

Permalink
add jwt testing
Browse files Browse the repository at this point in the history
  • Loading branch information
Marketen committed May 30, 2024
1 parent 30e5ca9 commit 3246306
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 45 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/go_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ jobs:
- name: Check out code
uses: actions/checkout@v3

- name: Build
- name: Build listener
run: |
cd ./listener
go build -a -installsuffix cgo -o bin/listener ./cmd/listener
- name: Build jwt-generator
run: |
cd ./listener
go build -a -installsuffix cgo -o bin/jwt-generator ./cmd/jwt-generator
- name: Setup Integration Test Environment
run: |
docker compose -f docker-compose-ci.yml up --build -d
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.env
./listener/tmp/*
./listener/bin/*
jwt
private.pem
public.pem
49 changes: 6 additions & 43 deletions listener/cmd/jwt-generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,36 @@ import (
"flag"
"fmt"
"os"
"time"

"github.com/dappnode/validator-monitoring/listener/internal/jwt"
"github.com/dappnode/validator-monitoring/listener/internal/logger"

"github.com/golang-jwt/jwt/v5"
)

func main() {
// Define flags for the command-line input
privateKeyPath := flag.String("private-key", "", "Path to the RSA private key file (mandatory)")
subject := flag.String("sub", "", "Subject claim for the JWT (optional)")
expiration := flag.String("exp", "", "Expiration duration for the JWT in hours (optional, e.g., '24h' for 24 hours). If no value is provided, the generated token will not expire.")
expiration := flag.String("exp", "", "Expiration duration for the JWT in hours (optional)")
kid := flag.String("kid", "", "Key ID (kid) for the JWT (mandatory)")
outputFilePath := flag.String("output", "token.jwt", "Output file path for the JWT. Defaults to ./token.jwt")
outputFilePath := flag.String("output", "token.jwt", "Output file path for the JWT")

flag.Parse()

// Check for mandatory parameters
if *kid == "" || *privateKeyPath == "" {
logger.Fatal("Key ID (kid) and private key path must be provided")
}

// Read the private key file
privateKeyData, err := os.ReadFile(*privateKeyPath)
if err != nil {
logger.Fatal(fmt.Sprintf("Failed to read private key file: %v", err))
}

// Parse the RSA private key
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyData)
tokenString, err := jwt.GenerateJWT(*kid, *privateKeyPath, *subject, *expiration)
if err != nil {
logger.Fatal(fmt.Sprintf("Failed to parse private key: %v", err))
}

// Prepare the claims for the JWT. These are optional
claims := jwt.MapClaims{}
if *subject != "" {
claims["sub"] = *subject
}
if *expiration != "" {
duration, err := time.ParseDuration(*expiration)
if err != nil {
logger.Fatal(fmt.Sprintf("Failed to parse expiration duration: %v", err))
}
claims["exp"] = time.Now().Add(duration).Unix()
logger.Fatal(fmt.Sprintf("Error generating JWT: %v", err))
}

// Create a new token object, specifying signing method and claims
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)

// 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 {
logger.Fatal(fmt.Sprintf("Failed to sign token: %v", err))
}

// Output the token to the console
fmt.Println("JWT generated successfully:")
fmt.Println(tokenString)

// Save the token to a file
err = os.WriteFile(*outputFilePath, []byte(tokenString), 0644)
if err != nil {
logger.Fatal(fmt.Sprintf("Failed to write the JWT to file: %v", err))
}

fmt.Println("JWT saved to file:", *outputFilePath)
}
52 changes: 52 additions & 0 deletions listener/internal/jwt/generateJWT.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package jwt

import (
"os"
"time"

"github.com/dappnode/validator-monitoring/listener/internal/logger"
"github.com/golang-jwt/jwt/v5"
)

func GenerateJWT(kid, privateKeyPath, subject, expiration string) (string, error) {
logger.Info("Starting JWT generation")

privateKeyData, err := os.ReadFile(privateKeyPath)
if err != nil {
logger.Error("Failed to read private key file: " + err.Error())
return "", err
}
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyData)
if err != nil {
logger.Error("Failed to parse private key: " + err.Error())
return "", err
}

claims := jwt.MapClaims{}
if subject != "" {
claims["sub"] = subject
logger.Info("Subject claim set: " + subject)
}
if expiration != "" {
duration, err := time.ParseDuration(expiration)
if err != nil {
logger.Error("Failed to parse expiration duration: " + err.Error())
return "", err
}
claims["exp"] = time.Now().Add(duration).Unix()
logger.Info("Expiration claim set: " + expiration)
}

token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = kid
logger.Info("JWT claims prepared")

tokenString, err := token.SignedString(privateKey)
if err != nil {
logger.Error("Failed to sign token: " + err.Error())
return "", err
}
logger.Info("JWT generated and signed successfully")

return tokenString, nil
}
77 changes: 77 additions & 0 deletions listener/internal/jwt/generateJWT_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package jwt

import (
"fmt"
"testing"
"time"

"github.com/golang-jwt/jwt/v5"
)

const testPublicKey string = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArVyO2yhG8kGX14//pdCc
XHEXIGB6qNAuVpiX7go8ZMC7dcSUXizQSxE4ffqegHnNiemyNbHOFAbhFaxszkP7
VOdbYdMe1IoFQ3yGGTfpAZtLWinq/CMwI9CSziSKgif3Rsvbj/xhDfQqfamLqmUJ
Mk8gRdeMp7SiwDHrwVnwUdAgwKioEiTGU2y+C6EHdJeX0I6eFkLvGjbYFt35y0du
wSp0ZYsB+P8glqGQvBOMtvWzoQQ8skuJ8yVBtR+GvU7hPXYBknL4jSLBOzJhHeEW
srsGhn9V5Lo775y7n/ZBJFU0tVn/2zi//HKAVCTfG3J7IHAZqnhEivoM3jaFkzHh
TQIDAQAB
-----END PUBLIC KEY-----`

// Test RSA Private Key (usually generated and safely stored; this is for testing only!)
const testPrivateKeyPath = "../../test/data/private.pem"

// TestGenerateJWT tests the GenerateJWT function. From an example private key in
// ../../test/data/private.pem, it generates a JWT token with the kid "testKid".
func TestGenerateJWT(t *testing.T) {
kid := "testKid"
subject := "testSubject"
expiration := "1h"

tokenString, err := GenerateJWT(kid, testPrivateKeyPath, subject, expiration)
if err != nil {
t.Fatalf("Generating JWT should not produce an error: %v", err)
}

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

kid, ok := token.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("kid not found in token header, generate a new token with a 'kid'")
}

if kid != "testKid" {
return nil, fmt.Errorf("expected kid to be 'testKid', got %v", kid)
}

return jwt.ParseRSAPublicKeyFromPEM([]byte(testPublicKey))
})

if err != nil {
t.Fatalf("The token should be valid: %v", err)
}

if !token.Valid {
t.Fatalf("The token should be successfully validated")
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
t.Fatalf("Claims should be of type MapClaims")
}

if claims["sub"] != subject {
t.Fatalf("Expected subject to be %v, got %v", subject, claims["sub"])
}

if exp, ok := claims["exp"].(float64); ok {
if time.Now().Add(50*time.Minute).Unix() >= int64(exp) {
t.Fatalf("Expiration should be correct")
}
} else {
t.Fatalf("Expiration claim missing or not a float64")
}
}
1 change: 1 addition & 0 deletions listener/test/sendSignatures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func TestPostSignaturesIntegration(t *testing.T) {
invalidPayload := base64.StdEncoding.EncodeToString(invalidPayloadBytes)

// Define test cases
// TODO: we should add the expected message for each case too, besides the expected code
tests := []struct {
description string
payload string
Expand Down

0 comments on commit 3246306

Please sign in to comment.