Skip to content
Merged
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ imscli authorize user help

The complete documentation of the project is available in the [DOCUMENTATION.md](DOCUMENTATION.md) file.

## Development Notes

### PersistentPreRunE and subcommands

The root command defines a `PersistentPreRunE` that loads configuration from flags, environment variables, and config files (see `cmd/root.go`). In cobra, if a subcommand defines its own `PersistentPreRunE`, it **overrides** the parent's — the root's `PersistentPreRunE` will not run for that subcommand or its children. If you need to add a `PersistentPreRunE` to a subcommand, you must explicitly call the parent's first.

## Contributing

Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for more information.
Expand Down
2 changes: 1 addition & 1 deletion cmd/admin/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func OrganizationsCmd(imsConfig *ims.Config) *cobra.Command {
},
}
cmd.Flags().StringVarP(&imsConfig.Guid, "guid", "g", "", "User ID.")
cmd.Flags().StringVarP(&imsConfig.AuthSrc, "authSrc", "s", "", "Authorization source.")
cmd.Flags().StringVarP(&imsConfig.AuthSrc, "authSrc", "A", "", "Authorization source.")
cmd.Flags().StringVarP(&imsConfig.ClientID, "clientID", "c", "", "IMS client ID.")
cmd.Flags().StringVarP(&imsConfig.ServiceToken, "serviceToken", "t", "", "Service token.")
cmd.Flags().StringVarP(&imsConfig.OrgsAPIVersion, "orgsApiVersion", "a", "v5", "Admin organizations API version.")
Expand Down
2 changes: 1 addition & 1 deletion cmd/admin/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func ProfileCmd(imsConfig *ims.Config) *cobra.Command {
},
}
cmd.Flags().StringVarP(&imsConfig.Guid, "guid", "g", "", "User ID.")
cmd.Flags().StringVarP(&imsConfig.AuthSrc, "authSrc", "s", "", "Authorization source.")
cmd.Flags().StringVarP(&imsConfig.AuthSrc, "authSrc", "A", "", "Authorization source.")
cmd.Flags().StringVarP(&imsConfig.ClientID, "clientID", "c", "", "IMS client ID.")
cmd.Flags().StringVarP(&imsConfig.ServiceToken, "serviceToken", "t", "", "Service token.")
cmd.Flags().StringVarP(&imsConfig.ProfileAPIVersion, "profileApiVersion", "a", "v1", "Admin profile API version.")
Expand Down
4 changes: 2 additions & 2 deletions cmd/authz/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ func JWTCmd(imsConfig *ims.Config) *cobra.Command {
}

cmd.Flags().StringVarP(&imsConfig.ClientID, "clientID", "c", "", "IMS Client ID.")
cmd.Flags().StringVarP(&imsConfig.ClientSecret, "clientSecret", "s", "", "IMS Client secret.")
cmd.Flags().StringVarP(&imsConfig.ClientSecret, "clientSecret", "p", "", "IMS Client secret.")
cmd.Flags().StringVarP(&imsConfig.Organization, "organization", "o", "", "IMS Organization.")
cmd.Flags().StringVarP(&imsConfig.Account, "account", "a", "", "Technical Account ID.")
cmd.Flags().StringVarP(&imsConfig.Account, "account", "A", "", "Technical Account ID.")
cmd.Flags().StringVarP(&imsConfig.PrivateKeyPath, "privateKey", "k", "", "Private Key file.")
cmd.Flags().StringSliceVarP(&imsConfig.Metascopes, "metascopes", "m", []string{}, "Metascopes to request.")

Expand Down
3 changes: 1 addition & 2 deletions cmd/authz/pkce.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ func UserPkceCmd(imsConfig *ims.Config) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true

