diff --git a/docs/auth0_token-exchange.md b/docs/auth0_token-exchange.md new file mode 100644 index 000000000..e732beb77 --- /dev/null +++ b/docs/auth0_token-exchange.md @@ -0,0 +1,17 @@ +--- +layout: default +has_toc: false +has_children: true +--- +# auth0 token-exchange + +Manage token exchange profiles. Token exchange profiles enable secure token exchange flows for authentication and authorization. + +## Commands + +- [auth0 token-exchange create](auth0_token-exchange_create.md) - Create a new token exchange profile +- [auth0 token-exchange delete](auth0_token-exchange_delete.md) - Delete a token exchange profile +- [auth0 token-exchange list](auth0_token-exchange_list.md) - List your token exchange profiles +- [auth0 token-exchange show](auth0_token-exchange_show.md) - Show a token exchange profile +- [auth0 token-exchange update](auth0_token-exchange_update.md) - Update a token exchange profile + diff --git a/docs/auth0_token-exchange_create.md b/docs/auth0_token-exchange_create.md new file mode 100644 index 000000000..c5f926bdf --- /dev/null +++ b/docs/auth0_token-exchange_create.md @@ -0,0 +1,61 @@ +--- +layout: default +parent: auth0 token-exchange +has_toc: false +--- +# auth0 token-exchange create + +Create a new token exchange profile. + +To create interactively, use `auth0 token-exchange create` with no flags. + +To create non-interactively, supply the name, subject token type, action ID, and type through the flags. + +## Usage +``` +auth0 token-exchange create [flags] +``` + +## Examples + +``` + auth0 token-exchange create + auth0 token-exchange create --name "My Token Exchange Profile" + auth0 token-exchange create --name "My Token Exchange Profile" --subject-token-type "urn:ietf:params:oauth:token-type:jwt" + auth0 token-exchange create --name "My Token Exchange Profile" --subject-token-type "urn:ietf:params:oauth:token-type:jwt" --action-id "act_123abc" --type "custom_authentication" + auth0 token-exchange create -n "My Token Exchange Profile" -s "urn:ietf:params:oauth:token-type:jwt" -a "act_123abc" -t "custom_authentication" --json + auth0 token-exchange create -n "My Token Exchange Profile" -s "urn:ietf:params:oauth:token-type:jwt" -a "act_123abc" -t "custom_authentication" --json-compact +``` + + +## Flags + +``` + -a, --action-id string Identifier of the action. + --json Output in json format. + --json-compact Output in compact json format. + -n, --name string Name of the token exchange profile. + -s, --subject-token-type string Type of the subject token. Must be a valid URI format (e.g., urn:ietf:params:oauth:token-type:jwt). Cannot use reserved prefixes: http://auth0.com, https://auth0.com, http://okta.com, https://okta.com, urn:ietf, urn:auth0, urn:okta. + -t, --type string Type of the token exchange profile. Currently only 'custom_authentication' is supported. +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 token-exchange create](auth0_token-exchange_create.md) - Create a new token exchange profile +- [auth0 token-exchange delete](auth0_token-exchange_delete.md) - Delete a token exchange profile +- [auth0 token-exchange list](auth0_token-exchange_list.md) - List your token exchange profiles +- [auth0 token-exchange show](auth0_token-exchange_show.md) - Show a token exchange profile +- [auth0 token-exchange update](auth0_token-exchange_update.md) - Update a token exchange profile + + diff --git a/docs/auth0_token-exchange_delete.md b/docs/auth0_token-exchange_delete.md new file mode 100644 index 000000000..feb9c93f5 --- /dev/null +++ b/docs/auth0_token-exchange_delete.md @@ -0,0 +1,56 @@ +--- +layout: default +parent: auth0 token-exchange +has_toc: false +--- +# auth0 token-exchange delete + +Delete a token exchange profile. + +To delete interactively, use `auth0 token-exchange delete` with no arguments. + +To delete non-interactively, supply the profile id and the `--force` flag to skip confirmation. + +## Usage +``` +auth0 token-exchange delete [flags] +``` + +## Examples + +``` + auth0 token-exchange delete + auth0 token-exchange rm + auth0 token-exchange delete + auth0 token-exchange delete --force + auth0 token-exchange delete + auth0 token-exchange delete --force +``` + + +## Flags + +``` + --force Skip confirmation. +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 token-exchange create](auth0_token-exchange_create.md) - Create a new token exchange profile +- [auth0 token-exchange delete](auth0_token-exchange_delete.md) - Delete a token exchange profile +- [auth0 token-exchange list](auth0_token-exchange_list.md) - List your token exchange profiles +- [auth0 token-exchange show](auth0_token-exchange_show.md) - Show a token exchange profile +- [auth0 token-exchange update](auth0_token-exchange_update.md) - Update a token exchange profile + + diff --git a/docs/auth0_token-exchange_list.md b/docs/auth0_token-exchange_list.md new file mode 100644 index 000000000..84062c6a6 --- /dev/null +++ b/docs/auth0_token-exchange_list.md @@ -0,0 +1,53 @@ +--- +layout: default +parent: auth0 token-exchange +has_toc: false +--- +# auth0 token-exchange list + +List your existing token exchange profiles. To create one, run: `auth0 token-exchange create`. + +## Usage +``` +auth0 token-exchange list [flags] +``` + +## Examples + +``` + auth0 token-exchange list + auth0 token-exchange ls + auth0 token-exchange ls --json + auth0 token-exchange ls --json-compact + auth0 token-exchange ls --csv +``` + + +## Flags + +``` + --csv Output in csv format. + --json Output in json format. + --json-compact Output in compact json format. +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 token-exchange create](auth0_token-exchange_create.md) - Create a new token exchange profile +- [auth0 token-exchange delete](auth0_token-exchange_delete.md) - Delete a token exchange profile +- [auth0 token-exchange list](auth0_token-exchange_list.md) - List your token exchange profiles +- [auth0 token-exchange show](auth0_token-exchange_show.md) - Show a token exchange profile +- [auth0 token-exchange update](auth0_token-exchange_update.md) - Update a token exchange profile + + diff --git a/docs/auth0_token-exchange_show.md b/docs/auth0_token-exchange_show.md new file mode 100644 index 000000000..81149ffc5 --- /dev/null +++ b/docs/auth0_token-exchange_show.md @@ -0,0 +1,51 @@ +--- +layout: default +parent: auth0 token-exchange +has_toc: false +--- +# auth0 token-exchange show + +Display the name, subject token type, action ID, type and other information about a token exchange profile. + +## Usage +``` +auth0 token-exchange show [flags] +``` + +## Examples + +``` + auth0 token-exchange show + auth0 token-exchange show + auth0 token-exchange show --json + auth0 token-exchange show --json-compact +``` + + +## Flags + +``` + --json Output in json format. + --json-compact Output in compact json format. +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 token-exchange create](auth0_token-exchange_create.md) - Create a new token exchange profile +- [auth0 token-exchange delete](auth0_token-exchange_delete.md) - Delete a token exchange profile +- [auth0 token-exchange list](auth0_token-exchange_list.md) - List your token exchange profiles +- [auth0 token-exchange show](auth0_token-exchange_show.md) - Show a token exchange profile +- [auth0 token-exchange update](auth0_token-exchange_update.md) - Update a token exchange profile + + diff --git a/docs/auth0_token-exchange_update.md b/docs/auth0_token-exchange_update.md new file mode 100644 index 000000000..3f503c888 --- /dev/null +++ b/docs/auth0_token-exchange_update.md @@ -0,0 +1,61 @@ +--- +layout: default +parent: auth0 token-exchange +has_toc: false +--- +# auth0 token-exchange update + +Update a token exchange profile. + +To update interactively, use `auth0 token-exchange update` with no arguments. + +To update non-interactively, supply the profile id, name, and subject token type through the flags. + +Note: Only name and subject token type can be updated. Action ID and type are immutable after creation. + +## Usage +``` +auth0 token-exchange update [flags] +``` + +## Examples + +``` + auth0 token-exchange update + auth0 token-exchange update + auth0 token-exchange update --name "Updated Profile Name" + auth0 token-exchange update --name "Updated Profile Name" --subject-token-type "urn:ietf:params:oauth:token-type:jwt" + auth0 token-exchange update -n "Updated Profile Name" -s "urn:ietf:params:oauth:token-type:jwt" --json + auth0 token-exchange update -n "Updated Profile Name" -s "urn:ietf:params:oauth:token-type:jwt" --json-compact +``` + + +## Flags + +``` + --json Output in json format. + --json-compact Output in compact json format. + -n, --name string Name of the token exchange profile. + -s, --subject-token-type string Type of the subject token. Must be a valid URI format (e.g., urn:ietf:params:oauth:token-type:jwt). Cannot use reserved prefixes: http://auth0.com, https://auth0.com, http://okta.com, https://okta.com, urn:ietf, urn:auth0, urn:okta. +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 token-exchange create](auth0_token-exchange_create.md) - Create a new token exchange profile +- [auth0 token-exchange delete](auth0_token-exchange_delete.md) - Delete a token exchange profile +- [auth0 token-exchange list](auth0_token-exchange_list.md) - List your token exchange profiles +- [auth0 token-exchange show](auth0_token-exchange_show.md) - Show a token exchange profile +- [auth0 token-exchange update](auth0_token-exchange_update.md) - Update a token exchange profile + + diff --git a/docs/index.md b/docs/index.md index 79c703b37..0454ce86b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -102,6 +102,7 @@ Authenticating as a user is not supported for **private cloud** tenants. Instead - [auth0 tenants](auth0_tenants.md) - Manage configured tenants - [auth0 terraform](auth0_terraform.md) - Manage terraform configuration for your Auth0 Tenant - [auth0 test](auth0_test.md) - Try your Universal Login box or get a token +- [auth0 token-exchange](auth0_token-exchange.md) - Manage token exchange profiles - [auth0 universal-login](auth0_universal-login.md) - Manage the Universal Login experience - [auth0 users](auth0_users.md) - Manage resources for users diff --git a/go.mod b/go.mod index 0a37d2823..8685d6dd9 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/PuerkitoBio/rehttp v1.4.0 github.com/atotto/clipboard v0.1.4 - github.com/auth0/go-auth0 v1.32.0 + github.com/auth0/go-auth0 v1.32.1 github.com/briandowns/spinner v1.23.2 github.com/charmbracelet/glamour v0.10.0 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e diff --git a/go.sum b/go.sum index 276b102da..cd57010f5 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/auth0/go-auth0 v1.32.0 h1:PuojPRBDQPvFMtXDX7ags8ackLVYXDU7gpTi7/8sEws= -github.com/auth0/go-auth0 v1.32.0/go.mod h1:32sQB1uAn+99fJo6N819EniKq8h785p0ag0lMWhiTaE= +github.com/auth0/go-auth0 v1.32.1 h1:AAXQqaNaFZWkRm2bg5mVVXpqDLmusv7v238uIaxuFpo= +github.com/auth0/go-auth0 v1.32.1/go.mod h1:32sQB1uAn+99fJo6N819EniKq8h785p0ag0lMWhiTaE= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0/go.mod h1:6L7zgvqo0idzI7IO8de6ZC051AfXb5ipkIJ7bIA2tGA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index b6678131e..7d039b0f2 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -148,6 +148,7 @@ var RequiredScopes = []string{ "read:attack_protection", "update:attack_protection", "read:event_streams", "create:event_streams", "update:event_streams", "delete:event_streams", "read:network_acls", "create:network_acls", "update:network_acls", "delete:network_acls", + "read:token_exchange_profiles", "create:token_exchange_profiles", "update:token_exchange_profiles", "delete:token_exchange_profiles", } // GetDeviceCode kicks-off the device authentication flow by requesting diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index 438d7828f..514aea343 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -32,6 +32,7 @@ type API struct { Role RoleAPI Rule RuleAPI Tenant TenantAPI + TokenExchange TokenExchangeAPI User UserAPI Jobs JobsAPI SelfServiceProfile SelfServiceProfileAPI @@ -66,6 +67,7 @@ func NewAPI(m *management.Management) *API { Role: m.Role, Rule: m.Rule, Tenant: m.Tenant, + TokenExchange: m.TokenExchangeProfile, User: m.User, Jobs: m.Job, SelfServiceProfile: m.SelfServiceProfile, diff --git a/internal/auth0/mock/token_exchange_mock.go b/internal/auth0/mock/token_exchange_mock.go new file mode 100644 index 000000000..1093cf6a0 --- /dev/null +++ b/internal/auth0/mock/token_exchange_mock.go @@ -0,0 +1,133 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: token_exchange.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + management "github.com/auth0/go-auth0/management" + gomock "github.com/golang/mock/gomock" +) + +// MockTokenExchangeAPI is a mock of TokenExchangeAPI interface. +type MockTokenExchangeAPI struct { + ctrl *gomock.Controller + recorder *MockTokenExchangeAPIMockRecorder +} + +// MockTokenExchangeAPIMockRecorder is the mock recorder for MockTokenExchangeAPI. +type MockTokenExchangeAPIMockRecorder struct { + mock *MockTokenExchangeAPI +} + +// NewMockTokenExchangeAPI creates a new mock instance. +func NewMockTokenExchangeAPI(ctrl *gomock.Controller) *MockTokenExchangeAPI { + mock := &MockTokenExchangeAPI{ctrl: ctrl} + mock.recorder = &MockTokenExchangeAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTokenExchangeAPI) EXPECT() *MockTokenExchangeAPIMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockTokenExchangeAPI) Create(ctx context.Context, profile *management.TokenExchangeProfile, opts ...management.RequestOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, profile} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockTokenExchangeAPIMockRecorder) Create(ctx, profile interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, profile}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockTokenExchangeAPI)(nil).Create), varargs...) +} + +// Delete mocks base method. +func (m *MockTokenExchangeAPI) Delete(ctx context.Context, id string, opts ...management.RequestOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, id} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Delete", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockTokenExchangeAPIMockRecorder) Delete(ctx, id interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, id}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockTokenExchangeAPI)(nil).Delete), varargs...) +} + +// List mocks base method. +func (m *MockTokenExchangeAPI) List(ctx context.Context, opts ...management.RequestOption) (*management.TokenExchangeProfileList, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].(*management.TokenExchangeProfileList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockTokenExchangeAPIMockRecorder) List(ctx interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTokenExchangeAPI)(nil).List), varargs...) +} + +// Read mocks base method. +func (m *MockTokenExchangeAPI) Read(ctx context.Context, id string, opts ...management.RequestOption) (*management.TokenExchangeProfile, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, id} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Read", varargs...) + ret0, _ := ret[0].(*management.TokenExchangeProfile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockTokenExchangeAPIMockRecorder) Read(ctx, id interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, id}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTokenExchangeAPI)(nil).Read), varargs...) +} + +// Update mocks base method. +func (m *MockTokenExchangeAPI) Update(ctx context.Context, id string, profile *management.TokenExchangeProfile, opts ...management.RequestOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, id, profile} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockTokenExchangeAPIMockRecorder) Update(ctx, id, profile interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, id, profile}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockTokenExchangeAPI)(nil).Update), varargs...) +} diff --git a/internal/auth0/token_exchange.go b/internal/auth0/token_exchange.go new file mode 100644 index 000000000..7f1762e09 --- /dev/null +++ b/internal/auth0/token_exchange.go @@ -0,0 +1,27 @@ +//go:generate mockgen -source=token_exchange.go -destination=mock/token_exchange_mock.go -package=mock + +package auth0 + +import ( + "context" + + "github.com/auth0/go-auth0/management" +) + +// TokenExchangeAPI is an interface that describes all the Token Exchange Profile related operations. +type TokenExchangeAPI interface { + // List retrieves all token exchange profiles. + List(ctx context.Context, opts ...management.RequestOption) (*management.TokenExchangeProfileList, error) + + // Read retrieves a token exchange profile by its ID. + Read(ctx context.Context, id string, opts ...management.RequestOption) (*management.TokenExchangeProfile, error) + + // Create creates a new token exchange profile. + Create(ctx context.Context, profile *management.TokenExchangeProfile, opts ...management.RequestOption) error + + // Update updates an existing token exchange profile. + Update(ctx context.Context, id string, profile *management.TokenExchangeProfile, opts ...management.RequestOption) error + + // Delete deletes a token exchange profile. + Delete(ctx context.Context, id string, opts ...management.RequestOption) error +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index a6634aff3..61d1fb5d7 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -164,7 +164,7 @@ func canPrompt(cmd *cobra.Command) bool { func shouldPromptWhenNoLocalFlagsSet(cmd *cobra.Command) bool { localFlagIsSet := false cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { - if f.Name != "json" && f.Name != "force" && f.Changed { + if f.Name != "json" && f.Name != "force" && f.Changed && f.Name != "json-compact" { localFlagIsSet = true } }) diff --git a/internal/cli/root.go b/internal/cli/root.go index 918362f7c..961696d79 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -174,6 +174,7 @@ func addSubCommands(rootCmd *cobra.Command, cli *cli) { rootCmd.AddCommand(eventStreamsCmd(cli)) rootCmd.AddCommand(networkACLCmd(cli)) rootCmd.AddCommand(tenantSettingsCmd(cli)) + rootCmd.AddCommand(tokenExchangeCmd(cli)) // Keep completion at the bottom. rootCmd.AddCommand(completionCmd(cli)) diff --git a/internal/cli/token_exchange.go b/internal/cli/token_exchange.go new file mode 100644 index 000000000..9d2101480 --- /dev/null +++ b/internal/cli/token_exchange.go @@ -0,0 +1,384 @@ +package cli + +import ( + "context" + "errors" + "fmt" + + "github.com/auth0/go-auth0/management" + "github.com/spf13/cobra" + + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/prompt" +) + +var ( + tokenExchangeProfileID = Argument{ + Name: "Id", + Help: "Id of the token exchange profile.", + } + + tokenExchangeProfileName = Flag{ + Name: "Name", + LongForm: "name", + ShortForm: "n", + Help: "Name of the token exchange profile.", + IsRequired: true, + } + + tokenExchangeProfileSubjectTokenType = Flag{ + Name: "Subject Token Type", + LongForm: "subject-token-type", + ShortForm: "s", + Help: "Type of the subject token. Must be a valid URI format (e.g., urn:ietf:params:oauth:token-type:jwt). Cannot use reserved prefixes: http://auth0.com, https://auth0.com, http://okta.com, https://okta.com, urn:ietf, urn:auth0, urn:okta.", + IsRequired: true, + } + + tokenExchangeProfileActionID = Flag{ + Name: "Action ID", + LongForm: "action-id", + ShortForm: "a", + Help: "Identifier of the action.", + IsRequired: true, + } + + tokenExchangeProfileType = Flag{ + Name: "Type", + LongForm: "type", + ShortForm: "t", + Help: "Type of the token exchange profile. Currently only 'custom_authentication' is supported.", + IsRequired: true, + } +) + +func tokenExchangeCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "token-exchange", + Aliases: []string{"te"}, + Short: "Manage token exchange profiles", + Long: "Manage token exchange profiles. Token exchange profiles enable secure token exchange flows for authentication and authorization.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listTokenExchangeProfilesCmd(cli)) + cmd.AddCommand(createTokenExchangeProfileCmd(cli)) + cmd.AddCommand(showTokenExchangeProfileCmd(cli)) + cmd.AddCommand(updateTokenExchangeProfileCmd(cli)) + cmd.AddCommand(deleteTokenExchangeProfileCmd(cli)) + + return cmd +} + +func listTokenExchangeProfilesCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + Short: "List your token exchange profiles", + Long: "List your existing token exchange profiles. To create one, run: `auth0 token-exchange create`.", + Example: ` auth0 token-exchange list + auth0 token-exchange ls + auth0 token-exchange ls --json + auth0 token-exchange ls --json-compact + auth0 token-exchange ls --csv`, + RunE: func(cmd *cobra.Command, args []string) error { + var list *management.TokenExchangeProfileList + + if err := ansi.Waiting(func() (err error) { + list, err = cli.api.TokenExchange.List(cmd.Context()) + return err + }); err != nil { + return fmt.Errorf("failed to list token exchange profiles: %w", err) + } + + cli.renderer.TokenExchangeProfileList(list.TokenExchangeProfiles) + + return nil + }, + } + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + cmd.Flags().BoolVar(&cli.csv, "csv", false, "Output in csv format.") + cmd.MarkFlagsMutuallyExclusive("json", "json-compact", "csv") + + return cmd +} + +func showTokenExchangeProfileCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "show", + Args: cobra.MaximumNArgs(1), + Short: "Show a token exchange profile", + Long: "Display the name, subject token type, action ID, type and other information about a token exchange profile.", + Example: ` auth0 token-exchange show + auth0 token-exchange show + auth0 token-exchange show --json + auth0 token-exchange show --json-compact`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + if err := tokenExchangeProfileID.Pick(cmd, &inputs.ID, cli.tokenExchangeProfilePickerOptions); err != nil { + return err + } + } else { + inputs.ID = args[0] + } + + var profile *management.TokenExchangeProfile + + if err := ansi.Waiting(func() (err error) { + profile, err = cli.api.TokenExchange.Read(cmd.Context(), inputs.ID) + return err + }); err != nil { + return fmt.Errorf("failed to read token exchange profile with ID %q: %w", inputs.ID, err) + } + + cli.renderer.TokenExchangeProfileShow(profile) + + return nil + }, + } + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + + return cmd +} + +func createTokenExchangeProfileCmd(cli *cli) *cobra.Command { + var inputs struct { + Name string + SubjectTokenType string + ActionID string + Type string + } + + cmd := &cobra.Command{ + Use: "create", + Args: cobra.NoArgs, + Short: "Create a new token exchange profile", + Long: "Create a new token exchange profile.\n\n" + + "To create interactively, use `auth0 token-exchange create` with no flags.\n\n" + + "To create non-interactively, supply the name, subject token type, action ID, and type through the flags.", + Example: ` auth0 token-exchange create + auth0 token-exchange create --name "My Token Exchange Profile" + auth0 token-exchange create --name "My Token Exchange Profile" --subject-token-type "urn:ietf:params:oauth:token-type:jwt" + auth0 token-exchange create --name "My Token Exchange Profile" --subject-token-type "urn:ietf:params:oauth:token-type:jwt" --action-id "act_123abc" --type "custom_authentication" + auth0 token-exchange create -n "My Token Exchange Profile" -s "urn:ietf:params:oauth:token-type:jwt" -a "act_123abc" -t "custom_authentication" --json + auth0 token-exchange create -n "My Token Exchange Profile" -s "urn:ietf:params:oauth:token-type:jwt" -a "act_123abc" -t "custom_authentication" --json-compact`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := tokenExchangeProfileName.Ask(cmd, &inputs.Name, nil); err != nil { + return err + } + + if err := tokenExchangeProfileSubjectTokenType.Ask(cmd, &inputs.SubjectTokenType, nil); err != nil { + return err + } + + // Use action picker to select an action with custom-token-exchange trigger. + if err := tokenExchangeProfileActionID.Pick(cmd, &inputs.ActionID, cli.customTokenExchangeActionPickerOptions); err != nil { + return err + } + + // Select type - currently only custom_authentication is supported. + if err := tokenExchangeProfileType.Select(cmd, &inputs.Type, []string{"custom_authentication"}, nil); err != nil { + return err + } + + profile := &management.TokenExchangeProfile{ + Name: &inputs.Name, + SubjectTokenType: &inputs.SubjectTokenType, + ActionID: &inputs.ActionID, + Type: &inputs.Type, + } + + if err := ansi.Waiting(func() error { + return cli.api.TokenExchange.Create(cmd.Context(), profile) + }); err != nil { + return fmt.Errorf("failed to create token exchange profile: %w", err) + } + + cli.renderer.TokenExchangeProfileCreate(profile) + + return nil + }, + } + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + tokenExchangeProfileName.RegisterString(cmd, &inputs.Name, "") + tokenExchangeProfileSubjectTokenType.RegisterString(cmd, &inputs.SubjectTokenType, "") + tokenExchangeProfileActionID.RegisterString(cmd, &inputs.ActionID, "") + tokenExchangeProfileType.RegisterString(cmd, &inputs.Type, "") + + return cmd +} + +func updateTokenExchangeProfileCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + Name string + SubjectTokenType string + } + + cmd := &cobra.Command{ + Use: "update", + Args: cobra.MaximumNArgs(1), + Short: "Update a token exchange profile", + Long: "Update a token exchange profile.\n\n" + + "To update interactively, use `auth0 token-exchange update` with no arguments.\n\n" + + "To update non-interactively, supply the profile id, name, and subject token type through the flags.\n\n" + + "Note: Only name and subject token type can be updated. Action ID and type are immutable after creation.", + Example: ` auth0 token-exchange update + auth0 token-exchange update + auth0 token-exchange update --name "Updated Profile Name" + auth0 token-exchange update --name "Updated Profile Name" --subject-token-type "urn:ietf:params:oauth:token-type:jwt" + auth0 token-exchange update -n "Updated Profile Name" -s "urn:ietf:params:oauth:token-type:jwt" --json + auth0 token-exchange update -n "Updated Profile Name" -s "urn:ietf:params:oauth:token-type:jwt" --json-compact`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + inputs.ID = args[0] + } else { + if err := tokenExchangeProfileID.Pick(cmd, &inputs.ID, cli.tokenExchangeProfilePickerOptions); err != nil { + return err + } + } + + var current *management.TokenExchangeProfile + if err := ansi.Waiting(func() (err error) { + current, err = cli.api.TokenExchange.Read(cmd.Context(), inputs.ID) + return err + }); err != nil { + return fmt.Errorf("failed to read token exchange profile with ID %q: %w", inputs.ID, err) + } + + if err := tokenExchangeProfileName.AskU(cmd, &inputs.Name, current.Name); err != nil { + return err + } + + if err := tokenExchangeProfileSubjectTokenType.AskU(cmd, &inputs.SubjectTokenType, current.SubjectTokenType); err != nil { + return err + } + + updatedProfile := &management.TokenExchangeProfile{} + + if inputs.Name != "" { + updatedProfile.Name = &inputs.Name + } + + if inputs.SubjectTokenType != "" { + updatedProfile.SubjectTokenType = &inputs.SubjectTokenType + } + + if err := ansi.Waiting(func() error { + return cli.api.TokenExchange.Update(cmd.Context(), inputs.ID, updatedProfile) + }); err != nil { + return fmt.Errorf("failed to update token exchange profile with ID %q: %w", inputs.ID, err) + } + + cli.renderer.TokenExchangeProfileUpdate(updatedProfile) + + return nil + }, + } + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + tokenExchangeProfileName.RegisterStringU(cmd, &inputs.Name, "") + tokenExchangeProfileSubjectTokenType.RegisterStringU(cmd, &inputs.SubjectTokenType, "") + + return cmd +} + +func deleteTokenExchangeProfileCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Aliases: []string{"rm"}, + Short: "Delete a token exchange profile", + Long: "Delete a token exchange profile.\n\n" + + "To delete interactively, use `auth0 token-exchange delete` with no arguments.\n\n" + + "To delete non-interactively, supply the profile id and the `--force` flag to skip confirmation.", + Example: ` auth0 token-exchange delete + auth0 token-exchange rm + auth0 token-exchange delete + auth0 token-exchange delete --force + auth0 token-exchange delete + auth0 token-exchange delete --force`, + RunE: func(cmd *cobra.Command, args []string) error { + ids := make([]string, len(args)) + if len(args) == 0 { + if err := tokenExchangeProfileID.PickMany(cmd, &ids, cli.tokenExchangeProfilePickerOptions); err != nil { + return err + } + } else { + ids = append(ids, args...) + } + + if !cli.force && canPrompt(cmd) { + if confirmed := prompt.Confirm("Are you sure you want to proceed?"); !confirmed { + return nil + } + } + + return ansi.ProgressBar("Deleting token exchange profile(s)", ids, func(i int, id string) error { + if id != "" { + if err := cli.api.TokenExchange.Delete(cmd.Context(), id); err != nil { + return fmt.Errorf("failed to delete token exchange profile with ID %q: %w", id, err) + } + } + return nil + }) + }, + } + + cmd.Flags().BoolVar(&cli.force, "force", false, "Skip confirmation.") + + return cmd +} + +func (c *cli) tokenExchangeProfilePickerOptions(ctx context.Context) (pickerOptions, error) { + list, err := c.api.TokenExchange.List(ctx) + if err != nil { + return nil, err + } + + var opts pickerOptions + for _, p := range list.TokenExchangeProfiles { + label := fmt.Sprintf("%s %s", p.GetName(), ansi.Faint("("+p.GetID()+")")) + opts = append(opts, pickerOption{value: p.GetID(), label: label}) + } + + if len(opts) == 0 { + return nil, errors.New("there are currently no token exchange profiles to choose from. Create one by running: `auth0 token-exchange create`") + } + + return opts, nil +} + +// customTokenExchangeActionPickerOptions returns actions filtered by custom-token-exchange trigger. +func (c *cli) customTokenExchangeActionPickerOptions(ctx context.Context) (pickerOptions, error) { + list, err := c.api.Action.List( + ctx, + management.Parameter("triggerId", "custom-token-exchange"), + ) + if err != nil { + return nil, err + } + + var opts pickerOptions + for _, action := range list.Actions { + value := action.GetID() + label := fmt.Sprintf("%s %s", action.GetName(), ansi.Faint("("+value+")")) + opts = append(opts, pickerOption{value: value, label: label}) + } + + if len(opts) == 0 { + return nil, errors.New("no actions found with trigger type 'custom-token-exchange'. Please create an action with this trigger first") + } + + return opts, nil +} diff --git a/internal/display/token_exchange.go b/internal/display/token_exchange.go new file mode 100644 index 000000000..088a1fe62 --- /dev/null +++ b/internal/display/token_exchange.go @@ -0,0 +1,102 @@ +package display + +import ( + "github.com/auth0/go-auth0/management" + + "github.com/auth0/auth0-cli/internal/ansi" +) + +type tokenExchangeProfileView struct { + ID string + Name string + SubjectTokenType string + ActionID string + Type string + CreatedAt string + UpdatedAt string + raw interface{} +} + +func (v *tokenExchangeProfileView) AsTableHeader() []string { + return []string{"ID", "Name", "Type", "Subject Token Type", "Action ID"} +} + +func (v *tokenExchangeProfileView) AsTableRow() []string { + return []string{ + ansi.Faint(v.ID), + v.Name, + v.Type, + v.SubjectTokenType, + ansi.Faint(v.ActionID), + } +} + +func (v *tokenExchangeProfileView) KeyValues() [][]string { + return [][]string{ + {"ID", ansi.Faint(v.ID)}, + {"NAME", v.Name}, + {"TYPE", v.Type}, + {"SUBJECT TOKEN TYPE", v.SubjectTokenType}, + {"ACTION ID", ansi.Faint(v.ActionID)}, + {"CREATED AT", v.CreatedAt}, + {"UPDATED AT", v.UpdatedAt}, + } +} + +func (v *tokenExchangeProfileView) Object() interface{} { + return v.raw +} + +func (r *Renderer) TokenExchangeProfileList(profiles []*management.TokenExchangeProfile) { + resource := "token exchange profiles" + + r.Heading(resource) + + if len(profiles) == 0 { + r.EmptyState(resource, "Use 'auth0 token-exchange create' to add one") + return + } + + var res []View + for _, p := range profiles { + res = append(res, makeTokenExchangeProfileView(p)) + } + + r.Results(res) +} + +func (r *Renderer) TokenExchangeProfileShow(profile *management.TokenExchangeProfile) { + r.Heading("token exchange profile") + r.Result(makeTokenExchangeProfileView(profile)) +} + +func (r *Renderer) TokenExchangeProfileCreate(profile *management.TokenExchangeProfile) { + r.Heading("token exchange profile created") + r.Result(makeTokenExchangeProfileView(profile)) +} + +func (r *Renderer) TokenExchangeProfileUpdate(profile *management.TokenExchangeProfile) { + r.Heading("token exchange profile updated") + r.Result(makeTokenExchangeProfileView(profile)) +} + +func makeTokenExchangeProfileView(profile *management.TokenExchangeProfile) *tokenExchangeProfileView { + view := &tokenExchangeProfileView{ + ID: profile.GetID(), + Name: profile.GetName(), + SubjectTokenType: profile.GetSubjectTokenType(), + ActionID: profile.GetActionID(), + Type: profile.GetType(), + raw: profile, + } + + if profile.CreatedAt != nil { + view.CreatedAt = timeAgo(profile.GetCreatedAt()) + } + + if profile.UpdatedAt != nil { + view.UpdatedAt = timeAgo(profile.GetUpdatedAt()) + } + + return view +} diff --git a/test/integration/scripts/test-cleanup.sh b/test/integration/scripts/test-cleanup.sh index 2fda11504..292e50aa7 100755 --- a/test/integration/scripts/test-cleanup.sh +++ b/test/integration/scripts/test-cleanup.sh @@ -29,6 +29,7 @@ delete_resources "roles" "integration-test-role" "id" delete_resources "rules" "integration-test-rule" "id" delete_resources "orgs" "integration-test-org" "id" delete_resources "actions" "integration-test-" "id" +delete_resources "token-exchange" "integration-test-" "id" delete_resources "event-streams" "integration-test-" "id" delete_resources "logs streams" "integration-test-" "id"