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..460864a 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 request'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..31de5ba 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,86 @@ import ( "github.com/stretchr/testify/require" ) +func TestReadRequestValuesFile(t *testing.T) { + 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 request's values file", + }, + } + + for _, tc := range tests { + tt := tc + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + inputMap := map[string]string{ + "testKey": "testValue", + } + + 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() + + inputMap := map[string]string{ + "testKey": "testValue", + } + + _, 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 +}