Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions cmd/change_client_secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"

"code.cloudfoundry.org/uaa-cli/cli"
"code.cloudfoundry.org/uaa-cli/config"
"code.cloudfoundry.org/uaa-cli/utils"
"github.com/cloudfoundry-community/go-uaa"
"github.com/spf13/cobra"
)

var oldSecret string

func ChangeClientSecretValidation(cfg config.Config, oldSecret, newSecret string) error {
if err := cli.EnsureContextInConfig(cfg); err != nil {
return err
}

context := cfg.GetActiveContext()
if context.GrantType != config.CLIENT_CREDENTIALS {
return errors.New("You must have a client_credentials token in your context to perform this command.")
}

if oldSecret == "" {
return cli.MissingArgumentError("old_secret")
}
if newSecret == "" {
return cli.MissingArgumentError("secret")
}
return nil
}

func ChangeClientSecretCmd(api *uaa.API, log cli.Logger, cfg config.Config, oldSecret, newSecret string) error {
context := cfg.GetActiveContext()
clientId := context.ClientId

// Prepare the request body for the secret change
requestBody := map[string]interface{}{
"oldSecret": oldSecret,
"secret": newSecret,
}
Comment on lines +41 to +44

requestBodyJSON, err := json.Marshal(requestBody)
if err != nil {
return err
}

// Make the API call to change the client secret
path := fmt.Sprintf("/oauth/clients/%s/secret", clientId)
headers := []string{"Content-Type: application/json"}

// Add zone header if specified
if cfg.ZoneSubdomain != "" {
headers = append(headers, fmt.Sprintf("X-Identity-Zone-Id: %s", cfg.ZoneSubdomain))
}
Comment on lines +53 to +58

_, _, status, err := api.Curl(path, "PUT", string(requestBodyJSON), headers)
if err != nil {
return err
}

if status >= 400 {
return errors.New("The secret for client " + clientId + " was not updated.")
}

log.Infof("The secret for client %v has been successfully updated.", utils.Emphasize(clientId))
return nil
}

var changeClientSecretCmd = &cobra.Command{
Use: "change-client-secret --old_secret OLD_SECRET --secret NEW_SECRET",
Short: "Change secret for authenticated client",
PreRun: func(cmd *cobra.Command, args []string) {
cli.NotifyValidationErrors(ChangeClientSecretValidation(GetSavedConfig(), oldSecret, clientSecret), cmd, log)
},
Run: func(cmd *cobra.Command, args []string) {
cfg := GetSavedConfig()
api := GetAPIFromSavedTokenInContext()
cli.NotifyErrorsWithRetry(ChangeClientSecretCmd(api, log, cfg, oldSecret, clientSecret), log, cfg)
},
}

func init() {
RootCmd.AddCommand(changeClientSecretCmd)
changeClientSecretCmd.Annotations = make(map[string]string)
changeClientSecretCmd.Annotations[CLIENT_CRUD_CATEGORY] = "true"
changeClientSecretCmd.Flags().StringVar(&oldSecret, "old_secret", "", "current client secret")
changeClientSecretCmd.Flags().StringVarP(&clientSecret, "secret", "s", "", "new client secret")
changeClientSecretCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain where the client resides")
}
128 changes: 128 additions & 0 deletions cmd/change_client_secret_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package cmd_test

import (
"net/http"

"code.cloudfoundry.org/uaa-cli/config"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gbytes"
. "github.com/onsi/gomega/gexec"
. "github.com/onsi/gomega/ghttp"
)

