From 393a122a4018c47b378f329437ed6c0c5fd424ad Mon Sep 17 00:00:00 2001 From: KIRAN KUMAR B Date: Tue, 14 Jul 2026 16:30:08 +0530 Subject: [PATCH] feat(apps): add session-transfer delegation flags - Add --delegation-allow-delegated-access and --delegation-enforce-device-binding flags to `apps session-transfer update`, surfacing the Early Access Custom Token Exchange impersonation settings (client.session_transfer.delegation). - Send delegation only when a flag is set, so updates to legacy session-transfer settings leave it untouched for clients not using the feature. - Validate --delegation-enforce-device-binding as 'ip' or 'asn' before any API call, matching the Management API enum (no 'none', unlike the parent field). - Render delegation in `session-transfer show`/`update` output only when the client has it configured, keeping output unchanged for existing clients. - Add display unit tests plus an integration case for the validation error, and regenerate the command docs. --- docs/auth0_apps_session-transfer_update.md | 13 ++-- internal/cli/apps.go | 51 ++++++++++++-- internal/display/apps_session_transfer.go | 26 ++++++- .../display/apps_session_transfer_test.go | 67 +++++++++++++++++++ test/integration/apps-test-cases.yaml | 36 ++++++---- 5 files changed, 169 insertions(+), 24 deletions(-) create mode 100644 internal/display/apps_session_transfer_test.go diff --git a/docs/auth0_apps_session-transfer_update.md b/docs/auth0_apps_session-transfer_update.md index 5081e575d..5db6ef76e 100644 --- a/docs/auth0_apps_session-transfer_update.md +++ b/docs/auth0_apps_session-transfer_update.md @@ -18,6 +18,7 @@ auth0 apps session-transfer update [flags] auth0 apps session-transfer update auth0 apps session-transfer update auth0 apps session-transfer update --can-create-token --json + auth0 apps session-transfer update --delegation-allow-delegated-access=true --delegation-enforce-device-binding=asn auth0 apps session-transfer update --can-create-token=true --allowed-auth-methods=cookie,query --enforce-device-binding=ip ``` @@ -25,11 +26,13 @@ auth0 apps session-transfer update [flags] ## Flags ``` - -m, --allowed-auth-methods strings Comma-separated list of authentication methods (e.g., cookie, query). - -t, --can-create-token Allow creation of session transfer tokens. - -e, --enforce-device-binding string Device binding enforcement: 'none', 'ip', or 'asn'. - --json Output in json format. - --json-compact Output in compact json format. + -m, --allowed-auth-methods strings Comma-separated list of authentication methods (e.g., cookie, query). + -t, --can-create-token Allow creation of session transfer tokens. + --delegation-allow-delegated-access (Early Access) Allow the application to accept Session Transfer Tokens containing an Actor, enabling delegated (impersonation) access. Defaults to false. + --delegation-enforce-device-binding string (Early Access) Device binding enforcement for delegated (impersonation) access: 'ip' or 'asn'. Defaults to 'ip'. + -e, --enforce-device-binding string Device binding enforcement: 'none', 'ip', or 'asn'. + --json Output in json format. + --json-compact Output in compact json format. ``` diff --git a/internal/cli/apps.go b/internal/cli/apps.go index 82481ac8d..f170853bd 100644 --- a/internal/cli/apps.go +++ b/internal/cli/apps.go @@ -164,6 +164,18 @@ var ( Help: "Device binding enforcement: 'none', 'ip', or 'asn'.", AlwaysPrompt: true, } + appSTDelegationAllowAccess = Flag{ + Name: "Allow Delegated Access", + LongForm: "delegation-allow-delegated-access", + Help: "(Early Access) Allow the application to accept Session Transfer Tokens containing an Actor, " + + "enabling delegated (impersonation) access. Defaults to false.", + } + appSTDelegationDeviceBinding = Flag{ + Name: "Delegation Enforce Device Binding", + LongForm: "delegation-enforce-device-binding", + Help: "(Early Access) Device binding enforcement for delegated (impersonation) access: 'ip' or 'asn'. " + + "Defaults to 'ip'.", + } refreshToken = Flag{ Name: "Refresh Token", LongForm: "refresh-token", @@ -1211,10 +1223,12 @@ func appsSessionTransferShowCmd(cli *cli) *cobra.Command { func appsSessionTransferUpdateCmd(cli *cli) *cobra.Command { var inputs struct { - ID string - CanCreateToken bool - AllowedAuthMethods []string - EnforceDeviceBinding string + ID string + CanCreateToken bool + AllowedAuthMethods []string + EnforceDeviceBinding string + DelegationAllowAccess bool + DelegationDeviceBinding string } cmd := &cobra.Command{ @@ -1224,6 +1238,7 @@ func appsSessionTransferUpdateCmd(cli *cli) *cobra.Command { Example: ` auth0 apps session-transfer update auth0 apps session-transfer update auth0 apps session-transfer update --can-create-token --json + auth0 apps session-transfer update --delegation-allow-delegated-access=true --delegation-enforce-device-binding=asn auth0 apps session-transfer update --can-create-token=true --allowed-auth-methods=cookie,query --enforce-device-binding=ip`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { @@ -1235,6 +1250,16 @@ func appsSessionTransferUpdateCmd(cli *cli) *cobra.Command { inputs.ID = args[0] } + // Validate the delegation device binding before any API call. + if appSTDelegationDeviceBinding.IsSet(cmd) && + inputs.DelegationDeviceBinding != "ip" && inputs.DelegationDeviceBinding != "asn" { + return fmt.Errorf( + "invalid value %q for --%s: must be one of 'ip' or 'asn'", + inputs.DelegationDeviceBinding, + appSTDelegationDeviceBinding.LongForm, + ) + } + var ( current *management.Client st management.SessionTransfer @@ -1283,6 +1308,22 @@ func appsSessionTransferUpdateCmd(cli *cli) *cobra.Command { st.EnforceDeviceBinding = current.SessionTransfer.EnforceDeviceBinding } + // Delegation (EA) is sent only when a flag is set, leaving it untouched for + // others. The API merges sub-fields, so sending just the changed one is enough. + if appSTDelegationAllowAccess.IsSet(cmd) || appSTDelegationDeviceBinding.IsSet(cmd) { + delegation := &management.SessionTransferDelegation{} + + if appSTDelegationAllowAccess.IsSet(cmd) { + delegation.AllowDelegatedAccess = &inputs.DelegationAllowAccess + } + + if appSTDelegationDeviceBinding.IsSet(cmd) { + delegation.EnforceDeviceBinding = &inputs.DelegationDeviceBinding + } + + st.Delegation = delegation + } + // Send update request. clientST := &management.Client{SessionTransfer: &st} if err := ansi.Waiting(func() error { @@ -1302,6 +1343,8 @@ func appsSessionTransferUpdateCmd(cli *cli) *cobra.Command { appSTCanCreateToken.RegisterBoolU(cmd, &inputs.CanCreateToken, false) appSTAllowedAuthMethods.RegisterStringSliceU(cmd, &inputs.AllowedAuthMethods, nil) appSTEnforceDeviceBinding.RegisterStringU(cmd, &inputs.EnforceDeviceBinding, "") + appSTDelegationAllowAccess.RegisterBoolU(cmd, &inputs.DelegationAllowAccess, false) + appSTDelegationDeviceBinding.RegisterStringU(cmd, &inputs.DelegationDeviceBinding, "") return cmd } diff --git a/internal/display/apps_session_transfer.go b/internal/display/apps_session_transfer.go index b5a9bd08b..7534034dd 100644 --- a/internal/display/apps_session_transfer.go +++ b/internal/display/apps_session_transfer.go @@ -12,6 +12,11 @@ type SessionTransferView struct { AllowedMethods string DeviceBinding string + // Delegation (EA) fields, shown only when hasDelegation is true. + hasDelegation bool + DelegationAllowAccess string + DelegationDeviceBinding string + raw interface{} } @@ -29,12 +34,21 @@ func (v *SessionTransferView) AsTableRow() []string { } func (v *SessionTransferView) KeyValues() [][]string { - return [][]string{ + keyValues := [][]string{ {"CLIENT ID", v.ID}, {"CAN CREATE TOKEN", v.CanCreateTOKEN}, {"ALLOWED METHODS", v.AllowedMethods}, {"DEVICE BINDING", v.DeviceBinding}, } + + if v.hasDelegation { + keyValues = append(keyValues, + []string{"ALLOW DELEGATED ACCESS", v.DelegationAllowAccess}, + []string{"DELEGATION DEVICE BINDING", v.DelegationDeviceBinding}, + ) + } + + return keyValues } func (v *SessionTransferView) Object() interface{} { @@ -54,11 +68,19 @@ func (r *Renderer) SessionTransferUpdate(client *management.Client, id string) { } func MakeSessionTransferView(client *management.Client) *SessionTransferView { - return &SessionTransferView{ + view := &SessionTransferView{ ID: client.GetClientID(), CanCreateTOKEN: boolean(client.SessionTransfer.GetCanCreateSessionTransferToken()), AllowedMethods: stringSliceToCommaSeparatedString(client.SessionTransfer.GetAllowedAuthenticationMethods()), DeviceBinding: client.SessionTransfer.GetEnforceDeviceBinding(), raw: client.SessionTransfer, } + + if delegation := client.GetSessionTransfer().GetDelegation(); delegation != nil { + view.hasDelegation = true + view.DelegationAllowAccess = boolean(delegation.GetAllowDelegatedAccess()) + view.DelegationDeviceBinding = delegation.GetEnforceDeviceBinding() + } + + return view } diff --git a/internal/display/apps_session_transfer_test.go b/internal/display/apps_session_transfer_test.go new file mode 100644 index 000000000..5cfecfabc --- /dev/null +++ b/internal/display/apps_session_transfer_test.go @@ -0,0 +1,67 @@ +package display + +import ( + "testing" + + "github.com/auth0/go-auth0" + "github.com/auth0/go-auth0/management" + "github.com/stretchr/testify/assert" +) + +func TestMakeSessionTransferView_WithoutDelegation(t *testing.T) { + client := &management.Client{ + ClientID: auth0.String("client-id"), + SessionTransfer: &management.SessionTransfer{ + CanCreateSessionTransferToken: auth0.Bool(true), + AllowedAuthenticationMethods: &[]string{"cookie", "query"}, + EnforceDeviceBinding: auth0.String("ip"), + }, + } + + view := MakeSessionTransferView(client) + + assert.False(t, view.hasDelegation) + assert.Equal(t, "", view.DelegationAllowAccess) + assert.Equal(t, "", view.DelegationDeviceBinding) + + // Delegation rows must be omitted when no delegation is configured. + keyValues := view.KeyValues() + assert.Equal(t, [][]string{ + {"CLIENT ID", "client-id"}, + {"CAN CREATE TOKEN", boolean(true)}, + {"ALLOWED METHODS", "cookie, query"}, + {"DEVICE BINDING", "ip"}, + }, keyValues) +} + +func TestMakeSessionTransferView_WithDelegation(t *testing.T) { + client := &management.Client{ + ClientID: auth0.String("client-id"), + SessionTransfer: &management.SessionTransfer{ + CanCreateSessionTransferToken: auth0.Bool(true), + AllowedAuthenticationMethods: &[]string{"cookie"}, + EnforceDeviceBinding: auth0.String("ip"), + Delegation: &management.SessionTransferDelegation{ + AllowDelegatedAccess: auth0.Bool(true), + EnforceDeviceBinding: auth0.String("asn"), + }, + }, + } + + view := MakeSessionTransferView(client) + + assert.True(t, view.hasDelegation) + assert.Equal(t, boolean(true), view.DelegationAllowAccess) + assert.Equal(t, "asn", view.DelegationDeviceBinding) + + // Delegation rows must be appended after the base session-transfer rows. + keyValues := view.KeyValues() + assert.Equal(t, [][]string{ + {"CLIENT ID", "client-id"}, + {"CAN CREATE TOKEN", boolean(true)}, + {"ALLOWED METHODS", "cookie"}, + {"DEVICE BINDING", "ip"}, + {"ALLOW DELEGATED ACCESS", boolean(true)}, + {"DELEGATION DEVICE BINDING", "asn"}, + }, keyValues) +} diff --git a/test/integration/apps-test-cases.yaml b/test/integration/apps-test-cases.yaml index ebad6713e..59fc6ee42 100644 --- a/test/integration/apps-test-cases.yaml +++ b/test/integration/apps-test-cases.yaml @@ -318,7 +318,17 @@ tests: - ALLOWED METHODS cookie, query - DEVICE BINDING ip - 042 - it successfully creates a m2m app with allow-any-profile-of-type and outputs in json: + # Delegation is an Early Access sub-feature gated behind the + # `cte_session_transfer_token` tenant flag. So this case only covers the client-side validation, which + # runs before any API call and is therefore safe on any tenant. + 042 - it rejects an invalid delegation device binding value: + command: auth0 apps session-transfer update $(./test/integration/scripts/get-app-id.sh) --delegation-enforce-device-binding=none --json + exit-code: 1 + stderr: + contains: + - "must be one of 'ip' or 'asn'" + + 043 - it successfully creates a m2m app with allow-any-profile-of-type and outputs in json: command: auth0 apps create --name integration-test-app-te1 --type m2m --description TEApp1 --allow-any-profile-of-type custom_authentication --json exit-code: 0 stdout: @@ -327,29 +337,29 @@ tests: app_type: non_interactive token_exchange.allow_any_profile_of_type: "[custom_authentication]" - 043 - it successfully updates token exchange types on an app and outputs in json: + 044 - it successfully updates token exchange types on an app and outputs in json: command: auth0 apps update $(./test/integration/scripts/get-app-id.sh) --allow-any-profile-of-type custom_authentication --json exit-code: 0 stdout: json: token_exchange.allow_any_profile_of_type: "[custom_authentication]" - 044 - it successfully shows token exchange types on an app: + 045 - it successfully shows token exchange types on an app: command: auth0 apps show $(./test/integration/scripts/get-app-id.sh) exit-code: 0 stdout: contains: - TOKEN EXCHANGE TYPES custom_authentication - 045 - given a test app, it successfully deletes the app: + 046 - given a test app, it successfully deletes the app: command: auth0 apps delete $(./test/integration/scripts/get-app-id.sh) --force exit-code: 0 - 046 - it successfully creates a resource server app with resource-server-identifier: + 047 - it successfully creates a resource server app with resource-server-identifier: command: ./test/integration/scripts/get-resource-server-app-id.sh exit-code: 0 - 047 - it successfully creates a resource server app with resource-server-identifier and outputs in json: + 048 - it successfully creates a resource server app with resource-server-identifier and outputs in json: command: auth0 apps show $(./test/integration/scripts/get-resource-server-app-id.sh) --json exit-code: 0 stdout: @@ -358,7 +368,7 @@ tests: app_type: resource_server resource_server_identifier: http://integration-test-api-newapi - 048 - it shows resource server app with resource-server-identifier: + 049 - it shows resource server app with resource-server-identifier: command: auth0 apps show $(./test/integration/scripts/get-resource-server-app-id.sh) exit-code: 0 stdout: @@ -366,22 +376,22 @@ tests: - TYPE Resource Server - RESOURCE SERVER IDENTIFIER http://integration-test-api-newapi - 049 - it fails to update resource-server-identifier (immutable property): + 050 - it fails to update resource-server-identifier (immutable property): command: auth0 apps update $(./test/integration/scripts/get-resource-server-app-id.sh) --resource-server-identifier http://new-api.localhost --json exit-code: 1 stderr: contains: - "Unknown flag: --resource-server-identifier" - 050 - given a resource server app, it successfully deletes the app: + 051 - given a resource server app, it successfully deletes the app: command: auth0 apps delete $(./test/integration/scripts/get-resource-server-app-id.sh) --force exit-code: 0 - 051 - it successfully creates a third-party app with strict security mode: + 052 - it successfully creates a third-party app with strict security mode: command: ./test/integration/scripts/get-3p-app-id.sh exit-code: 0 - 052 - it successfully shows a third-party app with security mode and redirection policy in json: + 053 - it successfully shows a third-party app with security mode and redirection policy in json: command: auth0 apps show $(./test/integration/scripts/get-3p-app-id.sh) --json exit-code: 0 stdout: @@ -392,13 +402,13 @@ tests: third_party_security_mode: strict redirection_policy: open_redirect_protection - 053 - it successfully updates the redirection-policy of a third-party app and outputs in json: + 054 - it successfully updates the redirection-policy of a third-party app and outputs in json: command: auth0 apps update $(./test/integration/scripts/get-3p-app-id.sh) --redirection-policy allow_always --json exit-code: 0 stdout: json: redirection_policy: allow_always - 054 - given a third-party app, it successfully deletes the app: + 055 - given a third-party app, it successfully deletes the app: command: auth0 apps delete $(./test/integration/scripts/get-3p-app-id.sh) --force exit-code: 0