diff --git a/docs/auth0_quickstarts.md b/docs/auth0_quickstarts.md index 9ad2f2ed4..d2359e3d4 100644 --- a/docs/auth0_quickstarts.md +++ b/docs/auth0_quickstarts.md @@ -12,4 +12,5 @@ Step-by-step guides to quickly integrate Auth0 into your application. - [auth0 quickstarts download](auth0_quickstarts_download.md) - Download a Quickstart sample app for a specific tech stack - [auth0 quickstarts list](auth0_quickstarts_list.md) - List the available Quickstarts - [auth0 quickstarts setup](auth0_quickstarts_setup.md) - Set up Auth0 for your quickstart application +- [auth0 quickstarts setup-experimental](auth0_quickstarts_setup-experimental.md) - Set up Auth0 for your quickstart application diff --git a/docs/auth0_quickstarts_download.md b/docs/auth0_quickstarts_download.md index ea55fbbd4..1f3638b22 100644 --- a/docs/auth0_quickstarts_download.md +++ b/docs/auth0_quickstarts_download.md @@ -47,5 +47,6 @@ auth0 quickstarts download [flags] - [auth0 quickstarts download](auth0_quickstarts_download.md) - Download a Quickstart sample app for a specific tech stack - [auth0 quickstarts list](auth0_quickstarts_list.md) - List the available Quickstarts - [auth0 quickstarts setup](auth0_quickstarts_setup.md) - Set up Auth0 for your quickstart application +- [auth0 quickstarts setup-experimental](auth0_quickstarts_setup-experimental.md) - Set up Auth0 for your quickstart application diff --git a/docs/auth0_quickstarts_list.md b/docs/auth0_quickstarts_list.md index 8213dbbf3..4fc23e842 100644 --- a/docs/auth0_quickstarts_list.md +++ b/docs/auth0_quickstarts_list.md @@ -49,5 +49,6 @@ auth0 quickstarts list [flags] - [auth0 quickstarts download](auth0_quickstarts_download.md) - Download a Quickstart sample app for a specific tech stack - [auth0 quickstarts list](auth0_quickstarts_list.md) - List the available Quickstarts - [auth0 quickstarts setup](auth0_quickstarts_setup.md) - Set up Auth0 for your quickstart application +- [auth0 quickstarts setup-experimental](auth0_quickstarts_setup-experimental.md) - Set up Auth0 for your quickstart application diff --git a/docs/auth0_quickstarts_setup-experimental.md b/docs/auth0_quickstarts_setup-experimental.md new file mode 100644 index 000000000..7613b4f65 --- /dev/null +++ b/docs/auth0_quickstarts_setup-experimental.md @@ -0,0 +1,57 @@ +--- +layout: default +parent: auth0 quickstarts +has_toc: false +--- +# auth0 quickstarts setup-experimental + +Creates an Auth0 application and generates a .env file with the necessary configuration. + +The command will: + 1. Check if you are authenticated (and prompt for login if needed) + 2. Create an Auth0 application based on the specified type + 3. Generate a .env file with the appropriate environment variables + +Supported types are dynamically loaded from the `QuickstartConfigs` map in the codebase. + +## Usage +``` +auth0 quickstarts setup-experimental [flags] +``` + +## Examples + +``` + auth0 quickstarts setup-experimental --type spa:react:vite + auth0 quickstarts setup-experimental --type regular:nextjs:none + auth0 quickstarts setup-experimental --type native:react-native:none +``` + + +## Flags + +``` + --name string Name of the Auth0 application + --port int Port number for the application + --type string Type of the quickstart application (e.g., spa:react:vite, regular:nextjs:none) +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 quickstarts download](auth0_quickstarts_download.md) - Download a Quickstart sample app for a specific tech stack +- [auth0 quickstarts list](auth0_quickstarts_list.md) - List the available Quickstarts +- [auth0 quickstarts setup](auth0_quickstarts_setup.md) - Set up Auth0 for your quickstart application +- [auth0 quickstarts setup-experimental](auth0_quickstarts_setup-experimental.md) - Set up Auth0 for your quickstart application + + diff --git a/docs/auth0_quickstarts_setup.md b/docs/auth0_quickstarts_setup.md index c7a158fb1..ceb09a84e 100644 --- a/docs/auth0_quickstarts_setup.md +++ b/docs/auth0_quickstarts_setup.md @@ -61,5 +61,6 @@ auth0 quickstarts setup [flags] - [auth0 quickstarts download](auth0_quickstarts_download.md) - Download a Quickstart sample app for a specific tech stack - [auth0 quickstarts list](auth0_quickstarts_list.md) - List the available Quickstarts - [auth0 quickstarts setup](auth0_quickstarts_setup.md) - Set up Auth0 for your quickstart application +- [auth0 quickstarts setup-experimental](auth0_quickstarts_setup-experimental.md) - Set up Auth0 for your quickstart application diff --git a/internal/auth0/quickstart.go b/internal/auth0/quickstart.go index 60a10fee2..a3cbd582d 100644 --- a/internal/auth0/quickstart.go +++ b/internal/auth0/quickstart.go @@ -14,7 +14,6 @@ import ( "github.com/auth0/go-auth0/management" "github.com/auth0/auth0-cli/internal/buildinfo" - "github.com/auth0/auth0-cli/internal/utils" ) @@ -174,3 +173,499 @@ func (q Quickstarts) Stacks() []string { return stacks } + +const DetectionSub = "DETECTION_SUB" + +type FileOutputStrategy struct { + Path string + Format string +} + +type RequestParams struct { + AppType string + Callbacks []string + AllowedLogoutURLs []string + WebOrigins []string + Name string +} + +type AppConfig struct { + EnvValues map[string]string + RequestParams RequestParams + Strategy FileOutputStrategy +} + +var QuickstartConfigs = map[string]AppConfig{ + + // ==========================================. + "spa:react:vite": { + EnvValues: map[string]string{ + "VITE_AUTH0_DOMAIN": DetectionSub, + "VITE_AUTH0_CLIENT_ID": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "spa", + Callbacks: []string{"http://localhost:5173/callback"}, + AllowedLogoutURLs: []string{"http://localhost:5173"}, + WebOrigins: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "spa:angular:none": { + EnvValues: map[string]string{ + "domain": DetectionSub, + "clientId": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "spa", + Callbacks: []string{"http://localhost:4200/callback"}, + AllowedLogoutURLs: []string{"http://localhost:4200"}, + WebOrigins: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "src/environments/environment.ts", Format: "ts"}, + }, + "spa:vue:vite": { + EnvValues: map[string]string{ + "VITE_AUTH0_DOMAIN": DetectionSub, + "VITE_AUTH0_CLIENT_ID": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "spa", + Callbacks: []string{"http://localhost:5173/callback"}, + AllowedLogoutURLs: []string{"http://localhost:5173"}, + WebOrigins: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "spa:svelte:vite": { + EnvValues: map[string]string{ + "VITE_AUTH0_DOMAIN": DetectionSub, + "VITE_AUTH0_CLIENT_ID": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "spa", + Callbacks: []string{"http://localhost:5173/callback"}, + AllowedLogoutURLs: []string{"http://localhost:5173"}, + WebOrigins: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "spa:vanilla-javascript:vite": { + EnvValues: map[string]string{ + "VITE_AUTH0_DOMAIN": DetectionSub, + "VITE_AUTH0_CLIENT_ID": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "spa", + Callbacks: []string{"http://localhost:5173/callback"}, + AllowedLogoutURLs: []string{"http://localhost:5173"}, + WebOrigins: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "spa:flutter-web:none": { + EnvValues: map[string]string{ + "domain": DetectionSub, + "clientId": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "spa", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + WebOrigins: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "lib/auth_config.dart", Format: "dart"}, + }, + + // ==========================================. + "regular:nextjs:none": { + EnvValues: map[string]string{ + "AUTH0_DOMAIN": DetectionSub, + "AUTH0_CLIENT_ID": DetectionSub, + "AUTH0_CLIENT_SECRET": DetectionSub, + "AUTH0_SECRET": DetectionSub, + "APP_BASE_URL": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{"http://localhost:3000/callback"}, + AllowedLogoutURLs: []string{"http://localhost:3000"}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:nuxt:none": { + EnvValues: map[string]string{ + "NUXT_AUTH0_DOMAIN": DetectionSub, + "NUXT_AUTH0_CLIENT_ID": DetectionSub, + "NUXT_AUTH0_CLIENT_SECRET": DetectionSub, + "NUXT_AUTH0_SESSION_SECRET": DetectionSub, + "NUXT_AUTH0_APP_BASE_URL": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{"http://localhost:3000/callback"}, + AllowedLogoutURLs: []string{"http://localhost:3000"}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:fastify:none": { + EnvValues: map[string]string{ + "AUTH0_DOMAIN": DetectionSub, + "AUTH0_CLIENT_ID": DetectionSub, + "AUTH0_CLIENT_SECRET": DetectionSub, + "SESSION_SECRET": DetectionSub, + "APP_BASE_URL": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{"http://localhost:3000/callback"}, + AllowedLogoutURLs: []string{"http://localhost:3000"}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:sveltekit:none": { + EnvValues: map[string]string{ + "VITE_AUTH0_DOMAIN": DetectionSub, + "VITE_AUTH0_CLIENT_ID": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:express:none": { + EnvValues: map[string]string{ + "ISSUER_BASE_URL": DetectionSub, + "CLIENT_ID": DetectionSub, + "SECRET": DetectionSub, + "BASE_URL": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{"http://localhost:3000/callback"}, + AllowedLogoutURLs: []string{"http://localhost:3000"}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:hono:none": { + EnvValues: map[string]string{ + "AUTH0_DOMAIN": DetectionSub, + "AUTH0_CLIENT_ID": DetectionSub, + "AUTH0_CLIENT_SECRET": DetectionSub, + "AUTH0_SESSION_ENCRYPTION_KEY": DetectionSub, + "BASE_URL": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{"http://localhost:3000/callback"}, + AllowedLogoutURLs: []string{"http://localhost:3000"}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:vanilla-python:none": { + EnvValues: map[string]string{ + "AUTH0_DOMAIN": DetectionSub, + "AUTH0_CLIENT_ID": DetectionSub, + "AUTH0_CLIENT_SECRET": DetectionSub, + "AUTH0_SECRET": DetectionSub, + "AUTH0_REDIRECT_URI": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{"http://localhost:5000/callback"}, + AllowedLogoutURLs: []string{"http://localhost:5000"}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:django:none": { + EnvValues: map[string]string{ + "AUTH0_DOMAIN": DetectionSub, + "AUTH0_CLIENT_ID": DetectionSub, + "AUTH0_CLIENT_SECRET": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:vanilla-go:none": { + EnvValues: map[string]string{ + "AUTH0_DOMAIN": DetectionSub, + "AUTH0_CLIENT_ID": DetectionSub, + "AUTH0_CLIENT_SECRET": DetectionSub, + "AUTH0_CALLBACK_URL": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:vanilla-java:maven": { + EnvValues: map[string]string{ + "auth0.domain": DetectionSub, + "auth0.clientId": DetectionSub, + "auth0.clientSecret": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "src/main/resources/application.properties", Format: "properties"}, + }, + "regular:java-ee:maven": { + EnvValues: map[string]string{ + "auth0.domain": DetectionSub, + "auth0.clientId": DetectionSub, + "auth0.clientSecret": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "src/main/resources/META-INF/microprofile-config.properties", Format: "properties"}, + }, + "regular:spring-boot:maven": { + EnvValues: map[string]string{ + "okta.oauth2.issuer": DetectionSub, + "okta.oauth2.client-id": DetectionSub, + "okta.oauth2.client-secret": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{"http://localhost:8000/callback"}, + AllowedLogoutURLs: []string{"http://localhost:8000"}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "src/main/resources/application.yml", Format: "yaml"}, + }, + "regular:aspnet-mvc:none": { + EnvValues: map[string]string{ + "Auth0:Domain": DetectionSub, + "Auth0:ClientId": DetectionSub, + "Auth0:ClientSecret": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "appsettings.json", Format: "json"}, + }, + "regular:aspnet-blazor:none": { + EnvValues: map[string]string{ + "Auth0:Domain": DetectionSub, + "Auth0:ClientId": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "appsettings.json", Format: "json"}, + }, + "regular:aspnet-owin:none": { + EnvValues: map[string]string{ + "auth0:Domain": DetectionSub, + "auth0:ClientId": DetectionSub, + "auth0:ClientSecret": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "Web.config", Format: "xml"}, + }, + "regular:vanilla-php:composer": { + EnvValues: map[string]string{ + "AUTH0_DOMAIN": DetectionSub, + "AUTH0_CLIENT_ID": DetectionSub, + "AUTH0_CLIENT_SECRET": DetectionSub, + "AUTH0_COOKIE_SECRET": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:laravel:composer": { + EnvValues: map[string]string{ + "AUTH0_DOMAIN": DetectionSub, + "AUTH0_CLIENT_ID": DetectionSub, + "AUTH0_CLIENT_SECRET": DetectionSub, + "AUTH0_COOKIE_SECRET": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{"http://localhost:8000/callback"}, + AllowedLogoutURLs: []string{"http://localhost:8000"}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "regular:rails:none": { + EnvValues: map[string]string{ + "auth0_domain": DetectionSub, + "auth0_client_id": DetectionSub, + "auth0_client_secret": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "regular_web", + Callbacks: []string{"http://localhost:3000/callback"}, + AllowedLogoutURLs: []string{"http://localhost:3000"}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + + // ==========================================. + "native:flutter:none": { + EnvValues: map[string]string{ + "domain": DetectionSub, + "clientId": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "native", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "lib/auth_config.dart", Format: "dart"}, + }, + "native:react-native:none": { + EnvValues: map[string]string{ + "AUTH0_DOMAIN": DetectionSub, + "AUTH0_CLIENT_ID": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "native", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "native:expo:none": { + EnvValues: map[string]string{ + "EXPO_PUBLIC_AUTH0_DOMAIN": DetectionSub, + "EXPO_PUBLIC_AUTH0_CLIENT_ID": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "native", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "native:ionic-angular:none": { + EnvValues: map[string]string{ + "domain": DetectionSub, + "clientId": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "native", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "src/environments/environment.ts", Format: "ts"}, + }, + "native:ionic-react:vite": { + EnvValues: map[string]string{ + "VITE_AUTH0_DOMAIN": DetectionSub, + "VITE_AUTH0_CLIENT_ID": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "native", + Callbacks: []string{DetectionSub}, + Name: DetectionSub, + AllowedLogoutURLs: []string{DetectionSub}, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "native:ionic-vue:vite": { + EnvValues: map[string]string{ + "VITE_AUTH0_DOMAIN": DetectionSub, + "VITE_AUTH0_CLIENT_ID": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "native", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"}, + }, + "native:dotnet-mobile:none": { + EnvValues: map[string]string{ + "Auth0:Domain": DetectionSub, + "Auth0:ClientId": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "native", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "appsettings.json", Format: "json"}, + }, + "native:maui:none": { + EnvValues: map[string]string{ + "Auth0:Domain": DetectionSub, + "Auth0:ClientId": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "native", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "appsettings.json", Format: "json"}, + }, + "native:wpf-winforms:none": { + EnvValues: map[string]string{ + "Auth0:Domain": DetectionSub, + "Auth0:ClientId": DetectionSub, + "Auth0:ClientSecret": DetectionSub, + }, + RequestParams: RequestParams{ + AppType: "native", + Callbacks: []string{DetectionSub}, + AllowedLogoutURLs: []string{DetectionSub}, + Name: DetectionSub, + }, + Strategy: FileOutputStrategy{Path: "appsettings.json", Format: "json"}, + }, +} diff --git a/internal/cli/quickstarts.go b/internal/cli/quickstarts.go index d9978fc24..51d5b553d 100644 --- a/internal/cli/quickstarts.go +++ b/internal/cli/quickstarts.go @@ -3,16 +3,19 @@ package cli import ( "context" _ "embed" + "encoding/json" "fmt" "os" "path" "path/filepath" "regexp" + "sort" "strconv" "strings" "github.com/auth0/go-auth0/management" "github.com/spf13/cobra" + "gopkg.in/yaml.v2" "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/auth0" @@ -68,6 +71,7 @@ func quickstartsCmd(cli *cli) *cobra.Command { cmd.AddCommand(listQuickstartsCmd(cli)) cmd.AddCommand(downloadQuickstartCmd(cli)) cmd.AddCommand(setupQuickstartCmd(cli)) + cmd.AddCommand(setupQuickstartCmdExperimental(cli)) return cmd } @@ -438,6 +442,27 @@ var ( } ) +// SetupInputs holds the user-provided inputs for the setup-experimental command. +type SetupInputs struct { + Name string + App bool + Type string + Framework string + BuildTool string + Port int + CallbackURL string + LogoutURL string + WebOriginURL string + API bool + Identifier string + Audience string + SigningAlg string + Scopes string + TokenLifetime string + OfflineAccess bool + MetaData map[string]interface{} +} + func setupQuickstartCmd(cli *cli) *cobra.Command { var inputs struct { Type string @@ -656,3 +681,529 @@ func setupQuickstartCmd(cli *cli) *cobra.Command { return cmd } + +func setupQuickstartCmdExperimental(cli *cli) *cobra.Command { + var inputs SetupInputs + + cmd := &cobra.Command{ + Use: "setup-experimental", + Args: cobra.NoArgs, + Short: "Set up Auth0 for your quickstart application", + Long: "Creates an Auth0 application and generates a .env file with the necessary configuration.\n\n" + + "The command will:\n" + + " 1. Check if you are authenticated (and prompt for login if needed)\n" + + " 2. Create an Auth0 application based on the specified type\n" + + " 3. Generate a .env file with the appropriate environment variables\n\n" + + "Supported types are dynamically loaded from the `QuickstartConfigs` map in the codebase.", + Example: ` auth0 quickstarts setup-experimental --type spa:react:vite + auth0 quickstarts setup-experimental --type regular:nextjs:none + auth0 quickstarts setup-experimental --type native:react-native:none`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if err := cli.setupWithAuthentication(ctx); err != nil { + return fmt.Errorf("authentication required: %w", err) + } + + qsConfigKey, updatedInputs, err := getQuickstartConfigKey(inputs) + if err != nil { + return fmt.Errorf("failed to get quickstart configuration: %w", err) + } + inputs = updatedInputs + + // Create the Auth0 application client if requested. + if inputs.App { + // Validate the config key only when an app is being created. + config, exists := auth0.QuickstartConfigs[qsConfigKey] + if !exists { + return fmt.Errorf("unsupported quickstart arguments: %s. Supported types: %v", qsConfigKey, getSupportedQuickstartTypes()) + } + + client, err := generateClient(inputs, config.RequestParams) + if err != nil { + return fmt.Errorf("failed to generate client: %w", err) + } + + if err := ansi.Waiting(func() error { + return cli.api.Client.Create(ctx, client) + }); err != nil { + return fmt.Errorf("failed to create application: %w", err) + } + + tenant, err := cli.Config.GetTenant(cli.tenant) + if err != nil { + return fmt.Errorf("failed to get tenant: %w", err) + } + + envFileName, _, err := GenerateAndWriteQuickstartConfig(&config.Strategy, config.EnvValues, tenant.Domain, client, inputs.Port) + if err != nil { + return fmt.Errorf("failed to generate config file: %w", err) + } + printClientDetails(cli, client, inputs.Port, envFileName) + } + + // Create the Auth0 API resource server if requested. + if inputs.API { + tokenLifetime, _ := strconv.Atoi(inputs.TokenLifetime) + if tokenLifetime <= 0 { + tokenLifetime = 86400 + } + + rs := &management.ResourceServer{ + Name: &inputs.Identifier, + Identifier: &inputs.Identifier, + SigningAlgorithm: &inputs.SigningAlg, + TokenLifetime: &tokenLifetime, + } + if inputs.OfflineAccess { + allow := true + rs.AllowOfflineAccess = &allow + } + + if err := ansi.Waiting(func() error { + return cli.api.ResourceServer.Create(ctx, rs) + }); err != nil { + return fmt.Errorf("failed to create API: %w", err) + } + printAPIDetails(cli, rs) + } + + return nil + }, + } + + cmd.Flags().StringVar(&inputs.Type, "type", "", "Type of the quickstart application (e.g., spa:react:vite, regular:nextjs:none)") + cmd.Flags().StringVar(&inputs.Name, "name", "", "Name of the Auth0 application") + cmd.Flags().IntVar(&inputs.Port, "port", 0, "Port number for the application") + + return cmd +} + +func printClientDetails(cli *cli, client *management.Client, port int, configFileLocation string) { + cli.renderer.Infof("Application %q created (Client ID: %s)", client.GetName(), client.GetClientID()) + cli.renderer.Infof("Manage: https://manage.auth0.com/dashboard/#/applications/%s/settings", client.GetClientID()) + + if client.Callbacks != nil && len(client.GetCallbacks()) > 0 { + cli.renderer.Infof("Callback URLs: %s", strings.Join(client.GetCallbacks(), ", ")) + } + if client.AllowedLogoutURLs != nil && len(client.GetAllowedLogoutURLs()) > 0 { + cli.renderer.Infof("Logout URLs: %s", strings.Join(client.GetAllowedLogoutURLs(), ", ")) + } + cli.renderer.Infof("Config file created: %s", configFileLocation) +} + +func printAPIDetails(cli *cli, rs *management.ResourceServer) { + cli.renderer.Infof("API %q registered (Identifier: %s)", rs.GetName(), rs.GetIdentifier()) + cli.renderer.Infof("Manage: https://manage.auth0.com/dashboard/#/apis/%s/settings", rs.GetID()) +} + +// Helper function to get supported quickstart types. +func getSupportedQuickstartTypes() []string { + var types []string + for key := range auth0.QuickstartConfigs { + types = append(types, key) + } + sort.Strings(types) + return types +} + +// frameworksForType returns the list of unique frameworks available for the given app type. +func frameworksForType(qsType string) []string { + seen := make(map[string]bool) + var frameworks []string + for key := range auth0.QuickstartConfigs { + parts := strings.SplitN(key, ":", 3) + if len(parts) >= 2 && parts[0] == qsType { + fw := parts[1] + if !seen[fw] { + seen[fw] = true + frameworks = append(frameworks, fw) + } + } + } + sort.Strings(frameworks) + return frameworks +} + +func getQuickstartConfigKey(inputs SetupInputs) (string, SetupInputs, error) { + // Prompt for target resource(s) when neither flag is provided. + if !inputs.App && !inputs.API { + var selections []string + + err := prompt.AskMultiSelect( + "What do you want to create? (select whatever applies)", + &selections, + "App", + "API", + ) + if err != nil { + return "", inputs, fmt.Errorf("failed to select target resource(s): %v", err) + } + + for _, selection := range selections { + switch strings.ToLower(selection) { + case "app": + inputs.App = true + case "api": + inputs.API = true + } + } + + if !inputs.App && !inputs.API { + return "", inputs, fmt.Errorf("please select at least one option: App and/or API") + } + } + + // Handle application creation inputs. + if inputs.App { + // Prompt for --type if not provided. + if inputs.Type == "" { + types := []string{"spa", "regular", "native"} + q := prompt.SelectInput("type", "Select the application type", "", types, "spa", true) + if err := prompt.AskOne(q, &inputs.Type); err != nil { + return "", inputs, fmt.Errorf("failed to select application type: %v", err) + } + } + + // Prompt for --framework filtered to the selected type. + if inputs.Framework == "" { + frameworks := frameworksForType(inputs.Type) + if len(frameworks) == 0 { + return "", inputs, fmt.Errorf("no frameworks available for type %q", inputs.Type) + } + q := prompt.SelectInput("framework", "Select the framework", "", frameworks, frameworks[0], true) + if err := prompt.AskOne(q, &inputs.Framework); err != nil { + return "", inputs, fmt.Errorf("failed to select framework: %v", err) + } + } + + // Prompt for --build-tool if not provided (optional). + if inputs.BuildTool == "" { + buildTools := []string{"vite", "webpack", "cra", "none"} + q := prompt.SelectInput("build-tool", "Select the build tool (optional)", "", buildTools, "none", false) + if err := prompt.AskOne(q, &inputs.BuildTool); err != nil { + return "", inputs, fmt.Errorf("failed to select build tool: %v", err) + } + } + } + + // Handle API creation inputs. + if inputs.API { + // Prompt for --identifier or --audience if not provided. + if inputs.Identifier == "" && inputs.Audience == "" { + q := prompt.TextInput("identifier", "Enter the API identifier (or audience)", "", "", true) + if err := prompt.AskOne(q, &inputs.Identifier); err != nil { + return "", inputs, fmt.Errorf("failed to enter API identifier: %v", err) + } + } + + // Use --audience as an alias for --identifier if provided. + if inputs.Identifier == "" { + inputs.Identifier = inputs.Audience + } + + // Prompt for --signing-alg if not provided. + if inputs.SigningAlg == "" { + signingAlgs := []string{"RS256", "PS256", "HS256"} + q := prompt.SelectInput("signing-alg", "Select the signing algorithm", "", signingAlgs, "RS256", true) + if err := prompt.AskOne(q, &inputs.SigningAlg); err != nil { + return "", inputs, fmt.Errorf("failed to select signing algorithm: %v", err) + } + } + + // Prompt for --scopes if not provided. + if inputs.Scopes == "" { + q := prompt.TextInput("scopes", "Enter the scopes (comma-separated)", "", "", false) + if err := prompt.AskOne(q, &inputs.Scopes); err != nil { + return "", inputs, fmt.Errorf("failed to enter scopes: %v", err) + } + } + + // Prompt for --token-lifetime if not provided. + if inputs.TokenLifetime == "" { + q := prompt.TextInput("token-lifetime", "Enter the token lifetime (in seconds)", "", "86400", true) + if err := prompt.AskOne(q, &inputs.TokenLifetime); err != nil { + return "", inputs, fmt.Errorf("failed to enter token lifetime: %v", err) + } + } + } + + // Config key is only meaningful when an app is being created. + if !inputs.App { + return "", inputs, nil + } + + // Fallback to "none" if build tool wasn't asked/selected to match the config map keys. + buildToolKey := inputs.BuildTool + if buildToolKey == "" { + buildToolKey = "none" + } + + configKey := fmt.Sprintf("%s:%s:%s", inputs.Type, inputs.Framework, buildToolKey) + return configKey, inputs, nil +} + +func generateClient(input SetupInputs, reqParams auth0.RequestParams) (*management.Client, error) { + // Prompt for name only if not already provided via flag. + if input.Name == "" { + input.Name = "My App" + q := prompt.TextInput("name", "Application Name", input.Name, "", true) + if err := prompt.AskOne(q, &input.Name); err != nil { + return nil, fmt.Errorf("failed to enter application name: %v", err) + } + } + + if input.MetaData == nil { + input.MetaData = map[string]interface{}{ + "created_by": "quickstart-docs-manual-cli", + } + } + + resolved := resolveRequestParams(reqParams, input.Name, input.Port) + + algorithm := "RS256" + oidcConformant := true + client := &management.Client{ + Name: &input.Name, + AppType: &resolved.AppType, + Callbacks: &resolved.Callbacks, + AllowedLogoutURLs: &resolved.AllowedLogoutURLs, + OIDCConformant: &oidcConformant, + JWTConfiguration: &management.ClientJWTConfiguration{ + Algorithm: &algorithm, + }, + ClientMetadata: &input.MetaData, + } + + if len(resolved.WebOrigins) > 0 { + client.WebOrigins = &resolved.WebOrigins + } + + return client, nil +} + +// resolveRequestParams replaces DetectionSub placeholders in RequestParams fields +// with actual values derived from the user inputs. +func resolveRequestParams(reqParams auth0.RequestParams, name string, port int) auth0.RequestParams { + if port == 0 { + port = 3000 + } + baseURL := fmt.Sprintf("http://localhost:%d", port) + + callbacks := make([]string, len(reqParams.Callbacks)) + copy(callbacks, reqParams.Callbacks) + logoutURLs := make([]string, len(reqParams.AllowedLogoutURLs)) + copy(logoutURLs, reqParams.AllowedLogoutURLs) + webOrigins := make([]string, len(reqParams.WebOrigins)) + copy(webOrigins, reqParams.WebOrigins) + + resolvedName := reqParams.Name + if resolvedName == auth0.DetectionSub { + resolvedName = name + } + for i, cb := range callbacks { + if cb == auth0.DetectionSub { + callbacks[i] = baseURL + "/callback" + } + } + for i, u := range logoutURLs { + if u == auth0.DetectionSub { + logoutURLs[i] = baseURL + } + } + for i, u := range webOrigins { + if u == auth0.DetectionSub { + webOrigins[i] = baseURL + } + } + + return auth0.RequestParams{ + AppType: reqParams.AppType, + Callbacks: callbacks, + AllowedLogoutURLs: logoutURLs, + WebOrigins: webOrigins, + Name: resolvedName, + } +} + +func replaceDetectionSub(envValues map[string]string, tenantDomain string, client *management.Client, port int) (map[string]string, error) { + if port == 0 { + port = 3000 + } + baseURL := fmt.Sprintf("http://localhost:%d", port) + + updatedEnvValues := make(map[string]string) + + for key, value := range envValues { + if value != auth0.DetectionSub { + updatedEnvValues[key] = value + continue + } + + switch key { + case "VITE_AUTH0_DOMAIN", "AUTH0_DOMAIN", "domain", "NUXT_AUTH0_DOMAIN", + "auth0.domain", "Auth0:Domain", "auth0:Domain", "auth0_domain", + "EXPO_PUBLIC_AUTH0_DOMAIN": + updatedEnvValues[key] = tenantDomain + + // Express SDK specifically requires the https:// prefix. + case "ISSUER_BASE_URL": + updatedEnvValues[key] = "https://" + tenantDomain + + // Spring Boot okta issuer specifically requires https:// and a trailing slash. + case "okta.oauth2.issuer": + updatedEnvValues[key] = "https://" + tenantDomain + "/" + + case "VITE_AUTH0_CLIENT_ID", "AUTH0_CLIENT_ID", "clientId", "NUXT_AUTH0_CLIENT_ID", + "CLIENT_ID", "auth0.clientId", "okta.oauth2.client-id", "Auth0:ClientId", + "auth0:ClientId", "auth0_client_id", "EXPO_PUBLIC_AUTH0_CLIENT_ID": + updatedEnvValues[key] = client.GetClientID() + + case "AUTH0_CLIENT_SECRET", "NUXT_AUTH0_CLIENT_SECRET", "auth0.clientSecret", + "okta.oauth2.client-secret", "Auth0:ClientSecret", "auth0:ClientSecret", + "auth0_client_secret": + updatedEnvValues[key] = client.GetClientSecret() + + case "AUTH0_SECRET", "NUXT_AUTH0_SESSION_SECRET", "SESSION_SECRET", + "SECRET", "AUTH0_SESSION_ENCRYPTION_KEY", "AUTH0_COOKIE_SECRET": + secret, err := generateState(32) + if err != nil { + return nil, fmt.Errorf("failed to generate secret for %s: %w", key, err) + } + updatedEnvValues[key] = secret + + case "APP_BASE_URL", "NUXT_AUTH0_APP_BASE_URL", "BASE_URL": + updatedEnvValues[key] = baseURL + + case "AUTH0_REDIRECT_URI", "AUTH0_CALLBACK_URL": + updatedEnvValues[key] = baseURL + "/callback" + + default: + updatedEnvValues[key] = value + } + } + + return updatedEnvValues, nil +} + +// buildNestedMap converts a flat map with dot-delimited keys into a nested map. +// e.g. {"okta.oauth2.issuer": "x"} -> {"okta": {"oauth2": {"issuer": "x"}}} +func buildNestedMap(flat map[string]string) map[string]interface{} { + result := make(map[string]interface{}) + for key, value := range flat { + parts := strings.Split(key, ".") + current := result + for i, part := range parts { + if i == len(parts)-1 { + current[part] = value + } else { + if _, exists := current[part]; !exists { + current[part] = make(map[string]interface{}) + } + current = current[part].(map[string]interface{}) + } + } + } + return result +} + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// GenerateAndWriteQuickstartConfig takes the selected stack, resolves the dynamic values, +// and writes them to the appropriate file in the Current Working Directory (CWD). +// It returns the generated file name, the file path, and an error (if any). +func GenerateAndWriteQuickstartConfig(strategy *auth0.FileOutputStrategy, envValues map[string]string, tenantDomain string, client *management.Client, port int) (string, string, error) { + // 1. Resolve the environment variables. + resolvedEnv, err := replaceDetectionSub(envValues, tenantDomain, client, port) + if err != nil { + return "", "", err + } + + // 2. Determine output file path and format. + if strategy == nil { + strategy = &auth0.FileOutputStrategy{Path: ".env", Format: "dotenv"} + } + + // 3. Ensure the directory path exists. + dir := filepath.Dir(strategy.Path) + if dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + return "", "", fmt.Errorf("failed to create directory structure %s: %w", dir, err) + } + } + + // 4. Format the file content based on the target framework's requirement. + var contentBuilder strings.Builder + + switch strategy.Format { + case "dotenv", "properties": + for _, key := range sortedKeys(resolvedEnv) { + contentBuilder.WriteString(fmt.Sprintf("%s=%s\n", key, resolvedEnv[key])) + } + + case "yaml": + // Produce nested YAML from dot-delimited keys (e.g. Spring Boot application.yml). + nested := buildNestedMap(resolvedEnv) + yamlBytes, err := yaml.Marshal(nested) + if err != nil { + return "", "", fmt.Errorf("failed to marshal YAML for %s: %w", strategy.Path, err) + } + contentBuilder.Write(yamlBytes) + + case "ts": + contentBuilder.WriteString("export const environment = {\n") + for _, key := range sortedKeys(resolvedEnv) { + contentBuilder.WriteString(fmt.Sprintf(" %s: '%s',\n", key, resolvedEnv[key])) + } + contentBuilder.WriteString("};\n") + + case "dart": + contentBuilder.WriteString("const Map authConfig = {\n") + for _, key := range sortedKeys(resolvedEnv) { + contentBuilder.WriteString(fmt.Sprintf(" '%s': '%s',\n", key, resolvedEnv[key])) + } + contentBuilder.WriteString("};\n") + + case "json": + // C# appsettings.json expects nested JSON: {"Auth0": {"Domain": "...", "ClientId": "..."}}. + auth0Section := make(map[string]string) + for key, val := range resolvedEnv { + cleanKey := strings.TrimPrefix(key, "Auth0:") + auth0Section[cleanKey] = val + } + jsonBody := map[string]interface{}{"Auth0": auth0Section} + jsonBytes, err := json.MarshalIndent(jsonBody, "", " ") + if err != nil { + return "", "", fmt.Errorf("failed to marshal JSON for %s: %w", strategy.Path, err) + } + contentBuilder.Write(jsonBytes) + + case "xml": + // ASP.NET OWIN Web.config. + contentBuilder.WriteString("\n") + contentBuilder.WriteString("\n") + contentBuilder.WriteString(" \n") + for _, key := range sortedKeys(resolvedEnv) { + contentBuilder.WriteString(fmt.Sprintf(" \n", key, resolvedEnv[key])) + } + contentBuilder.WriteString(" \n") + contentBuilder.WriteString("\n") + } + + // 5. Write the generated content to disk. + if err := os.WriteFile(strategy.Path, []byte(contentBuilder.String()), 0600); err != nil { + return "", "", fmt.Errorf("failed to write config file %s: %w", strategy.Path, err) + } + + // 6. Return the base file name and full path. + fileName := filepath.Base(strategy.Path) + return fileName, strategy.Path, nil +}