imsConfig.PKCE = true
resp, err := imsConfig.AuthorizeUser()
resp, err := imsConfig.AuthorizeUserPKCE()
if err != nil {
return fmt.Errorf("error in user authorization: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/authz/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func ServiceCmd(imsConfig *ims.Config) *cobra.Command {

cmd.Flags().StringVarP(&imsConfig.ClientID, "clientID", "c", "", "IMS client ID.")
cmd.Flags().StringVarP(&imsConfig.ClientSecret, "clientSecret", "p", "", "IMS client secret.")
cmd.Flags().StringVarP(&imsConfig.AuthorizationCode, "authorizationCode", "a", "", "Permanent authorization code.")
cmd.Flags().StringVarP(&imsConfig.AuthorizationCode, "authorizationCode", "x", "", "Permanent authorization code.")

return cmd
}
1 change: 0 additions & 1 deletion cmd/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ func registerCmd(imsConfig *ims.Config) *cobra.Command {
},
}

cmd.Flags().StringVarP(&imsConfig.RegisterURL, "url", "u", "", "Registration endpoint URL.")
cmd.Flags().StringVarP(&imsConfig.ClientName, "clientName", "n", "", "Client application name.")
cmd.Flags().StringSliceVarP(&imsConfig.RedirectURIs, "redirectURIs", "r", []string{}, "Redirect URIs (comma-separated or multiple flags).")

Expand Down
6 changes: 3 additions & 3 deletions cmd/invalidate/invalidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func RefreshTokenCmd(imsConfig *ims.Config) *cobra.Command {
flagName: "refreshToken", field: &imsConfig.RefreshToken,
successMsg: "Refresh token successfully invalidated.",
extraFlags: func(cmd *cobra.Command, c *ims.Config) {
cmd.Flags().BoolVarP(&c.Cascading, "cascading", "a", false,
cmd.Flags().BoolVarP(&c.Cascading, "cascading", "C", false,
"Also invalidate all tokens obtained with the refresh token.")
},
})
Expand All @@ -80,7 +80,7 @@ func DeviceTokenCmd(imsConfig *ims.Config) *cobra.Command {
flagName: "deviceToken", field: &imsConfig.DeviceToken,
successMsg: "Token invalidated successfully.",
extraFlags: func(cmd *cobra.Command, c *ims.Config) {
cmd.Flags().BoolVarP(&c.Cascading, "cascading", "a", false,
cmd.Flags().BoolVarP(&c.Cascading, "cascading", "C", false,
"Also invalidate all tokens obtained with the device token.")
},
})
Expand All @@ -92,7 +92,7 @@ func ServiceTokenCmd(imsConfig *ims.Config) *cobra.Command {
flagName: "serviceToken", field: &imsConfig.ServiceToken,
successMsg: "Service token successfully invalidated.",
extraFlags: func(cmd *cobra.Command, c *ims.Config) {
cmd.Flags().StringVarP(&c.ClientSecret, "clientSecret", "s", "", "IMS Client Secret.")
cmd.Flags().StringVarP(&c.ClientSecret, "clientSecret", "p", "", "IMS Client Secret.")
},
})
}
8 changes: 4 additions & 4 deletions cmd/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ func refreshCmd(imsConfig *ims.Config) *cobra.Command {
return fmt.Errorf("error during the token refresh: %w", err)
}
if imsConfig.FullOutput {
data := map[string]interface{}{
"access_token": resp.AccessToken,
"refresh_token": resp.RefreshToken,
}
data := struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}{resp.AccessToken, resp.RefreshToken}
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("error marshalling full JSON response: %w", err)
Expand Down
32 changes: 17 additions & 15 deletions ims/authz_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import (
"github.com/pkg/browser"
)

const defaultPort = 8888