var _ = Describe("ChangeClientSecret", func() {
BeforeEach(func() {
c := config.NewConfigWithServerURL(server.URL())
// Create a client context with client_credentials grant type
ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
ctx.GrantType = config.CLIENT_CREDENTIALS
ctx.ClientId = "myclient"
c.AddContext(ctx)
config.WriteConfig(c)
})

It("successfully changes client secret", func() {
server.RouteToHandler("PUT", "/oauth/clients/myclient/secret", CombineHandlers(
VerifyRequest("PUT", "/oauth/clients/myclient/secret"),
VerifyJSON(`{"oldSecret":"oldsecret","secret":"newsecret"}`),
VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"),
VerifyHeaderKV("Content-Type", "application/json"),
RespondWith(http.StatusOK, `{"status":"ok","message":"Secret is updated"}`),
))

session := runCommand("change-client-secret", "--old_secret", "oldsecret", "--secret", "newsecret")

Expect(session.Out).To(Say("The secret for client myclient has been successfully updated."))
Eventually(session).Should(Exit(0))
})

It("displays error when API request fails", func() {
server.RouteToHandler("PUT", "/oauth/clients/myclient/secret", CombineHandlers(
VerifyRequest("PUT", "/oauth/clients/myclient/secret"),
VerifyJSON(`{"oldSecret":"wrongsecret","secret":"newsecret"}`),
RespondWith(http.StatusBadRequest, `{"error":"invalid_secret","error_description":"The old secret is incorrect"}`),
))

session := runCommand("change-client-secret", "--old_secret", "wrongsecret", "--secret", "newsecret")

Expect(session.Err).To(Say("The secret for client myclient was not updated."))
Expect(session.Out).To(Say("Retry with --verbose for more information."))
Eventually(session).Should(Exit(1))
})

It("complains when there is no active target", func() {
config.WriteConfig(config.NewConfig())
session := runCommand("change-client-secret", "--old_secret", "old", "--secret", "new")

Expect(session.Err).To(Say("You must set a target in order to use this command."))
Eventually(session).Should(Exit(1))
})

It("complains when there is no active context", func() {
c := config.NewConfig()
t := config.NewTarget()
c.AddTarget(t)
config.WriteConfig(c)
session := runCommand("change-client-secret", "--old_secret", "old", "--secret", "new")

Expect(session.Err).To(Say("You must have a token in your context to perform this command."))
Eventually(session).Should(Exit(1))
})

It("complains when context is not client_credentials", func() {
c := config.NewConfigWithServerURL(server.URL())
// Create a password grant context (user context)
ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
ctx.GrantType = config.PASSWORD
ctx.Username = "testuser"
c.AddContext(ctx)
config.WriteConfig(c)

session := runCommand("change-client-secret", "--old_secret", "old", "--secret", "new")

Expect(session.Err).To(Say("You must have a client_credentials token in your context to perform this command."))
Eventually(session).Should(Exit(1))
})

It("supports zone switching", func() {
server.RouteToHandler("PUT", "/oauth/clients/myclient/secret", CombineHandlers(
VerifyRequest("PUT", "/oauth/clients/myclient/secret"),
VerifyJSON(`{"oldSecret":"oldsecret","secret":"newsecret"}`),
VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"),
VerifyHeaderKV("X-Identity-Zone-Id", "twilight-zone"),
RespondWith(http.StatusOK, `{"status":"ok","message":"Secret is updated"}`),
))

session := runCommand("change-client-secret", "--old_secret", "oldsecret", "--secret", "newsecret", "--zone", "twilight-zone")

Expect(session.Out).To(Say("The secret for client myclient has been successfully updated."))
Eventually(session).Should(Exit(0))
})

It("shows verbose output when requested", func() {
server.RouteToHandler("PUT", "/oauth/clients/myclient/secret", CombineHandlers(
VerifyRequest("PUT", "/oauth/clients/myclient/secret"),
RespondWith(http.StatusOK, `{"status":"ok","message":"Secret is updated"}`),
))

session := runCommand("change-client-secret", "--old_secret", "oldsecret", "--secret", "newsecret", "--verbose")

Expect(session.Out).To(Say("The secret for client myclient has been successfully updated."))
Eventually(session).Should(Exit(0))
})

It("complains when no old secret is provided", func() {
session := runCommand("change-client-secret", "--secret", "newsecret")

Expect(session.Err).To(Say("Missing argument `old_secret` must be specified."))
Eventually(session).Should(Exit(1))
})

It("complains when no new secret is provided", func() {
session := runCommand("change-client-secret", "--old_secret", "oldsecret")

Expect(session.Err).To(Say("Missing argument `secret` must be specified."))
Eventually(session).Should(Exit(1))
})
})
1 change: 1 addition & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Each command name below links to a page with a full description, including all a
| [`get-client`](commands/get-client.md) | View a client registration |
| [`list-clients`](commands/list-clients.md) | See all clients in the targeted UAA |
| [`set-client-secret`](commands/set-client-secret.md) | Update the secret for a client |
| [`change-client-secret`](commands/change-client-secret.md) | Change secret for authenticated client |

