diff --git a/cmd/authz/pkce.go b/cmd/authz/pkce.go
index d848fde..7252d7f 100644
--- a/cmd/authz/pkce.go
+++ b/cmd/authz/pkce.go
@@ -42,7 +42,7 @@ func UserPkceCmd(imsConfig *ims.Config) *cobra.Command {
cmd.Flags().StringVarP(&imsConfig.Organization, "organization", "o", "", "IMS Organization.")
cmd.Flags().StringSliceVarP(&imsConfig.Scopes, "scopes", "s", []string{""}, "Scopes to request.")
cmd.Flags().BoolVarP(&imsConfig.PublicClient, "public", "b", false, "Public client, ignore secret.")
- //TODO: cmd.Flags().IntVarP(&imsConfig.Port, "port", "C", 8888, "Local port to be used by the OAuth Client.")
+ cmd.Flags().IntVarP(&imsConfig.Port, "port", "l", 8888, "Local port to be used by the OAuth Client.")
return cmd
}
diff --git a/cmd/authz/user.go b/cmd/authz/user.go
index 2ebdff8..66c39ae 100644
--- a/cmd/authz/user.go
+++ b/cmd/authz/user.go
@@ -41,7 +41,7 @@ func UserCmd(imsConfig *ims.Config) *cobra.Command {
cmd.Flags().StringVarP(&imsConfig.ClientSecret, "clientSecret", "p", "", "IMS client secret.")
cmd.Flags().StringVarP(&imsConfig.Organization, "organization", "o", "", "IMS Organization.")
cmd.Flags().StringSliceVarP(&imsConfig.Scopes, "scopes", "s", []string{""}, "Scopes to request.")
- //TODO: cmd.Flags().IntVarP(&imsConfig.Port, "port", "C", 8888, "Local port to be used by the OAuth Client.")
+ cmd.Flags().IntVarP(&imsConfig.Port, "port", "l", 8888, "Local port to be used by the OAuth Client.")
return cmd
}
diff --git a/cmd/decode.go b/cmd/decode.go
index c3f9632..baa9c12 100644
--- a/cmd/decode.go
+++ b/cmd/decode.go
@@ -22,18 +22,20 @@ func decodeCmd(imsConfig *ims.Config) *cobra.Command {
Use: "decode",
Aliases: []string{"dec"},
Short: "Decode a JWT token.",
- Long: "Decode a JWT token.",
+ Long: "Decode a JWT token and display the header and payload as prettified JSON.",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
cmd.SilenceErrors = true
- resp, err := imsConfig.DecodeToken()
+ decoded, err := imsConfig.DecodeToken()
if err != nil {
return fmt.Errorf("error decoding the token: %v", err)
}
- for _, part := range resp {
- fmt.Println(part)
- }
+
+ fmt.Println(decoded.Header)
+ fmt.Println()
+ fmt.Println(decoded.Payload)
+
return nil
},
}
diff --git a/cmd/organizations.go b/cmd/organizations.go
index 433f395..8f87945 100644
--- a/cmd/organizations.go
+++ b/cmd/organizations.go
@@ -14,6 +14,7 @@ import (
"fmt"
"github.com/adobe/imscli/ims"
+ "github.com/adobe/imscli/output"
"github.com/spf13/cobra"
)
@@ -32,7 +33,7 @@ func organizationsCmd(imsConfig *ims.Config) *cobra.Command {
if err != nil {
return fmt.Errorf("error in get organizations cmd: %v", err)
}
- fmt.Println(resp)
+ output.PrintPrettyJSON(resp)
return nil
},
}
diff --git a/cmd/profile.go b/cmd/profile.go
index 39e9e74..903f96f 100644
--- a/cmd/profile.go
+++ b/cmd/profile.go
@@ -14,6 +14,7 @@ import (
"fmt"
"github.com/adobe/imscli/ims"
+ "github.com/adobe/imscli/output"
"github.com/spf13/cobra"
)
@@ -31,7 +32,7 @@ func profileCmd(imsConfig *ims.Config) *cobra.Command {
if err != nil {
return fmt.Errorf("error in get profile cmd: %v", err)
}
- fmt.Println(resp)
+ output.PrintPrettyJSON(resp)
return nil
},
}
diff --git a/cmd/root.go b/cmd/root.go
index 4e0004e..4916557 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -11,7 +11,7 @@
package cmd
import (
- "io/ioutil"
+ "io"
"log"
"github.com/adobe/imscli/ims"
@@ -30,7 +30,7 @@ func RootCmd(version string) *cobra.Command {
Version: version,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if !verbose {
- log.SetOutput(ioutil.Discard)
+ log.SetOutput(io.Discard)
}
// This call of the initParams will load all env vars, config file and flags.
return initParams(cmd, imsConfig, configFile)
diff --git a/cmd/validate/access_token.go b/cmd/validate/access_token.go
index de0a8ed..8fa7ee2 100644
--- a/cmd/validate/access_token.go
+++ b/cmd/validate/access_token.go
@@ -14,6 +14,7 @@ import (
"fmt"
"github.com/adobe/imscli/ims"
+ "github.com/adobe/imscli/output"
"github.com/spf13/cobra"
)
@@ -34,7 +35,7 @@ func AccessTokenCmd(imsConfig *ims.Config) *cobra.Command {
if !resp.Valid {
return fmt.Errorf("invalid token: %v", resp.Info)
}
- fmt.Println(resp.Info)
+ output.PrintPrettyJSON(resp.Info)
return nil
},
}
diff --git a/cmd/validate/authorization_code.go b/cmd/validate/authorization_code.go
index 3b0ac64..49e7750 100644
--- a/cmd/validate/authorization_code.go
+++ b/cmd/validate/authorization_code.go
@@ -14,6 +14,7 @@ import (
"fmt"
"github.com/adobe/imscli/ims"
+ "github.com/adobe/imscli/output"
"github.com/spf13/cobra"
)
@@ -34,7 +35,7 @@ func AuthzCodeCmd(imsConfig *ims.Config) *cobra.Command {
if !resp.Valid {
return fmt.Errorf("invalid token: %v", resp.Info)
}
- fmt.Println(resp.Info)
+ output.PrintPrettyJSON(resp.Info)
return nil
},
}
diff --git a/cmd/validate/device_token.go b/cmd/validate/device_token.go
index d9d8741..dd50fe3 100644
--- a/cmd/validate/device_token.go
+++ b/cmd/validate/device_token.go
@@ -14,6 +14,7 @@ import (
"fmt"
"github.com/adobe/imscli/ims"
+ "github.com/adobe/imscli/output"
"github.com/spf13/cobra"
)
@@ -34,7 +35,7 @@ func DeviceTokenCmd(imsConfig *ims.Config) *cobra.Command {
if !resp.Valid {
return fmt.Errorf("invalid token: %v", resp.Info)
}
- fmt.Println(resp.Info)
+ output.PrintPrettyJSON(resp.Info)
return nil
},
}
diff --git a/cmd/validate/refresh_token.go b/cmd/validate/refresh_token.go
index 3ca16f3..5c0ab9e 100644
--- a/cmd/validate/refresh_token.go
+++ b/cmd/validate/refresh_token.go
@@ -14,6 +14,7 @@ import (
"fmt"
"github.com/adobe/imscli/ims"
+ "github.com/adobe/imscli/output"
"github.com/spf13/cobra"
)
@@ -34,7 +35,7 @@ func RefreshTokenCmd(imsConfig *ims.Config) *cobra.Command {
if !resp.Valid {
return fmt.Errorf("invalid token: %v", resp.Info)
}
- fmt.Println(resp.Info)
+ output.PrintPrettyJSON(resp.Info)
return nil
},
}
diff --git a/ims/authz_user.go b/ims/authz_user.go
index a013dbf..f70ad8f 100644
--- a/ims/authz_user.go
+++ b/ims/authz_user.go
@@ -24,7 +24,7 @@ import (
"github.com/pkg/browser"
)
-const port = 8888
+const defaultPort = 8888
/*
* Validate that:
@@ -66,6 +66,12 @@ func (i Config) AuthorizeUser() (string, error) {
return "", fmt.Errorf("invalid parameters for login user: %v", err)
}
+ // Use default port if not specified
+ port := i.Port
+ if port == 0 {
+ port = defaultPort
+ }
+
httpClient, err := i.httpClient()
if err != nil {
return "", fmt.Errorf("error creating the HTTP Client: %v", err)
@@ -85,7 +91,7 @@ func (i Config) AuthorizeUser() (string, error) {
ClientSecret: i.ClientSecret,
Scope: i.Scopes,
UsePKCE: i.PKCE,
- RedirectURI: "http://localhost:8888",
+ RedirectURI: fmt.Sprintf("http://localhost:%d", port),
OnError: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `
An error occurred
diff --git a/ims/decode.go b/ims/decode.go
index 8bb1ce0..4ab4db2 100644
--- a/ims/decode.go
+++ b/ims/decode.go
@@ -11,11 +11,20 @@
package ims
import (
+ "bytes"
"encoding/base64"
+ "encoding/json"
"fmt"
"strings"
)
+// DecodedToken represents the decoded parts of a JWT token.
+type DecodedToken struct {
+ Header string
+ Payload string
+ Signature string
+}
+
func (i Config) validateDecodeTokenConfig() error {
if i.Token == "" {
@@ -24,7 +33,7 @@ func (i Config) validateDecodeTokenConfig() error {
return nil
}
-func (i Config) DecodeToken() ([]string, error) {
+func (i Config) DecodeToken() (*DecodedToken, error) {
err := i.validateDecodeTokenConfig()
if err != nil {
return nil, fmt.Errorf("incomplete parameters for token decodification: %v", err)
@@ -35,13 +44,40 @@ func (i Config) DecodeToken() ([]string, error) {
return nil, fmt.Errorf("the JWT is not composed by 3 parts")
}
- // i<2 to not decode the signature since it is not encoded
- for i:=0; i<2; i++ {
- decodedPart, err := base64.RawURLEncoding.DecodeString(parts[i])
- if err != nil {
- return nil, fmt.Errorf("error decoding token, part %d: %v", i, err)
- }
- parts[i] = string(decodedPart)
+ // Decode header and payload (not signature since it's binary)
+ decoded := &DecodedToken{
+ Signature: parts[2],
+ }
+
+ // Decode and prettify header
+ headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
+ if err != nil {
+ return nil, fmt.Errorf("error decoding token header: %v", err)
+ }
+ decoded.Header, err = prettyJSON(headerBytes)
+ if err != nil {
+ return nil, fmt.Errorf("error formatting token header: %v", err)
+ }
+
+ // Decode and prettify payload
+ payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
+ if err != nil {
+ return nil, fmt.Errorf("error decoding token payload: %v", err)
+ }
+ decoded.Payload, err = prettyJSON(payloadBytes)
+ if err != nil {
+ return nil, fmt.Errorf("error formatting token payload: %v", err)
}
- return parts, nil
-}
\ No newline at end of file
+
+ return decoded, nil
+}
+
+// prettyJSON formats JSON bytes with indentation.
+func prettyJSON(data []byte) (string, error) {
+ var prettyBuf bytes.Buffer
+ err := json.Indent(&prettyBuf, data, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return prettyBuf.String(), nil
+}
diff --git a/ims/profile.go b/ims/profile.go
index d21108f..e175a65 100644
--- a/ims/profile.go
+++ b/ims/profile.go
@@ -145,11 +145,11 @@ func modifyFulfillableData(data string) (string, error) {
}
defer func() {
if _, gzErr := io.Copy(io.Discard, gzipReader); gzErr != nil {
- fmt.Errorf("error while consuming the gzip reader: %v", gzErr)
+ log.Printf("error while consuming the gzip reader: %v", gzErr)
}
if gzErr := gzipReader.Close(); gzErr != nil {
- fmt.Errorf("unable to close gzip reader: %v", gzErr)
+ log.Printf("unable to close gzip reader: %v", gzErr)
}
}()
diff --git a/output/output.go b/output/output.go
new file mode 100644
index 0000000..12d43cf
--- /dev/null
+++ b/output/output.go
@@ -0,0 +1,30 @@
+// Copyright 2025 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may obtain a copy
+// of the License at http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed under
+// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+// OF ANY KIND, either express or implied. See the License for the specific language
+// governing permissions and limitations under the License.
+
+package output
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+)
+
+// PrintPrettyJSON prints a JSON string with indentation.
+// If the input is not valid JSON, it prints the original string.
+func PrintPrettyJSON(jsonStr string) {
+ var prettyBuf bytes.Buffer
+ err := json.Indent(&prettyBuf, []byte(jsonStr), "", " ")
+ if err != nil {
+ // Not valid JSON, print as-is
+ fmt.Println(jsonStr)
+ return
+ }
+ fmt.Println(prettyBuf.String())
+}
diff --git a/output/output_test.go b/output/output_test.go
new file mode 100644
index 0000000..73005f1
--- /dev/null
+++ b/output/output_test.go
@@ -0,0 +1,82 @@
+// Copyright 2025 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may obtain a copy
+// of the License at http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed under
+// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+// OF ANY KIND, either express or implied. See the License for the specific language
+// governing permissions and limitations under the License.
+
+package output
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// captureStdout captures the output of a function that writes to stdout.
+func captureStdout(t *testing.T, fn func()) string {
+ t.Helper()
+ old := os.Stdout
+ r, w, err := os.Pipe()
+ if err != nil {
+ t.Fatalf("failed to create pipe: %v", err)
+ }
+ os.Stdout = w
+
+ fn()
+
+ w.Close()
+ os.Stdout = old
+
+ var buf bytes.Buffer
+ if _, err := io.Copy(&buf, r); err != nil {
+ t.Fatalf("failed to read pipe: %v", err)
+ }
+ return buf.String()
+}
+
+// loadExpected reads the expected output from a golden file in testdata/.
+func loadExpected(t *testing.T, filename string) string {
+ t.Helper()
+ data, err := os.ReadFile(filepath.Join("testdata", filename))
+ if err != nil {
+ t.Fatalf("failed to read golden file %s: %v", filename, err)
+ }
+ return string(data)
+}
+
+func TestPrintPrettyJSON(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ file string
+ }{
+ {name: "compact object is prettified", input: `{"name":"John","age":30}`, file: "compact_object.json"},
+ {name: "already indented JSON is normalized", input: "{\n \"key\": \"value\"\n}", file: "already_indented.json"},
+ {name: "compact array is prettified", input: `[{"id":1},{"id":2}]`, file: "compact_array.json"},
+ {name: "empty object", input: `{}`, file: "empty_object.json"},
+ {name: "empty array", input: `[]`, file: "empty_array.json"},
+ {name: "nested objects", input: `{"a":{"b":{"c":1}}}`, file: "nested_objects.json"},
+ {name: "non-JSON is printed as-is", input: "this is not JSON", file: "non_json.txt"},
+ {name: "empty string is printed as-is", input: "", file: "empty_string.txt"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ expected := loadExpected(t, tt.file)
+
+ got := captureStdout(t, func() {
+ PrintPrettyJSON(tt.input)
+ })
+
+ if got != expected {
+ t.Errorf("PrintPrettyJSON(%q)\ngot:\n%s\nexpected output is in testdata/%s", tt.input, got, tt.file)
+ }
+ })
+ }
+}
diff --git a/output/testdata/empty_string.txt b/output/testdata/empty_string.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/output/testdata/empty_string.txt
@@ -0,0 +1 @@
+
diff --git a/output/testdata/non_json.txt b/output/testdata/non_json.txt
new file mode 100644
index 0000000..7f8c66c
--- /dev/null
+++ b/output/testdata/non_json.txt
@@ -0,0 +1 @@
+this is not JSON