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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 24 additions & 17 deletions docs/auth0_quickstarts_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,36 @@ auth0 quickstarts setup [flags]

# API with custom settings:
auth0 quickstarts setup --api --linked-app-id <client-id> --identifier https://my-api --scopes "read:data,write:data"

# API + new app, granting a subset of the API scopes, with a JSON result:
auth0 quickstarts setup --api --app --type regular --framework express --identifier https://my-api --scopes "read:data,write:data" --grant-scopes read:data --json
```


## Flags

```
--api Create an Auth0 API resource server
--app Create an Auth0 application (SPA, regular web, or native)
--audience string Unique URL identifier for the API (audience), e.g. https://my-api
--build-tool string Build tool used by the project (vite, webpack, cra, none) (default "none")
--callback-url string Override the allowed callback URL for the application
--framework string Framework to configure (e.g., react, nextjs, vue, express)
--identifier string Unique URL identifier for the API (audience), e.g. https://my-api
--linked-app-id string [API] Client ID of an existing application to link to the API (skips app creation)
--logout-url string Override the allowed logout URL for the application
--name string Name of the Auth0 application
--offline-access Allow offline access (enables refresh tokens)
--port int Local port the application runs on (default varies by framework, e.g. 3000, 5173)
--scopes string [API] Comma-separated list of permission scopes for the API
--signing-alg string [API] Token signing algorithm: RS256, PS256, or HS256 (leave blank to be prompted interactively)
--token-lifetime string [API] Access token lifetime in seconds (default: 86400 = 24 hours)
--type string Application type: spa, regular, native, or m2m
--web-origin-url string Override the allowed web origin URL for the application
--api Create an Auth0 API resource server
--app Create an Auth0 application (SPA, regular web, or native)
--audience string Unique URL identifier for the API (audience), e.g. https://my-api
--build-tool string Build tool used by the project (composer, gradle, maven, none, vite, webpack) (default "none")
--callback-url strings Override the allowed callback URLs for the application. Comma-separate or repeat the flag for multiple values; replaces the default list.
--force Replace an existing config file without prompting (a backup is written first). In agent mode or with --no-input, an existing .env is merged instead, and any other existing config file is only replaced with --force.
--framework string Framework to configure (e.g., react, nextjs, vue, express)
--grant-scopes strings [API] Comma-separated subset of --scopes to grant the app created with --app or given with --linked-app-id (default: all --scopes). Every value must also be listed in --scopes. An empty value grants no scopes.
--identifier string Unique URL identifier for the API (audience), e.g. https://my-api
--json Output in json format.
--json-compact Output in compact json format.
--linked-app-id string [API] Client ID of an existing application to link to the API (skips app creation)
--logout-url strings Override the allowed logout URLs for the application. Comma-separate or repeat the flag for multiple values; replaces the default list.
--name string Name of the Auth0 application
--offline-access Allow offline access (enables refresh tokens)
--port int Local port the application runs on (default varies by framework, e.g. 3000, 5173)
--scopes string [API] Comma-separated permission scopes the API defines (e.g. read:data,write:data). The app created with --app or given with --linked-app-id is granted all of them unless --grant-scopes is set.
--signing-alg string [API] Token signing algorithm: RS256, PS256, or HS256 (leave blank to be prompted interactively)
--token-lifetime string [API] Access token lifetime in seconds (default: 86400 = 24 hours)
--type string Application type: spa, regular, native, or m2m
--web-origin-url strings Override the allowed web origin URLs for the application. Comma-separate or repeat the flag for multiple values; replaces the default list.
```


Expand Down
98 changes: 98 additions & 0 deletions internal/auth0/quickstart.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,26 @@ func FrameworkBuildToolRequired(typeFramework string) bool {
return ok
}

// SupportedBuildTools returns the sorted, de-duplicated build-tool segment
// ("type:framework:<tool>") of every key in QuickstartConfigs, including "none".
func SupportedBuildTools() []string {
seen := make(map[string]struct{})
for k := range QuickstartConfigs {
parts := strings.SplitN(k, ":", 3)
if len(parts) != 3 {
continue
}
seen[parts[2]] = struct{}{}
}

tools := make([]string, 0, len(seen))
for tool := range seen {
tools = append(tools, tool)
}
sort.Strings(tools)
return tools
}

// FrameworkBundleIDRequired lists native frameworks that need a resolved
// bundle/application ID (for example, "com.example.myapp") during project
// detection. Keep in sync with DetectProject BundleID assignments.
Expand Down Expand Up @@ -376,6 +396,36 @@ var QuickstartConfigs = map[string]AppConfig{
Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"},
AudienceVar: "VITE_AUTH0_AUDIENCE",
},
"spa:react:webpack": {
EnvValues: map[string]string{
"AUTH0_DOMAIN": DetectionSub,
"AUTH0_CLIENT_ID": DetectionSub,
},
RequestParams: RequestParams{
AppType: "spa",
Callbacks: []string{DetectionSubAsBase},
AllowedLogoutURLs: []string{DetectionSub},
WebOrigins: []string{DetectionSub},
Name: DetectionSub,
},
Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"},
AudienceVar: "AUTH0_AUDIENCE",
},
"spa:vanilla-javascript:webpack": {
EnvValues: map[string]string{
"AUTH0_DOMAIN": DetectionSub,
"AUTH0_CLIENT_ID": DetectionSub,
},
RequestParams: RequestParams{
AppType: "spa",
Callbacks: []string{DetectionSubAsBase},
AllowedLogoutURLs: []string{DetectionSub},
WebOrigins: []string{DetectionSub},
Name: DetectionSub,
},
Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"},
AudienceVar: "AUTH0_AUDIENCE",
},
"spa:flutter-web:none": {
EnvValues: map[string]string{
"domain": DetectionSub,
Expand Down Expand Up @@ -410,6 +460,24 @@ var QuickstartConfigs = map[string]AppConfig{
Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"},
AudienceVar: "AUTH0_AUDIENCE",
},
"regular:tanstack-start: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{DetectionSub},
AllowedLogoutURLs: []string{DetectionSub},
Name: DetectionSub,
CallbackPath: "/auth/callback",
},
Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"},
AudienceVar: "AUTH0_AUDIENCE",
},
"regular:nuxt:none": {
EnvValues: map[string]string{
"NUXT_AUTH0_DOMAIN": DetectionSub,
Expand Down Expand Up @@ -618,6 +686,36 @@ var QuickstartConfigs = map[string]AppConfig{
},
Strategy: FileOutputStrategy{Path: "src/main/resources/application.yml", Format: "yaml"},
},
"regular:java-mvc:maven": {
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,
CallbackPath: "/callback",
},
Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"},
},
"regular:java-mvc:gradle": {
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,
CallbackPath: "/callback",
},
Strategy: FileOutputStrategy{Path: ".env", Format: "dotenv"},
},
"regular:aspnet-mvc:none": {
EnvValues: map[string]string{
"Auth0:Domain": DetectionSub,
Expand Down
106 changes: 106 additions & 0 deletions internal/auth0/quickstarts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package auth0

import (
"fmt"
"sort"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var mockQuickStarts = Quickstarts{
Expand Down Expand Up @@ -71,3 +73,107 @@ func TestFindByStack(t *testing.T) {
assert.Equal(t, fmt.Sprintf("failed to find any quickstarts for stack: %q", "some-non-existent-qs-type"), err.Error())
})
}

func TestSupportedBuildTools(t *testing.T) {
got := SupportedBuildTools()
assert.Equal(t, []string{"composer", "gradle", "maven", "none", "vite", "webpack"}, got)
assert.True(t, sort.StringsAreSorted(got))
assert.NotContains(t, got, "cra")

// Every returned tool is the third segment of at least one config key.
for _, tool := range got {
found := false
for key := range QuickstartConfigs {
if len(key) > len(tool) && key[len(key)-len(tool)-1:] == ":"+tool {
found = true
break
}
}
assert.True(t, found, "build tool %q is not used by any QuickstartConfigs key", tool)
}
}

func TestFrameworkSupportedBuildTools_WebpackVariants(t *testing.T) {
assert.Equal(t, []string{"vite", "webpack"}, FrameworkSupportedBuildTools["spa:react"])
assert.Equal(t, []string{"vite", "webpack"}, FrameworkSupportedBuildTools["spa:vanilla-javascript"])
assert.Equal(t, []string{"vite", "webpack"}, FrameworkBuildTools["spa:react"], "react has no :none variant")
assert.True(t, FrameworkBuildToolRequired("spa:react"))
assert.False(t, FrameworkBuildToolRequired("regular:tanstack-start"))
assert.Equal(t, []string{"none"}, FrameworkBuildTools["regular:tanstack-start"])
}

func TestJavaMVCQuickstartConfigs(t *testing.T) {
assert.Equal(t, []string{"gradle", "maven"}, FrameworkBuildTools["regular:java-mvc"])
for _, key := range []string{"regular:java-mvc:maven", "regular:java-mvc:gradle"} {
t.Run(key, func(t *testing.T) {
cfg, ok := QuickstartConfigs[key]
require.True(t, ok, "missing config %q", key)
assert.Equal(t, FileOutputStrategy{Path: ".env", Format: "dotenv"}, cfg.Strategy)
assert.Equal(t, "regular_web", cfg.RequestParams.AppType)
assert.Equal(t, "/callback", cfg.RequestParams.CallbackPath)
assert.Empty(t, cfg.RequestParams.WebOrigins)
keys := make([]string, 0, len(cfg.EnvValues))
for k := range cfg.EnvValues {
keys = append(keys, k)
}
sort.Strings(keys)
assert.Equal(t, []string{"AUTH0_CLIENT_ID", "AUTH0_CLIENT_SECRET", "AUTH0_DOMAIN"}, keys)
})
}
}

func TestNewQuickstartConfigs(t *testing.T) {
tests := []struct {
key string
wantAppType string
wantCallbackPath string
wantWebOrigins bool
wantEnvKeys []string
}{
{"spa:react:webpack", "spa", "", true, []string{"AUTH0_CLIENT_ID", "AUTH0_DOMAIN"}},
{"spa:vanilla-javascript:webpack", "spa", "", true, []string{"AUTH0_CLIENT_ID", "AUTH0_DOMAIN"}},
{"regular:tanstack-start:none", "regular_web", "/auth/callback", false,
[]string{"APP_BASE_URL", "AUTH0_CLIENT_ID", "AUTH0_CLIENT_SECRET", "AUTH0_DOMAIN", "AUTH0_SECRET"}},
}

for _, tc := range tests {
t.Run(tc.key, func(t *testing.T) {
cfg, ok := QuickstartConfigs[tc.key]
require.True(t, ok, "missing config %q", tc.key)

assert.Equal(t, FileOutputStrategy{Path: ".env", Format: "dotenv"}, cfg.Strategy)
assert.Equal(t, "AUTH0_AUDIENCE", cfg.AudienceVar)
assert.Equal(t, tc.wantAppType, cfg.RequestParams.AppType)
assert.Equal(t, tc.wantCallbackPath, cfg.RequestParams.CallbackPath)
assert.Equal(t, DetectionSub, cfg.RequestParams.Name)
assert.Equal(t, []string{DetectionSub}, cfg.RequestParams.AllowedLogoutURLs)
if tc.wantWebOrigins {
assert.Equal(t, []string{DetectionSubAsBase}, cfg.RequestParams.Callbacks, "SPA callback is the bare base URL")
assert.Equal(t, []string{DetectionSub}, cfg.RequestParams.WebOrigins)
} else {
assert.Equal(t, []string{DetectionSub}, cfg.RequestParams.Callbacks)
assert.Empty(t, cfg.RequestParams.WebOrigins)
}

keys := make([]string, 0, len(cfg.EnvValues))
for k, v := range cfg.EnvValues {
keys = append(keys, k)
assert.Equal(t, DetectionSub, v, "env %q must be resolved at setup time", k)
}
sort.Strings(keys)
assert.Equal(t, tc.wantEnvKeys, keys)
})
}

t.Run("webpack configs mirror their vite counterparts' RequestParams", func(t *testing.T) {
assert.Equal(t, QuickstartConfigs["spa:react:vite"].RequestParams, QuickstartConfigs["spa:react:webpack"].RequestParams)
assert.Equal(t, QuickstartConfigs["spa:vanilla-javascript:vite"].RequestParams, QuickstartConfigs["spa:vanilla-javascript:webpack"].RequestParams)
})

t.Run("out-of-scope configs are absent", func(t *testing.T) {
for _, key := range []string{"spa:react:cra", "regular:java-mvc:none"} {
_, ok := QuickstartConfigs[key]
assert.False(t, ok, "%q must not exist", key)
}
})
}
62 changes: 53 additions & 9 deletions internal/cli/quickstart_detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
type DetectionResult struct {
Framework string
Type string // "spa" | "regular" | "native".
BuildTool string // "vite" | "maven" | "gradle" | "composer" | "" (NA).
BuildTool string // "vite" | "webpack" | "maven" | "gradle" | "composer" | "" (NA).
BundleID string // Package/bundle ID for native apps (e.g. "com.example.myapp"); empty if not found.
Detected bool // True if any signal file matched.
AmbiguousFrameworks []string // Set when >1 package.json dep matched.
Expand Down Expand Up @@ -129,6 +129,14 @@ func DetectProject(dir string) DetectionResult {
return result
}

// -- 7b. TanStack Start (@tanstack/react-start dep) — Before vite.config (TanStack Start uses Vite + react).
if hasDep(earlyDeps, "@tanstack/react-start") {
result.Framework = "tanstack-start"
result.Type = "regular"
result.Detected = true
return result
}

// -- 8. Vite.config.[ts|js] + package.json deps --.
if fileExistsAny(dir, "vite.config.ts", "vite.config.js") {
result.Type = "spa"
Expand Down Expand Up @@ -284,9 +292,35 @@ func DetectProject(dir string) DetectionResult {
}
}

// -- 20. Webpack.config.[js|ts|cjs|mjs] — last, because many non-SPA projects ship a webpack config.
if !result.Detected && fileExistsAny(dir, "webpack.config.js", "webpack.config.ts", "webpack.config.cjs", "webpack.config.mjs") {
if framework, ok := detectWebpackFramework(earlyDeps); ok {
result.Framework = framework
result.Type = "spa"
result.BuildTool = "webpack"
result.Detected = true
}
}

return result
}

// detectWebpackFramework maps client-side dependencies to a webpack SPA framework. A webpack
// config alone is not enough: other frameworks (Vue, Svelte, Angular) have no webpack quickstart,
// and server bundles also use webpack.
func detectWebpackFramework(deps map[string]bool) (string, bool) {
switch {
case hasDep(deps, "react"):
return "react", true
case hasDep(deps, "vue"), hasDep(deps, "svelte"), hasDep(deps, "@angular/core"):
return "", false
case hasDep(deps, "webpack-dev-server"):
return "vanilla-javascript", true
default:
return "", false
}
}

// collectPackageJSONCandidates returns framework candidates from package.json deps.
// Ionic deps are handled earlier in DetectProject (step 2) and excluded here.
func collectPackageJSONCandidates(deps map[string]bool) []detectionCandidate {
Expand Down Expand Up @@ -334,21 +368,31 @@ func detectFromCsproj(content string) (framework, qsType string, found bool) {
func detectJavaFramework(content string) string {
lower := strings.ToLower(content)
switch {
case strings.Contains(lower, "spring-boot") ||
strings.Contains(lower, "springframework.boot"):
case strings.Contains(lower, "mvc-auth-commons"):
return "java-mvc"
case containsAnyOf(lower, "spring-boot", "springframework.boot"):
return "spring-boot"
case strings.Contains(lower, "javax.ee") ||
strings.Contains(lower, "jakarta.ee") ||
strings.Contains(lower, "javax.servlet") ||
strings.Contains(lower, "jakarta.servlet") ||
// Jakarta.platform:jakarta.jakartaee-api is the standard BOM for Jakarta EE 9+.
strings.Contains(lower, "jakarta.platform"):
// Full Java EE / Jakarta EE platform or EE-only APIs (the Jakarta EE 9+ BOM is
// jakarta.platform:jakarta.jakartaee-api).
case containsAnyOf(lower, "javaee-api", "jakartaee-api", "jakarta.platform", "javax.ee", "jakarta.ee",
"security.enterprise", "microprofile"):
return "java-ee"
case containsAnyOf(lower, "javax.servlet", "jakarta.servlet"):
return "java-mvc"
default:
return "vanilla-java"
}
}

func containsAnyOf(s string, substrings ...string) bool {
for _, sub := range substrings {
if strings.Contains(s, sub) {
return true
}
}
return false
}

func isExpoProject(dir string) bool {
data, err := os.ReadFile(filepath.Join(dir, "app.json"))
if err != nil {
Expand Down
Loading
Loading