## Managing Users

Expand Down
62 changes: 62 additions & 0 deletions docs/commands/change-client-secret.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# uaa change-client-secret

## Overview

Change the secret for the currently authenticated client. This command allows a client to change its own secret by providing both the old secret and the new secret.

Comment on lines +1 to +6
## Usage

```
uaa change-client-secret --old_secret OLD_SECRET --secret NEW_SECRET [flags]
```

## Required Authentication

This command requires an active client context obtained via the `client_credentials` grant type.

## Arguments

| Argument | Description |
|----------|-------------|
| `--old_secret` | The current secret for the client |
| `--secret`, `-s` | The new secret for the client |

## Options

| Option | Description |
|--------|-------------|
| `--zone`, `-z` | Identity zone subdomain where the client resides |
| `--verbose`, `-v` | Display verbose output including HTTP request/response details |

## Examples

### Change client secret with explicit values

```bash
uaa change-client-secret --old_secret currentsecret --secret newsecret
```

### Change client secret in a specific zone

```bash
uaa change-client-secret --old_secret currentsecret --secret newsecret --zone myzone
```

### Change client secret with verbose output

```bash
uaa change-client-secret --old_secret currentsecret --secret newsecret --verbose
```

## Prerequisites

1. You must have targeted a UAA server using `uaa target`
2. You must have an active client context with `client_credentials` grant type (obtained via `uaa get-client-credentials-token`)
3. The client must have the necessary permissions to change its own secret

## Notes

- This command is for self-service secret changes where a client changes its own secret
- Both the old and new secrets must be provided for security reasons
- After changing the secret, you will need to re-authenticate with the new secret
- Use `--verbose` to see the actual HTTP request being made to the UAA
2 changes: 1 addition & 1 deletion docs/migrating-from-uaac.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ The `uaa` CLI outputs a combination of human-readable status messages and JSON d
| `uaac client update [id]` | [`uaa update-client CLIENT_ID ...`](commands/update-client.md) | uaac supports `--interactive` / `-i`; uaa-cli does not |
| `uaac client delete [id]` | [`uaa delete-client CLIENT_ID`](commands/delete-client.md) | |
| `uaac secret set [id]` | [`uaa set-client-secret CLIENT_ID -s SECRET`](commands/set-client-secret.md) | |
| `uaac secret change` | *(no equivalent)* | Use `uaa curl /oauth/clients/CLIENT_ID/secret -X PUT -d '{"oldSecret":"OLD","secret":"NEW"}'` |
| `uaac secret change` | [`uaa change-client-secret --old_secret OLD --secret NEW`](commands/change-client-secret.md) | Client must be authenticated with client_credentials token |
| `uaac client jwt add [id]` | *(no equivalent)* | Use `uaa curl /oauth/clients/CLIENT_ID/clientjwt -X PUT -d '{...}'` |
| `uaac client jwt update [id]` | *(no equivalent)* | Use `uaa curl /oauth/clients/CLIENT_ID/clientjwt -X PUT -d '{...}'` |
| `uaac client jwt delete [id]` | *(no equivalent)* | Use `uaa curl /oauth/clients/CLIENT_ID/clientjwt -X DELETE` |
Expand Down