From 8e1b67a183f2914d018d4c97d5237b47fc2cbe18 Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Mon, 28 Sep 2026 09:58:40 +0530 Subject: [PATCH 1/3] feat: add organization flags to auth0 apps create and update Add --organization-usage, --organization-require-behavior, and --organization-discovery-methods to auth0 apps create and update, so an application's organization behavior can be configured without dropping to the raw api command. The flags are additive and follow the existing optional-flag pattern: create sets each when provided, update only sends a field when its flag is set so unset flags never clobber existing values. --- docs/auth0_apps_create.md | 54 +++++++++++++++++++++------------------ docs/auth0_apps_update.md | 52 ++++++++++++++++++++----------------- internal/cli/apps.go | 52 +++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 49 deletions(-) diff --git a/docs/auth0_apps_create.md b/docs/auth0_apps_create.md index 285f3efd4..80daf14f2 100644 --- a/docs/auth0_apps_create.md +++ b/docs/auth0_apps_create.md @@ -35,6 +35,7 @@ auth0 apps create [flags] auth0 apps create --name "My API Client" --type resource_server --resource-server-identifier "https://api.example.com" auth0 apps create --name myapp --type resource_server --allow-any-profile-of-type custom_authentication,on_behalf_of_token_exchange auth0 apps create --name "My 3P App" --type regular --is-first-party=false --third-party-security-mode strict --redirection-policy open_redirect_protection + auth0 apps create --name myapp --type regular --organization-usage require --organization-require-behavior pre_login_prompt --organization-discovery-methods email,organization_name # Discover the payload schema auth0 apps create --schema @@ -50,31 +51,34 @@ auth0 apps create [flags] ## Flags ``` - -p, --allow-any-profile-of-type strings Comma-separated list of enabled token exchange types for this client. Possible values: custom_authentication, on_behalf_of_token_exchange. - -a, --auth-method string Defines the requested authentication method for the token endpoint. Possible values are 'None' (public application without a client secret), 'Post' (application uses HTTP POST parameters) or 'Basic' (application uses HTTP Basic). - -c, --callbacks strings After the user authenticates we will only call back to any of these URLs. You can specify multiple valid URLs by comma-separating them (typically to handle different environments like QA or testing). Make sure to specify the protocol (https://) otherwise the callback may fail in some cases. With the exception of custom URI schemes for native apps, all callbacks should use protocol https://. - --data string JSON payload for the operation, as a JSON string or file path (@file.json). Can also be piped via stdin. - -d, --description string Description of the application. Max character count is 140. - -g, --grants strings List of grant types supported for this application. Can include code, implicit, refresh-token, credentials, password, password-realm, mfa-oob, mfa-otp, mfa-recovery-code, and device-code. - -f, --is-first-party Whether the application is a first-party client (true) or third-party client (false). (default true) - --json Output in json format. - --json-compact Output in compact json format. - -l, --logout-urls strings Comma-separated list of URLs that are valid to redirect to after logout from Auth0. Wildcards are allowed for subdomains. - --metadata stringToString Arbitrary keys-value pairs (max 255 characters each), that can be assigned to each application. More about application metadata: https://auth0.com/docs/get-started/applications/configure-application-metadata (default []) - -n, --name string Name of the application. - -o, --origins strings Comma-separated list of URLs allowed to make requests from JavaScript to Auth0 API (typically used with CORS). By default, all your callback URLs will be allowed. This field allows you to enter other origins if necessary. You can also use wildcards at the subdomain level (e.g., https://*.contoso.com). Query strings and hash information are not taken into account when validating these URLs. - -y, --redirection-policy string Controls whether Auth0 redirects users to the application's callback URL on authentication errors or in email verification flows: 'allow_always' or 'open_redirect_protection'. Require --is-first-party=false - -z, --refresh-token string Refresh Token Config for the application, formatted as JSON. - --resource-server-identifier string The identifier of the resource server that this client is associated with. This property can only be sent when app_type=resource_server and cannot be changed once the client is created. - -r, --reveal-secrets Display the application secrets ('signing_keys', 'client_secret') as part of the command output. - --schema Print the request payload schema for this command and exit. Use with --json or --json-compact for machine-readable output. - -s, --third-party-security-mode string Security mode for third-party clients: 'strict' or 'permissive'. Require --is-first-party=false - -t, --type string Type of application: - - native: mobile, desktop, CLI and smart device apps running natively. - - spa (single page application): a JavaScript front-end app that uses an API. - - regular: Traditional web app using redirects. - - m2m (machine to machine): CLIs, daemons or services running on your backend. - -w, --web-origins strings Comma-separated list of allowed origins for use with Cross-Origin Authentication, Device Flow, and web message response mode. + -p, --allow-any-profile-of-type strings Comma-separated list of enabled token exchange types for this client. Possible values: custom_authentication, on_behalf_of_token_exchange. + -a, --auth-method string Defines the requested authentication method for the token endpoint. Possible values are 'None' (public application without a client secret), 'Post' (application uses HTTP POST parameters) or 'Basic' (application uses HTTP Basic). + -c, --callbacks strings After the user authenticates we will only call back to any of these URLs. You can specify multiple valid URLs by comma-separating them (typically to handle different environments like QA or testing). Make sure to specify the protocol (https://) otherwise the callback may fail in some cases. With the exception of custom URI schemes for native apps, all callbacks should use protocol https://. + --data string JSON payload for the operation, as a JSON string or file path (@file.json). Can also be piped via stdin. + -d, --description string Description of the application. Max character count is 140. + -g, --grants strings List of grant types supported for this application. Can include code, implicit, refresh-token, credentials, password, password-realm, mfa-oob, mfa-otp, mfa-recovery-code, and device-code. + -f, --is-first-party Whether the application is a first-party client (true) or third-party client (false). (default true) + --json Output in json format. + --json-compact Output in compact json format. + -l, --logout-urls strings Comma-separated list of URLs that are valid to redirect to after logout from Auth0. Wildcards are allowed for subdomains. + --metadata stringToString Arbitrary keys-value pairs (max 255 characters each), that can be assigned to each application. More about application metadata: https://auth0.com/docs/get-started/applications/configure-application-metadata (default []) + -n, --name string Name of the application. + --organization-discovery-methods strings Comma-separated list of methods for discovering organizations during the 'pre_login_prompt'. Possible values: 'email', 'organization_name'. Requires --organization-require-behavior=pre_login_prompt. + --organization-require-behavior string How to prompt for an organization when --organization-usage is 'require': 'no_prompt', 'pre_login_prompt', or 'post_login_prompt'. + --organization-usage string How the application handles organizations at authentication: 'deny', 'allow', or 'require'. + -o, --origins strings Comma-separated list of URLs allowed to make requests from JavaScript to Auth0 API (typically used with CORS). By default, all your callback URLs will be allowed. This field allows you to enter other origins if necessary. You can also use wildcards at the subdomain level (e.g., https://*.contoso.com). Query strings and hash information are not taken into account when validating these URLs. + -y, --redirection-policy string Controls whether Auth0 redirects users to the application's callback URL on authentication errors or in email verification flows: 'allow_always' or 'open_redirect_protection'. Require --is-first-party=false + -z, --refresh-token string Refresh Token Config for the application, formatted as JSON. + --resource-server-identifier string The identifier of the resource server that this client is associated with. This property can only be sent when app_type=resource_server and cannot be changed once the client is created. + -r, --reveal-secrets Display the application secrets ('signing_keys', 'client_secret') as part of the command output. + --schema Print the request payload schema for this command and exit. Use with --json or --json-compact for machine-readable output. + -s, --third-party-security-mode string Security mode for third-party clients: 'strict' or 'permissive'. Require --is-first-party=false + -t, --type string Type of application: + - native: mobile, desktop, CLI and smart device apps running natively. + - spa (single page application): a JavaScript front-end app that uses an API. + - regular: Traditional web app using redirects. + - m2m (machine to machine): CLIs, daemons or services running on your backend. + -w, --web-origins strings Comma-separated list of allowed origins for use with Cross-Origin Authentication, Device Flow, and web message response mode. ``` diff --git a/docs/auth0_apps_update.md b/docs/auth0_apps_update.md index d538ccb8b..d4ae7b611 100644 --- a/docs/auth0_apps_update.md +++ b/docs/auth0_apps_update.md @@ -34,6 +34,7 @@ auth0 apps update [flags] auth0 apps update -n myapp -d -t [native|spa|regular|m2m] -r --json --metadata "foo=bar,bazz=buzz" auth0 apps update --allow-any-profile-of-type custom_authentication,on_behalf_of_token_exchange auth0 apps update --redirection-policy allow_always + auth0 apps update --organization-usage require --organization-require-behavior pre_login_prompt # Discover the payload schema auth0 apps update --schema @@ -49,30 +50,33 @@ auth0 apps update [flags] ## Flags ``` - -p, --allow-any-profile-of-type strings Comma-separated list of enabled token exchange types for this client. Possible values: custom_authentication, on_behalf_of_token_exchange. - -a, --auth-method string Defines the requested authentication method for the token endpoint. Possible values are 'None' (public application without a client secret), 'Post' (application uses HTTP POST parameters) or 'Basic' (application uses HTTP Basic). - -c, --callbacks strings After the user authenticates we will only call back to any of these URLs. You can specify multiple valid URLs by comma-separating them (typically to handle different environments like QA or testing). Make sure to specify the protocol (https://) otherwise the callback may fail in some cases. With the exception of custom URI schemes for native apps, all callbacks should use protocol https://. - --data string JSON payload for the operation, as a JSON string or file path (@file.json). Can also be piped via stdin. - -d, --description string Description of the application. Max character count is 140. - -g, --grants strings List of grant types supported for this application. Can include code, implicit, refresh-token, credentials, password, password-realm, mfa-oob, mfa-otp, mfa-recovery-code, and device-code. - -f, --is-first-party Whether the application is a first-party client (true) or third-party client (false). (default true) - --json Output in json format. - --json-compact Output in compact json format. - -l, --logout-urls strings Comma-separated list of URLs that are valid to redirect to after logout from Auth0. Wildcards are allowed for subdomains. - --metadata stringToString Arbitrary keys-value pairs (max 255 characters each), that can be assigned to each application. More about application metadata: https://auth0.com/docs/get-started/applications/configure-application-metadata (default []) - -n, --name string Name of the application. - -o, --origins strings Comma-separated list of URLs allowed to make requests from JavaScript to Auth0 API (typically used with CORS). By default, all your callback URLs will be allowed. This field allows you to enter other origins if necessary. You can also use wildcards at the subdomain level (e.g., https://*.contoso.com). Query strings and hash information are not taken into account when validating these URLs. - -y, --redirection-policy string Controls whether Auth0 redirects users to the application's callback URL on authentication errors or in email verification flows: 'allow_always' or 'open_redirect_protection'. Require --is-first-party=false - -z, --refresh-token string Refresh Token Config for the application, formatted as JSON. - -r, --reveal-secrets Display the application secrets ('signing_keys', 'client_secret') as part of the command output. - --schema Print the request payload schema for this command and exit. Use with --json or --json-compact for machine-readable output. - -s, --third-party-security-mode string Security mode for third-party clients: 'strict' or 'permissive'. Require --is-first-party=false - -t, --type string Type of application: - - native: mobile, desktop, CLI and smart device apps running natively. - - spa (single page application): a JavaScript front-end app that uses an API. - - regular: Traditional web app using redirects. - - m2m (machine to machine): CLIs, daemons or services running on your backend. - -w, --web-origins strings Comma-separated list of allowed origins for use with Cross-Origin Authentication, Device Flow, and web message response mode. + -p, --allow-any-profile-of-type strings Comma-separated list of enabled token exchange types for this client. Possible values: custom_authentication, on_behalf_of_token_exchange. + -a, --auth-method string Defines the requested authentication method for the token endpoint. Possible values are 'None' (public application without a client secret), 'Post' (application uses HTTP POST parameters) or 'Basic' (application uses HTTP Basic). + -c, --callbacks strings After the user authenticates we will only call back to any of these URLs. You can specify multiple valid URLs by comma-separating them (typically to handle different environments like QA or testing). Make sure to specify the protocol (https://) otherwise the callback may fail in some cases. With the exception of custom URI schemes for native apps, all callbacks should use protocol https://. + --data string JSON payload for the operation, as a JSON string or file path (@file.json). Can also be piped via stdin. + -d, --description string Description of the application. Max character count is 140. + -g, --grants strings List of grant types supported for this application. Can include code, implicit, refresh-token, credentials, password, password-realm, mfa-oob, mfa-otp, mfa-recovery-code, and device-code. + -f, --is-first-party Whether the application is a first-party client (true) or third-party client (false). (default true) + --json Output in json format. + --json-compact Output in compact json format. + -l, --logout-urls strings Comma-separated list of URLs that are valid to redirect to after logout from Auth0. Wildcards are allowed for subdomains. + --metadata stringToString Arbitrary keys-value pairs (max 255 characters each), that can be assigned to each application. More about application metadata: https://auth0.com/docs/get-started/applications/configure-application-metadata (default []) + -n, --name string Name of the application. + --organization-discovery-methods strings Comma-separated list of methods for discovering organizations during the 'pre_login_prompt'. Possible values: 'email', 'organization_name'. Requires --organization-require-behavior=pre_login_prompt. + --organization-require-behavior string How to prompt for an organization when --organization-usage is 'require': 'no_prompt', 'pre_login_prompt', or 'post_login_prompt'. + --organization-usage string How the application handles organizations at authentication: 'deny', 'allow', or 'require'. + -o, --origins strings Comma-separated list of URLs allowed to make requests from JavaScript to Auth0 API (typically used with CORS). By default, all your callback URLs will be allowed. This field allows you to enter other origins if necessary. You can also use wildcards at the subdomain level (e.g., https://*.contoso.com). Query strings and hash information are not taken into account when validating these URLs. + -y, --redirection-policy string Controls whether Auth0 redirects users to the application's callback URL on authentication errors or in email verification flows: 'allow_always' or 'open_redirect_protection'. Require --is-first-party=false + -z, --refresh-token string Refresh Token Config for the application, formatted as JSON. + -r, --reveal-secrets Display the application secrets ('signing_keys', 'client_secret') as part of the command output. + --schema Print the request payload schema for this command and exit. Use with --json or --json-compact for machine-readable output. + -s, --third-party-security-mode string Security mode for third-party clients: 'strict' or 'permissive'. Require --is-first-party=false + -t, --type string Type of application: + - native: mobile, desktop, CLI and smart device apps running natively. + - spa (single page application): a JavaScript front-end app that uses an API. + - regular: Traditional web app using redirects. + - m2m (machine to machine): CLIs, daemons or services running on your backend. + -w, --web-origins strings Comma-separated list of allowed origins for use with Cross-Origin Authentication, Device Flow, and web message response mode. ``` diff --git a/internal/cli/apps.go b/internal/cli/apps.go index bac392890..dd0ee1a28 100644 --- a/internal/cli/apps.go +++ b/internal/cli/apps.go @@ -211,6 +211,21 @@ var ( ShortForm: "y", Help: "Controls whether Auth0 redirects users to the application's callback URL on authentication errors or in email verification flows: 'allow_always' or 'open_redirect_protection'. Require --is-first-party=false", } + appOrganizationUsage = Flag{ + Name: "Organization Usage", + LongForm: "organization-usage", + Help: "How the application handles organizations at authentication: 'deny', 'allow', or 'require'.", + } + appOrganizationRequireBehavior = Flag{ + Name: "Organization Require Behavior", + LongForm: "organization-require-behavior", + Help: "How to prompt for an organization when --organization-usage is 'require': 'no_prompt', 'pre_login_prompt', or 'post_login_prompt'.", + } + appOrganizationDiscoveryMethods = Flag{ + Name: "Organization Discovery Methods", + LongForm: "organization-discovery-methods", + Help: "Comma-separated list of methods for discovering organizations during the 'pre_login_prompt'. Possible values: 'email', 'organization_name'. Requires --organization-require-behavior=pre_login_prompt.", + } ) func appsCmd(cli *cli) *cobra.Command { @@ -498,6 +513,9 @@ func createAppCmd(cli *cli) *cobra.Command { IsFirstParty bool ThirdPartySecurityMode string RedirectionPolicy string + OrganizationUsage string + OrganizationRequire string + OrganizationDiscovery []string Data string Schema bool } @@ -526,6 +544,7 @@ func createAppCmd(cli *cli) *cobra.Command { auth0 apps create --name "My API Client" --type resource_server --resource-server-identifier "https://api.example.com" auth0 apps create --name myapp --type resource_server --allow-any-profile-of-type custom_authentication,on_behalf_of_token_exchange auth0 apps create --name "My 3P App" --type regular --is-first-party=false --third-party-security-mode strict --redirection-policy open_redirect_protection + auth0 apps create --name myapp --type regular --organization-usage require --organization-require-behavior pre_login_prompt --organization-discovery-methods email,organization_name # Discover the payload schema auth0 apps create --schema @@ -697,6 +716,17 @@ func createAppCmd(cli *cli) *cobra.Command { a.RedirectionPolicy = &inputs.RedirectionPolicy } + // Set organization behavior. + if inputs.OrganizationUsage != "" { + a.OrganizationUsage = &inputs.OrganizationUsage + } + if inputs.OrganizationRequire != "" { + a.OrganizationRequireBehavior = &inputs.OrganizationRequire + } + if len(inputs.OrganizationDiscovery) > 0 { + a.OrganizationDiscoveryMethods = &inputs.OrganizationDiscovery + } + // Set grants. if len(inputs.Grants) > 0 { a.GrantTypes = apiGrantsFor(inputs.Grants) @@ -740,6 +770,9 @@ func createAppCmd(cli *cli) *cobra.Command { appIsFirstParty.RegisterBool(cmd, &inputs.IsFirstParty, true) appThirdPartySecurityMode.RegisterString(cmd, &inputs.ThirdPartySecurityMode, "") appRedirectionPolicy.RegisterString(cmd, &inputs.RedirectionPolicy, "") + appOrganizationUsage.RegisterString(cmd, &inputs.OrganizationUsage, "") + appOrganizationRequireBehavior.RegisterString(cmd, &inputs.OrganizationRequire, "") + appOrganizationDiscoveryMethods.RegisterStringSlice(cmd, &inputs.OrganizationDiscovery, nil) dataFlag.RegisterString(cmd, &inputs.Data, "") schemaFlag.RegisterBool(cmd, &inputs.Schema, false) markDataExclusive(cmd) @@ -766,6 +799,9 @@ func updateAppCmd(cli *cli) *cobra.Command { IsFirstParty bool ThirdPartySecurityMode string RedirectionPolicy string + OrganizationUsage string + OrganizationRequire string + OrganizationDiscovery []string Data string Schema bool } @@ -792,6 +828,7 @@ func updateAppCmd(cli *cli) *cobra.Command { auth0 apps update -n myapp -d -t [native|spa|regular|m2m] -r --json --metadata "foo=bar,bazz=buzz" auth0 apps update --allow-any-profile-of-type custom_authentication,on_behalf_of_token_exchange auth0 apps update --redirection-policy allow_always + auth0 apps update --organization-usage require --organization-require-behavior pre_login_prompt # Discover the payload schema auth0 apps update --schema @@ -998,6 +1035,18 @@ func updateAppCmd(cli *cli) *cobra.Command { a.RedirectionPolicy = &inputs.RedirectionPolicy } + if appOrganizationUsage.IsSet(cmd) { + a.OrganizationUsage = &inputs.OrganizationUsage + } + + if appOrganizationRequireBehavior.IsSet(cmd) { + a.OrganizationRequireBehavior = &inputs.OrganizationRequire + } + + if appOrganizationDiscoveryMethods.IsSet(cmd) { + a.OrganizationDiscoveryMethods = &inputs.OrganizationDiscovery + } + if err := ansi.Waiting(func() error { return cli.api.Client.Update(cmd.Context(), inputs.ID, a) }); err != nil { @@ -1028,6 +1077,9 @@ func updateAppCmd(cli *cli) *cobra.Command { appIsFirstParty.RegisterBoolU(cmd, &inputs.IsFirstParty, true) appThirdPartySecurityMode.RegisterStringU(cmd, &inputs.ThirdPartySecurityMode, "") appRedirectionPolicy.RegisterStringU(cmd, &inputs.RedirectionPolicy, "") + appOrganizationUsage.RegisterStringU(cmd, &inputs.OrganizationUsage, "") + appOrganizationRequireBehavior.RegisterStringU(cmd, &inputs.OrganizationRequire, "") + appOrganizationDiscoveryMethods.RegisterStringSliceU(cmd, &inputs.OrganizationDiscovery, nil) dataFlag.RegisterString(cmd, &inputs.Data, "") schemaFlag.RegisterBool(cmd, &inputs.Schema, false) markDataExclusive(cmd) From 9e9db66c0785697fe1cf9e80a9dffc2488944448 Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Tue, 29 Sep 2026 17:19:14 +0530 Subject: [PATCH 2/3] test: cover organization flags and enforce discovery-methods dependency Add table-driven tests for the organization flag mapping on apps create and update, and validate client-side that --organization-discovery-methods is only used with --organization-require-behavior=pre_login_prompt so a misconfiguration returns an actionable error instead of a server 400. Gate the create org fields on flag presence to match update. --- internal/cli/apps.go | 45 +++++++++++++++-- internal/cli/apps_test.go | 104 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 3 deletions(-) diff --git a/internal/cli/apps.go b/internal/cli/apps.go index dd0ee1a28..82fca1450 100644 --- a/internal/cli/apps.go +++ b/internal/cli/apps.go @@ -228,6 +228,38 @@ var ( } ) +// validateAppOrganizationFlags enforces the documented dependency that +// --organization-discovery-methods only applies when the require behavior is +// pre_login_prompt, so a misconfiguration surfaces as an actionable client-side +// error instead of an opaque server-side 400. On create the require behavior +// must be supplied alongside the discovery methods; on update it may already be +// set on the application, so an absent flag is left to the server to validate. +func validateAppOrganizationFlags(cmd *cobra.Command, requireBehavior string, requireBehaviorMandatory bool) error { + if !appOrganizationDiscoveryMethods.IsSet(cmd) { + return nil + } + + if !appOrganizationRequireBehavior.IsSet(cmd) { + if requireBehaviorMandatory { + return usageError{ + err: errors.New("--organization-discovery-methods requires --organization-require-behavior=pre_login_prompt"), + reason: "missing_required_flags", + } + } + + return nil + } + + if requireBehavior != "pre_login_prompt" { + return validationError{ + err: fmt.Errorf("--organization-discovery-methods requires --organization-require-behavior=pre_login_prompt, but got %q", requireBehavior), + reason: "invalid_flag_value", + } + } + + return nil +} + func appsCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ Use: "apps", @@ -717,13 +749,16 @@ func createAppCmd(cli *cli) *cobra.Command { } // Set organization behavior. - if inputs.OrganizationUsage != "" { + if err := validateAppOrganizationFlags(cmd, inputs.OrganizationRequire, true); err != nil { + return err + } + if appOrganizationUsage.IsSet(cmd) { a.OrganizationUsage = &inputs.OrganizationUsage } - if inputs.OrganizationRequire != "" { + if appOrganizationRequireBehavior.IsSet(cmd) { a.OrganizationRequireBehavior = &inputs.OrganizationRequire } - if len(inputs.OrganizationDiscovery) > 0 { + if appOrganizationDiscoveryMethods.IsSet(cmd) { a.OrganizationDiscoveryMethods = &inputs.OrganizationDiscovery } @@ -1035,6 +1070,10 @@ func updateAppCmd(cli *cli) *cobra.Command { a.RedirectionPolicy = &inputs.RedirectionPolicy } + if err := validateAppOrganizationFlags(cmd, inputs.OrganizationRequire, false); err != nil { + return err + } + if appOrganizationUsage.IsSet(cmd) { a.OrganizationUsage = &inputs.OrganizationUsage } diff --git a/internal/cli/apps_test.go b/internal/cli/apps_test.go index 3b186768c..0489f276e 100644 --- a/internal/cli/apps_test.go +++ b/internal/cli/apps_test.go @@ -2,6 +2,7 @@ package cli import ( "bytes" + "context" "io" "testing" @@ -123,6 +124,25 @@ func TestAppsCreateCmd(t *testing.T) { }, expectedError: "resource-server-identifier cannot be empty for resource_server app type", }, + { + name: "Organization - discovery methods without require behavior", + args: []string{ + "--name", "My App", + "--type", "regular", + "--organization-discovery-methods", "email", + }, + expectedError: "--organization-discovery-methods requires --organization-require-behavior=pre_login_prompt", + }, + { + name: "Organization - discovery methods with wrong require behavior", + args: []string{ + "--name", "My App", + "--type", "regular", + "--organization-require-behavior", "no_prompt", + "--organization-discovery-methods", "email", + }, + expectedError: `--organization-discovery-methods requires --organization-require-behavior=pre_login_prompt, but got "no_prompt"`, + }, } for _, test := range tests { @@ -138,6 +158,90 @@ func TestAppsCreateCmd(t *testing.T) { } } +func TestAppsUpdateCmdOrganizationFlags(t *testing.T) { + tests := []struct { + name string + args []string + assertClient func(t testing.TB, c *management.Client) + }{ + { + name: "sets all organization fields when flags are provided", + args: []string{ + "some-id", + "--organization-usage", "require", + "--organization-require-behavior", "pre_login_prompt", + "--organization-discovery-methods", "email,organization_name", + }, + assertClient: func(t testing.TB, c *management.Client) { + assert.Equal(t, "require", c.GetOrganizationUsage()) + assert.Equal(t, "pre_login_prompt", c.GetOrganizationRequireBehavior()) + assert.Equal(t, []string{"email", "organization_name"}, c.GetOrganizationDiscoveryMethods()) + }, + }, + { + name: "leaves organization fields unset when flags are omitted", + args: []string{"some-id"}, + assertClient: func(t testing.TB, c *management.Client) { + assert.Nil(t, c.OrganizationUsage) + assert.Nil(t, c.OrganizationRequireBehavior) + assert.Nil(t, c.OrganizationDiscoveryMethods) + }, + }, + { + name: "updates only discovery methods when require behavior is already set server-side", + args: []string{ + "some-id", + "--organization-discovery-methods", "email", + }, + assertClient: func(t testing.TB, c *management.Client) { + assert.Nil(t, c.OrganizationRequireBehavior) + assert.Equal(t, []string{"email"}, c.GetOrganizationDiscoveryMethods()) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + clientAPI := mock.NewMockClientAPI(ctrl) + clientAPI.EXPECT(). + Read(gomock.Any(), "some-id", gomock.Any()). + Return(&management.Client{ + Name: auth0.String("some-name"), + AppType: auth0.String("regular_web"), + }, nil) + + var captured *management.Client + clientAPI.EXPECT(). + Update(gomock.Any(), "some-id", gomock.Any()). + DoAndReturn(func(_ context.Context, _ string, c *management.Client, _ ...management.RequestOption) error { + captured = c + return nil + }) + + cli := &cli{ + noInput: true, + renderer: &display.Renderer{ + MessageWriter: io.Discard, + ResultWriter: io.Discard, + }, + api: &auth0.API{Client: clientAPI}, + } + + cmd := updateAppCmd(cli) + cmd.SetArgs(test.args) + + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + test.assertClient(t, captured) + }) + } +} + func TestFormatAppSettingsPath(t *testing.T) { assert.Empty(t, formatAppSettingsPath("")) assert.Equal(t, "applications/app-id-1/settings", formatAppSettingsPath("app-id-1")) From ddbcdf6483f3cf73b75889ac1cba3757c09ddcb0 Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Tue, 29 Sep 2026 23:02:50 +0530 Subject: [PATCH 3/3] fix: align apps organization flag validation with the API - Drop the client-side organization-require-behavior requires organization-usage=require check; the Management API accepts any usage, or none, alongside any require behavior - Keep the one real cross-field rule: organization-discovery-methods requires organization-require-behavior=pre_login_prompt - Strip empty organization-discovery-methods entries before sending - Run organization flag validation before the network read and interactive prompts - Render organization fields in apps show output - Update require-behavior help text and regenerate docs --- docs/auth0_apps_create.md | 2 +- docs/auth0_apps_update.md | 2 +- internal/cli/apps.go | 84 +++++++++++++++++++++++---------------- internal/cli/apps_test.go | 13 ++++++ internal/display/apps.go | 28 +++++++++++-- 5 files changed, 89 insertions(+), 40 deletions(-) diff --git a/docs/auth0_apps_create.md b/docs/auth0_apps_create.md index 80daf14f2..c956ec8eb 100644 --- a/docs/auth0_apps_create.md +++ b/docs/auth0_apps_create.md @@ -64,7 +64,7 @@ auth0 apps create [flags] --metadata stringToString Arbitrary keys-value pairs (max 255 characters each), that can be assigned to each application. More about application metadata: https://auth0.com/docs/get-started/applications/configure-application-metadata (default []) -n, --name string Name of the application. --organization-discovery-methods strings Comma-separated list of methods for discovering organizations during the 'pre_login_prompt'. Possible values: 'email', 'organization_name'. Requires --organization-require-behavior=pre_login_prompt. - --organization-require-behavior string How to prompt for an organization when --organization-usage is 'require': 'no_prompt', 'pre_login_prompt', or 'post_login_prompt'. + --organization-require-behavior string How to prompt for an organization at authentication: 'no_prompt', 'pre_login_prompt', or 'post_login_prompt'. 'post_login_prompt' requires an OIDC-conformant application. --organization-usage string How the application handles organizations at authentication: 'deny', 'allow', or 'require'. -o, --origins strings Comma-separated list of URLs allowed to make requests from JavaScript to Auth0 API (typically used with CORS). By default, all your callback URLs will be allowed. This field allows you to enter other origins if necessary. You can also use wildcards at the subdomain level (e.g., https://*.contoso.com). Query strings and hash information are not taken into account when validating these URLs. -y, --redirection-policy string Controls whether Auth0 redirects users to the application's callback URL on authentication errors or in email verification flows: 'allow_always' or 'open_redirect_protection'. Require --is-first-party=false diff --git a/docs/auth0_apps_update.md b/docs/auth0_apps_update.md index d4ae7b611..f8c78db1d 100644 --- a/docs/auth0_apps_update.md +++ b/docs/auth0_apps_update.md @@ -63,7 +63,7 @@ auth0 apps update [flags] --metadata stringToString Arbitrary keys-value pairs (max 255 characters each), that can be assigned to each application. More about application metadata: https://auth0.com/docs/get-started/applications/configure-application-metadata (default []) -n, --name string Name of the application. --organization-discovery-methods strings Comma-separated list of methods for discovering organizations during the 'pre_login_prompt'. Possible values: 'email', 'organization_name'. Requires --organization-require-behavior=pre_login_prompt. - --organization-require-behavior string How to prompt for an organization when --organization-usage is 'require': 'no_prompt', 'pre_login_prompt', or 'post_login_prompt'. + --organization-require-behavior string How to prompt for an organization at authentication: 'no_prompt', 'pre_login_prompt', or 'post_login_prompt'. 'post_login_prompt' requires an OIDC-conformant application. --organization-usage string How the application handles organizations at authentication: 'deny', 'allow', or 'require'. -o, --origins strings Comma-separated list of URLs allowed to make requests from JavaScript to Auth0 API (typically used with CORS). By default, all your callback URLs will be allowed. This field allows you to enter other origins if necessary. You can also use wildcards at the subdomain level (e.g., https://*.contoso.com). Query strings and hash information are not taken into account when validating these URLs. -y, --redirection-policy string Controls whether Auth0 redirects users to the application's callback URL on authentication errors or in email verification flows: 'allow_always' or 'open_redirect_protection'. Require --is-first-party=false diff --git a/internal/cli/apps.go b/internal/cli/apps.go index 82fca1450..381f87152 100644 --- a/internal/cli/apps.go +++ b/internal/cli/apps.go @@ -219,7 +219,7 @@ var ( appOrganizationRequireBehavior = Flag{ Name: "Organization Require Behavior", LongForm: "organization-require-behavior", - Help: "How to prompt for an organization when --organization-usage is 'require': 'no_prompt', 'pre_login_prompt', or 'post_login_prompt'.", + Help: "How to prompt for an organization at authentication: 'no_prompt', 'pre_login_prompt', or 'post_login_prompt'. 'post_login_prompt' requires an OIDC-conformant application.", } appOrganizationDiscoveryMethods = Flag{ Name: "Organization Discovery Methods", @@ -228,32 +228,37 @@ var ( } ) -// validateAppOrganizationFlags enforces the documented dependency that -// --organization-discovery-methods only applies when the require behavior is -// pre_login_prompt, so a misconfiguration surfaces as an actionable client-side -// error instead of an opaque server-side 400. On create the require behavior -// must be supplied alongside the discovery methods; on update it may already be -// set on the application, so an absent flag is left to the server to validate. -func validateAppOrganizationFlags(cmd *cobra.Command, requireBehavior string, requireBehaviorMandatory bool) error { - if !appOrganizationDiscoveryMethods.IsSet(cmd) { - return nil - } - - if !appOrganizationRequireBehavior.IsSet(cmd) { - if requireBehaviorMandatory { - return usageError{ - err: errors.New("--organization-discovery-methods requires --organization-require-behavior=pre_login_prompt"), - reason: "missing_required_flags", +// validateAppOrganizationFlags mirrors the one cross-field dependency the +// Management API actually enforces, so a misconfiguration surfaces as an +// actionable client-side error instead of a 400 from the server: +// - --organization-discovery-methods can only be used when +// --organization-require-behavior is "pre_login_prompt". +// +// Deliberately NOT validated here, because the API imposes no such rule +// (confirmed by sweeping every combination against a live tenant): +// - --organization-require-behavior has no dependency on --organization-usage; +// the server accepts any usage, or none, alongside any require behavior. +// - --organization-require-behavior=post_login_prompt requires an +// OIDC-conformant application, a condition on a different field that the +// server validates and reports on its own. +// +// On create the prerequisite flag must be supplied alongside the dependent one. +// On update it may already be set on the application, so an absent prerequisite +// flag is left to the server to validate. +func validateAppOrganizationFlags(cmd *cobra.Command, requireBehavior string, prerequisiteMandatory bool) error { + if appOrganizationDiscoveryMethods.IsSet(cmd) { + if !appOrganizationRequireBehavior.IsSet(cmd) { + if prerequisiteMandatory { + return usageError{ + err: errors.New("--organization-discovery-methods requires --organization-require-behavior=pre_login_prompt"), + reason: "missing_required_flags", + } + } + } else if requireBehavior != "pre_login_prompt" { + return validationError{ + err: fmt.Errorf("--organization-discovery-methods requires --organization-require-behavior=pre_login_prompt, but got %q", requireBehavior), + reason: "invalid_flag_value", } - } - - return nil - } - - if requireBehavior != "pre_login_prompt" { - return validationError{ - err: fmt.Errorf("--organization-discovery-methods requires --organization-require-behavior=pre_login_prompt, but got %q", requireBehavior), - reason: "invalid_flag_value", } } @@ -599,6 +604,12 @@ func createAppCmd(cli *cli) *cobra.Command { return createAppFromJSON(cli, cmd, payload, inputs.RevealSecrets) } + // Validate organization flag dependencies before any interactive + // prompts so a misconfiguration fails fast. + if err := validateAppOrganizationFlags(cmd, inputs.OrganizationRequire, true); err != nil { + return err + } + if err := appName.Ask(cmd, &inputs.Name, nil); err != nil { return err } @@ -749,9 +760,6 @@ func createAppCmd(cli *cli) *cobra.Command { } // Set organization behavior. - if err := validateAppOrganizationFlags(cmd, inputs.OrganizationRequire, true); err != nil { - return err - } if appOrganizationUsage.IsSet(cmd) { a.OrganizationUsage = &inputs.OrganizationUsage } @@ -759,7 +767,8 @@ func createAppCmd(cli *cli) *cobra.Command { a.OrganizationRequireBehavior = &inputs.OrganizationRequire } if appOrganizationDiscoveryMethods.IsSet(cmd) { - a.OrganizationDiscoveryMethods = &inputs.OrganizationDiscovery + discoveryMethods := excludeEmptyEntries(inputs.OrganizationDiscovery) + a.OrganizationDiscoveryMethods = &discoveryMethods } // Set grants. @@ -878,6 +887,14 @@ func updateAppCmd(cli *cli) *cobra.Command { return printOperationSchema(cli, "PATCH", "/clients/{id}") } + // Validate organization flag dependencies before the network read + // and interactive prompts so a misconfiguration fails fast. On + // update a prerequisite may already be set on the application, so an + // absent flag is deferred to the server. + if err := validateAppOrganizationFlags(cmd, inputs.OrganizationRequire, false); err != nil { + return err + } + var current *management.Client if len(args) == 0 { @@ -1070,10 +1087,6 @@ func updateAppCmd(cli *cli) *cobra.Command { a.RedirectionPolicy = &inputs.RedirectionPolicy } - if err := validateAppOrganizationFlags(cmd, inputs.OrganizationRequire, false); err != nil { - return err - } - if appOrganizationUsage.IsSet(cmd) { a.OrganizationUsage = &inputs.OrganizationUsage } @@ -1083,7 +1096,8 @@ func updateAppCmd(cli *cli) *cobra.Command { } if appOrganizationDiscoveryMethods.IsSet(cmd) { - a.OrganizationDiscoveryMethods = &inputs.OrganizationDiscovery + discoveryMethods := excludeEmptyEntries(inputs.OrganizationDiscovery) + a.OrganizationDiscoveryMethods = &discoveryMethods } if err := ansi.Waiting(func() error { diff --git a/internal/cli/apps_test.go b/internal/cli/apps_test.go index 0489f276e..93c8dfabc 100644 --- a/internal/cli/apps_test.go +++ b/internal/cli/apps_test.go @@ -129,6 +129,7 @@ func TestAppsCreateCmd(t *testing.T) { args: []string{ "--name", "My App", "--type", "regular", + "--organization-usage", "require", "--organization-discovery-methods", "email", }, expectedError: "--organization-discovery-methods requires --organization-require-behavior=pre_login_prompt", @@ -138,6 +139,7 @@ func TestAppsCreateCmd(t *testing.T) { args: []string{ "--name", "My App", "--type", "regular", + "--organization-usage", "require", "--organization-require-behavior", "no_prompt", "--organization-discovery-methods", "email", }, @@ -198,6 +200,17 @@ func TestAppsUpdateCmdOrganizationFlags(t *testing.T) { assert.Equal(t, []string{"email"}, c.GetOrganizationDiscoveryMethods()) }, }, + { + name: "strips empty discovery method entries from a trailing comma", + args: []string{ + "some-id", + "--organization-require-behavior", "pre_login_prompt", + "--organization-discovery-methods", "email,", + }, + assertClient: func(t testing.TB, c *management.Client) { + assert.Equal(t, []string{"email"}, c.GetOrganizationDiscoveryMethods()) + }, + }, } for _, test := range tests { diff --git a/internal/display/apps.go b/internal/display/apps.go index fc162db77..d9bcb90fd 100644 --- a/internal/display/apps.go +++ b/internal/display/apps.go @@ -43,7 +43,12 @@ type applicationView struct { IsFirstParty *bool ThirdPartySecurityMode string RedirectionPolicy string - revealSecret bool + + OrganizationUsage string + OrganizationRequireBehavior string + OrganizationDiscoveryMethods []string + + revealSecret bool raw interface{} } @@ -143,6 +148,18 @@ func (v *applicationView) KeyValues() [][]string { keyValues = append(keyValues, []string{"REDIRECTION POLICY", v.RedirectionPolicy}) } + if v.OrganizationUsage != "" { + keyValues = append(keyValues, []string{"ORGANIZATION USAGE", v.OrganizationUsage}) + } + + if v.OrganizationRequireBehavior != "" { + keyValues = append(keyValues, []string{"ORGANIZATION REQUIRE BEHAVIOR", v.OrganizationRequireBehavior}) + } + + if len(v.OrganizationDiscoveryMethods) > 0 { + keyValues = append(keyValues, []string{"ORGANIZATION DISCOVERY METHODS", strings.Join(v.OrganizationDiscoveryMethods, ", ")}) + } + return keyValues } @@ -231,8 +248,13 @@ func makeApplicationView(client *management.Client, revealSecrets bool) *applica IsFirstParty: client.IsFirstParty, ThirdPartySecurityMode: client.GetThirdPartySecurityMode(), RedirectionPolicy: client.GetRedirectionPolicy(), - raw: client, - RefreshToken: string(jsonRefreshToken), + + OrganizationUsage: client.GetOrganizationUsage(), + OrganizationRequireBehavior: client.GetOrganizationRequireBehavior(), + OrganizationDiscoveryMethods: client.GetOrganizationDiscoveryMethods(), + + raw: client, + RefreshToken: string(jsonRefreshToken), } }