// Validate that:
// - the ims.Config struct has the necessary parameters for AuthorizeUser
// - the provided environment exists
Expand All @@ -42,6 +40,8 @@ func (i Config) validateAuthorizeUserConfig() error {
return fmt.Errorf("missing client id parameter")
case i.Organization == "":
return fmt.Errorf("missing organization parameter")
case i.Port <= 0:
return fmt.Errorf("missing or invalid port parameter")
case i.ClientSecret == "":
if i.PublicClient {
log.Println("all needed parameters verified not empty")
Expand All @@ -55,21 +55,23 @@ func (i Config) validateAuthorizeUserConfig() error {
return nil
}

// AuthorizeUser uses the standard Oauth2 authorization code grant flow. The Oauth2 configuration is
// taken from the Config struct.
// AuthorizeUser uses the standard OAuth2 authorization code grant flow.
func (i Config) AuthorizeUser() (string, error) {
return i.authorizeUser(false)
}

// AuthorizeUserPKCE uses the OAuth2 authorization code grant flow with PKCE.
func (i Config) AuthorizeUserPKCE() (string, error) {
return i.authorizeUser(true)
}

func (i Config) authorizeUser(pkce bool) (string, error) {
// Perform parameter validation
err := i.validateAuthorizeUserConfig()
if err != nil {
return "", fmt.Errorf("invalid parameters for login user: %w", err)
}

// Use default port if not specified
port := i.Port
if port == 0 {
port = defaultPort
}

c, err := i.newIMSClient()
if err != nil {
return "", fmt.Errorf("error creating the IMS client: %w", err)
Expand All @@ -80,8 +82,8 @@ func (i Config) AuthorizeUser() (string, error) {
ClientID: i.ClientID,
ClientSecret: i.ClientSecret,
Scope: i.Scopes,
UsePKCE: i.PKCE,
RedirectURI: fmt.Sprintf("http://localhost:%d", port),
UsePKCE: pkce,
RedirectURI: fmt.Sprintf("http://localhost:%d", i.Port),
OnError: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `
<h1>An error occurred</h1>
Expand All @@ -99,15 +101,15 @@ func (i Config) AuthorizeUser() (string, error) {
return "", fmt.Errorf("create authorization server: %w", err)
}

listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", i.Port))
if err != nil {
return "", fmt.Errorf("unable to listen at port %d", port)
return "", fmt.Errorf("unable to listen at port %d", i.Port)
}
defer listener.Close()

log.Println("Local server successfully launched and contacted.")

localUrl := fmt.Sprintf("http://localhost:%d/", port)
localUrl := fmt.Sprintf("http://localhost:%d/", i.Port)

// Suppress "Opening in existing browser session." messages from chromium-based
// browsers. The CLI token output goes to stdout, so stray browser messages
Expand Down
3 changes: 0 additions & 3 deletions ims/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,17 @@ type Config struct {
Cascading bool
Token string
Port int
PKCE bool
FullOutput bool
Guid string
AuthSrc string
DecodeFulfillableData bool
RegisterURL string
ClientName string
RedirectURIs []string
}

// Access token information
type TokenInfo struct {
AccessToken string
Expires int //(response.ExpiresIn * time.Millisecond),
Valid bool
Info string
}
Expand Down
10 changes: 6 additions & 4 deletions ims/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ func TestValidateAuthorizeUserConfig(t *testing.T) {
ClientSecret: "s",
Organization: "org",
Scopes: []string{"openid"},
Port: 8888,
}
tests := []struct {
name string
Expand All @@ -239,6 +240,7 @@ func TestValidateAuthorizeUserConfig(t *testing.T) {
{name: "missing clientID", config: withField(validConfig, func(c *Config) { c.ClientID = "" }), wantErr: "missing client id"},
{name: "missing organization", config: withField(validConfig, func(c *Config) { c.Organization = "" }), wantErr: "missing organization"},
{name: "missing secret non-public", config: withField(validConfig, func(c *Config) { c.ClientSecret = "" }), wantErr: "missing client secret"},
{name: "missing port", config: withField(validConfig, func(c *Config) { c.Port = 0 }), wantErr: "missing or invalid port"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -295,10 +297,10 @@ func TestValidateRegisterConfig(t *testing.T) {
config Config
wantErr string
}{
{name: "valid", config: Config{RegisterURL: "https://example.com/register", ClientName: "app", RedirectURIs: []string{"https://example.com/cb"}}, wantErr: ""},
{name: "missing URL", config: Config{ClientName: "app", RedirectURIs: []string{"https://example.com/cb"}}, wantErr: "missing registration endpoint URL"},
{name: "missing client name", config: Config{RegisterURL: "https://example.com/register", RedirectURIs: []string{"https://example.com/cb"}}, wantErr: "missing client name"},
{name: "missing redirect URIs", config: Config{RegisterURL: "https://example.com/register", ClientName: "app"}, wantErr: "missing redirect URIs"},
{name: "valid", config: Config{URL: "https://ims.example.com", ClientName: "app", RedirectURIs: []string{"https://example.com/cb"}}, wantErr: ""},
{name: "missing URL", config: Config{ClientName: "app", RedirectURIs: []string{"https://example.com/cb"}}, wantErr: "missing IMS base URL"},
{name: "missing client name", config: Config{URL: "https://ims.example.com", RedirectURIs: []string{"https://example.com/cb"}}, wantErr: "missing client name"},
{name: "missing redirect URIs", config: Config{URL: "https://ims.example.com", ClientName: "app"}, wantErr: "missing redirect URIs"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
2 changes: 0 additions & 2 deletions ims/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ package ims

import (
"fmt"
"time"

"github.com/adobe/ims-go/ims"
)
Expand Down Expand Up @@ -62,6 +61,5 @@ func (i Config) ClusterExchange() (TokenInfo, error) {

return TokenInfo{
AccessToken: r.AccessToken,
Expires: int(r.ExpiresIn * time.Millisecond),
}, nil
}
10 changes: 8 additions & 2 deletions ims/jwt_exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package ims
import (
"fmt"
"os"
"strings"
"time"

"github.com/adobe/ims-go/ims"
Expand All @@ -29,14 +30,20 @@ func (i Config) AuthorizeJWTExchange() (TokenInfo, error) {
if err != nil {
return TokenInfo{}, fmt.Errorf("error read private key file: %s, %w", i.PrivateKeyPath, err)
}
defer func() {
for i := range key {
key[i] = 0
}
}()

// Metascopes are passed as generic claims with the format map[string]interface{}
// where the strings are in the form: baseIMSUrl/s/metascope
// and the interface{} is 'true'

baseURL := strings.TrimRight(i.URL, "/")
claims := make(map[string]interface{})
for _, metascope := range i.Metascopes {
claims[fmt.Sprintf("%s/s/%s", i.URL, metascope)] = true
claims[fmt.Sprintf("%s/s/%s", baseURL, metascope)] = true
}

r, err := c.ExchangeJWT(&ims.ExchangeJWTRequest{
Expand All @@ -54,6 +61,5 @@ func (i Config) AuthorizeJWTExchange() (TokenInfo, error) {

return TokenInfo{
AccessToken: r.AccessToken,
Expires: int(r.ExpiresIn * time.Millisecond),
}, nil
}
2 changes: 1 addition & 1 deletion ims/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (i Config) validateGetOrganizationsConfig() error {
switch i.OrgsAPIVersion {
case "v1", "v2", "v3", "v4", "v5", "v6":
default:
return fmt.Errorf("invalid API version parameter, use something like v5")
return fmt.Errorf("invalid API version parameter, latest version is v6")
}

switch {
Expand Down
3 changes: 1 addition & 2 deletions ims/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ package ims

import (
"fmt"

"github.com/adobe/ims-go/ims"
"time"
)

func (i Config) validateRefreshConfig() error {
Expand Down Expand Up @@ -56,7 +56,6 @@ func (i Config) Refresh() (RefreshInfo, error) {
return RefreshInfo{
TokenInfo: TokenInfo{
AccessToken: r.AccessToken,
Expires: int(r.ExpiresIn * time.Second),
},
RefreshToken: r.RefreshToken,
}, nil
Expand Down
8 changes: 5 additions & 3 deletions ims/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ type RegisterResponse struct {

func (i Config) validateRegisterConfig() error {
switch {
case i.RegisterURL == "":
return fmt.Errorf("missing registration endpoint URL parameter")
case i.URL == "":
return fmt.Errorf("missing IMS base URL parameter")
case i.ClientName == "":
return fmt.Errorf("missing client name parameter")
case len(i.RedirectURIs) == 0:
Expand Down Expand Up @@ -47,7 +47,9 @@ func (i Config) Register() (RegisterResponse, error) {
"redirect_uris": %s
}`, i.ClientName, redirectURIsJSON))

req, err := http.NewRequest("POST", i.RegisterURL, payload)
endpoint := strings.TrimRight(i.URL, "/") + "/ims/register"

req, err := http.NewRequest("POST", endpoint, payload)
if err != nil {
return RegisterResponse{}, fmt.Errorf("error creating request: %v", err)
}
Expand Down