From a9b7c3e638eb17562d6aba097b1ad1ae6876e86b Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Thu, 26 Mar 2026 03:51:02 +0100 Subject: [PATCH 1/3] feat: add file input option to JwtInfo --- cmd/jwtinfo.go | 50 +++++++++++++++++++- devenv.nix | 18 +++++++ internal/jwtinfo/jwtinfo.go | 21 +++++++++ internal/jwtinfo/jwtinfo_test.go | 80 ++++++++++++++++++++++++++++++-- internal/jwtinfo/main_test.go | 23 +++++++++ 5 files changed, 185 insertions(+), 7 deletions(-) diff --git a/cmd/jwtinfo.go b/cmd/jwtinfo.go index db752ee..c72aa68 100644 --- a/cmd/jwtinfo.go +++ b/cmd/jwtinfo.go @@ -17,9 +17,11 @@ import ( var ( flagNameRequestJSONValues = "request-values-json" + flagNameRequestValuesFile = "request-values-file" flagNameRequestURL = "request-url" flagNameJwksURL = "validation-url" requestJSONValues string + requestValuesFile string requestURL string jwksURL string keyfuncDefOverride keyfunc.Override @@ -27,13 +29,50 @@ var ( var jwtinfoCmd = &cobra.Command{ Use: "jwtinfo", - Short: "Request and display JWT token data", - Long: `Request and display JWT token data.`, + Short: "JwtInfo request and display JWT token data", + Long: `JwtInfo request and display JWT token data + +Examples: + export REQ_URL="https://sample.provider/oauth/token" + export REQ_VALUES="{\"login\":\"values\"}" + export VALIDATION_URL="https://url.to/jkws.json" + + # Get the JWT token using inline values + https-wrench jwtinfo --request-url $REQ_URL --request-values-json $REQ_VALUES + + # Get the JWT token using values file + https-wrench jwtinfo --request-url $REQ_URL --request-values-file request-values.json + + # Get and validate the JWT token + https-wrench jwtinfo --request-url $REQ_URL --request-values-json $REQ_VALUES --validation-url $VALIDATION_URL +`, Run: func(cmd *cobra.Command, args []string) { + // TODO: display version and exit + // TODO: remove global --config option + + if len(requestJSONValues+requestURL) == 0 && len(requestValuesFile+requestURL) == 0 { + _ = cmd.Help() + return + } + var err error client := &http.Client{} requestValuesMap := make(map[string]string) + if requestValuesFile != "" { + requestValuesMap, err = jwtinfo.ReadRequestValuesFile( + requestValuesFile, + requestValuesMap, + ) + if err != nil { + fmt.Printf( + "error while reading request's values from file: %s", + err, + ) + return + } + } + if requestJSONValues != "" { requestValuesMap, err = jwtinfo.ParseRequestJSONValues( requestJSONValues, @@ -98,6 +137,13 @@ func init() { "JSON encoded values to use for the JWT token request", ) + jwtinfoCmd.Flags().StringVar( + &requestValuesFile, + flagNameRequestValuesFile, + "", + "File containing the JSON encoded values to use for the JWT token request", + ) + jwtinfoCmd.Flags().StringVar( &jwksURL, flagNameJwksURL, diff --git a/devenv.nix b/devenv.nix index 38ea610..03a2dba 100644 --- a/devenv.nix +++ b/devenv.nix @@ -608,6 +608,15 @@ in { ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-json "$JWTINFO_TEST_AUTH0" ''; + scripts.run-jwtinfo-test-auth0-values-file.exec = '' + gum format "### JwtInfo request against Auth0 with values file" + + REQ_URL="https://dev-x3cci6dykofnlj5z.eu.auth0.com/oauth/token" + VALIDATION_URL="https://dev-x3cci6dykofnlj5z.eu.auth0.com/.well-known/jwks.json" + + ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-file ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json --validation-url "$VALIDATION_URL" + ''; + scripts.run-jwtinfo-test-keycloak.exec = '' gum format "### JwtInfo request against priv Keycloak" @@ -617,6 +626,15 @@ in { ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-json "$JWTINFO_TEST_KEYCLOAK" --validation-url "$VALIDATION_URL" ''; + scripts.run-jwtinfo-test-keycloak-values-file.exec = '' + gum format "### JwtInfo request against priv Keycloak with values file" + + REQ_URL="https://keycloak.k3s.os76.xyz/realms/os76/protocol/openid-connect/token" + VALIDATION_URL="https://keycloak.k3s.os76.xyz/realms/os76/protocol/openid-connect/certs" + + ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-file ~/.config/https-wrench/jwtinfo_test_keycloak_req_values.json --validation-url "$VALIDATION_URL" + ''; + scripts.run-go-tests.exec = '' gum format "## Run GO tests" diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index d2bd065..ddd3ef4 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -12,6 +12,7 @@ import ( "mime" "net/http" "net/url" + "os" "strconv" "strings" "time" @@ -148,6 +149,26 @@ func ParseRequestJSONValues( return reqValuesMap, nil } +func ReadRequestValuesFile( + fileName string, + reqValuesMap map[string]string, +) ( + map[string]string, + error, +) { + data, err := os.ReadFile(fileName) + if err != nil { + return nil, fmt.Errorf("unable to read request's values file: %w", err) + } + + returnValuesMap, err := ParseRequestJSONValues(string(data), reqValuesMap) + if err != nil { + return nil, fmt.Errorf("unable to parse JSON from requests's values file: %w", err) + } + + return returnValuesMap, nil +} + func isValidJSON(data []byte) bool { var v any return json.Unmarshal(data, &v) == nil diff --git a/internal/jwtinfo/jwtinfo_test.go b/internal/jwtinfo/jwtinfo_test.go index 500557c..8b82712 100644 --- a/internal/jwtinfo/jwtinfo_test.go +++ b/internal/jwtinfo/jwtinfo_test.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "io" "maps" + "os" "testing" "time" @@ -13,13 +14,82 @@ import ( "github.com/stretchr/testify/require" ) +func TestReadRequestValuesFile(t *testing.T) { + inputMap := map[string]string{ + "testKey": "testValue", + } + + tests := []struct { + name string + fileContent []byte + expErr bool + errMsg string + }{ + { + name: "success", + fileContent: []byte("{\"jsonKey\":\"jsonValue\"}"), + }, + { + name: "No JSON content", + fileContent: []byte("not json"), + expErr: true, + errMsg: "unable to parse JSON from requests's values file", + }, + } + + for _, tc := range tests { + tt := tc + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + tempFile, err := createTmpFileWithContent( + tmpDir, + "fileReadTest.txt", + tt.fileContent, + ) + + require.NoError(t, err) + + outputMap, err := ReadRequestValuesFile( + tempFile, + inputMap, + ) + + if tt.expErr { + require.ErrorContains(t, err, tt.errMsg) + return + } + + require.NoError(t, err) + require.Equal(t, "jsonValue", outputMap["jsonKey"]) + require.Equal(t, "testValue", outputMap["testKey"]) + + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + }) + } + + t.Run("fileNotExist", func(t *testing.T) { + t.Parallel() + + _, err := ReadRequestValuesFile( + "fileNotExist", + inputMap, + ) + + require.ErrorContains(t, err, "no such file or directory") + }) +} + func TestParseRequestJSONValues(t *testing.T) { - inputMap := make(map[string]string) - inputMap["testKey"] = "testValue" + inputMap := map[string]string{ + "testKey": "testValue", + } - mapToValidJSON := make(map[string]string) - mapToValidJSON["testKey2"] = "testValue2" - mapToValidJSON["testKey3"] = "testValue3" + mapToValidJSON := map[string]string{ + "testKey2": "testValue2", + "testKey3": "testValue3", + } tests := []struct { name string diff --git a/internal/jwtinfo/main_test.go b/internal/jwtinfo/main_test.go index a2e040e..332e208 100644 --- a/internal/jwtinfo/main_test.go +++ b/internal/jwtinfo/main_test.go @@ -247,3 +247,26 @@ func NewJwtTestServer() (*httptest.Server, error) { return ts, nil } + +func createTmpFileWithContent( + tempDir string, + filePattern string, + fileContent []byte, +) (filePath string, err error) { + f, err := os.CreateTemp(tempDir, filePattern) + if err != nil { + return emptyString, err + } + + defer func() { + if closeErr := f.Close(); closeErr != nil { + err = errors.Join(err, closeErr) + } + }() + + if err = os.WriteFile(f.Name(), fileContent, 0o600); err != nil { + return emptyString, err + } + + return f.Name(), nil +} From 6eaabd7de2a8e249eb3e6fb30395b745cdb957f2 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Thu, 26 Mar 2026 04:01:07 +0100 Subject: [PATCH 2/3] fix: typo and variable allocation --- internal/jwtinfo/jwtinfo.go | 2 +- internal/jwtinfo/jwtinfo_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index ddd3ef4..460864a 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -163,7 +163,7 @@ func ReadRequestValuesFile( returnValuesMap, err := ParseRequestJSONValues(string(data), reqValuesMap) if err != nil { - return nil, fmt.Errorf("unable to parse JSON from requests's values file: %w", err) + return nil, fmt.Errorf("unable to parse JSON from request's values file: %w", err) } return returnValuesMap, nil diff --git a/internal/jwtinfo/jwtinfo_test.go b/internal/jwtinfo/jwtinfo_test.go index 8b82712..1b204b0 100644 --- a/internal/jwtinfo/jwtinfo_test.go +++ b/internal/jwtinfo/jwtinfo_test.go @@ -15,10 +15,6 @@ import ( ) func TestReadRequestValuesFile(t *testing.T) { - inputMap := map[string]string{ - "testKey": "testValue", - } - tests := []struct { name string fileContent []byte @@ -42,6 +38,10 @@ func TestReadRequestValuesFile(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() + inputMap := map[string]string{ + "testKey": "testValue", + } + tmpDir := t.TempDir() tempFile, err := createTmpFileWithContent( tmpDir, From 6a85c1de78197fb31539d8066278e6a5bc50046e Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Thu, 26 Mar 2026 04:06:34 +0100 Subject: [PATCH 3/3] fix: missing variable and typo in tests --- internal/jwtinfo/jwtinfo_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/jwtinfo/jwtinfo_test.go b/internal/jwtinfo/jwtinfo_test.go index 1b204b0..31de5ba 100644 --- a/internal/jwtinfo/jwtinfo_test.go +++ b/internal/jwtinfo/jwtinfo_test.go @@ -29,7 +29,7 @@ func TestReadRequestValuesFile(t *testing.T) { name: "No JSON content", fileContent: []byte("not json"), expErr: true, - errMsg: "unable to parse JSON from requests's values file", + errMsg: "unable to parse JSON from request's values file", }, } @@ -72,6 +72,10 @@ func TestReadRequestValuesFile(t *testing.T) { t.Run("fileNotExist", func(t *testing.T) { t.Parallel() + inputMap := map[string]string{ + "testKey": "testValue", + } + _, err := ReadRequestValuesFile( "fileNotExist", inputMap,