Skip to content

Commit 2e62989

Browse files
intel352claude
andcommitted
feat(auth): add allowRegistration config for open self-registration
The auth.jwt module previously only allowed the first user to self-register. This adds an allowRegistration config option that, when true, lets any visitor register without admin intervention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 784b11d commit 2e62989

5 files changed

Lines changed: 39 additions & 8 deletions

File tree

module/jwt_auth.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ type JWTAuthModule struct {
3939
users map[string]*User // keyed by email (used when no external userStore)
4040
mu sync.RWMutex
4141
nextID int
42-
app modular.Application
43-
logger modular.Logger
44-
persistence *PersistenceStore // optional write-through backend
45-
userStore *UserStore // optional external user store (from auth.user-store module)
42+
app modular.Application
43+
logger modular.Logger
44+
persistence *PersistenceStore // optional write-through backend
45+
userStore *UserStore // optional external user store (from auth.user-store module)
46+
allowRegistration bool // when true, any visitor may self-register
4647
}
4748

4849
// NewJWTAuthModule creates a new JWT auth module
@@ -76,6 +77,13 @@ func (j *JWTAuthModule) SetResponseFormat(format string) {
7677
j.responseFormat = format
7778
}
7879

80+
// SetAllowRegistration enables or disables open self-registration.
81+
// When true, any visitor may register; when false (default), registration is
82+
// only permitted when no users exist (initial setup mode).
83+
func (j *JWTAuthModule) SetAllowRegistration(allow bool) {
84+
j.allowRegistration = allow
85+
}
86+
7987
// Name returns the module name
8088
func (j *JWTAuthModule) Name() string {
8189
return j.name
@@ -177,9 +185,9 @@ func (j *JWTAuthModule) Handle(w http.ResponseWriter, r *http.Request) {
177185
}
178186

179187
func (j *JWTAuthModule) handleRegister(w http.ResponseWriter, r *http.Request) {
180-
// Self-registration is only allowed when no users exist (initial setup).
181-
// After setup, new users must be created by an admin via the user management API.
182-
if j.userCount() > 0 {
188+
// Self-registration is only allowed when no users exist (initial setup) OR
189+
// when allowRegistration is explicitly enabled.
190+
if !j.allowRegistration && j.userCount() > 0 {
183191
w.WriteHeader(http.StatusForbidden)
184192
_ = json.NewEncoder(w).Encode(map[string]string{"error": "registration is disabled; contact an administrator"})
185193
return

module/jwt_auth_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,24 @@ func TestJWTAuth_Register(t *testing.T) {
8686
}
8787
}
8888

89+
func TestJWTAuth_OpenRegistration(t *testing.T) {
90+
j := setupJWTAuth(t)
91+
j.SetAllowRegistration(true)
92+
93+
// First user registers successfully
94+
registerUser(t, j, "user1@example.com", "User One", "pass1")
95+
96+
// Second user should also succeed when allowRegistration is true
97+
body := `{"email":"user2@example.com","name":"User Two","password":"pass2"}`
98+
req := httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewBufferString(body))
99+
w := httptest.NewRecorder()
100+
j.Handle(w, req)
101+
102+
if w.Code != http.StatusCreated {
103+
t.Errorf("expected status %d (open registration enabled), got %d; body: %s", http.StatusCreated, w.Code, w.Body.String())
104+
}
105+
}
106+
89107
func TestJWTAuth_RegisterDisabledAfterSetup(t *testing.T) {
90108
j := setupJWTAuth(t)
91109

plugins/auth/plugin.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ func (p *Plugin) ModuleFactories() map[string]plugin.ModuleFactory {
8888
if rf, ok := cfg["responseFormat"].(string); ok && rf != "" {
8989
authMod.SetResponseFormat(rf)
9090
}
91+
if ar, ok := cfg["allowRegistration"].(bool); ok && ar {
92+
authMod.SetAllowRegistration(true)
93+
}
9194
return authMod
9295
},
9396
"auth.user-store": func(name string, _ map[string]any) modular.Module {
@@ -201,6 +204,7 @@ func (p *Plugin) ModuleSchemas() []*schema.ModuleSchema {
201204
{Key: "issuer", Label: "Issuer", Type: schema.FieldTypeString, DefaultValue: "workflow", Description: "Token issuer claim", Placeholder: "workflow"},
202205
{Key: "seedFile", Label: "Seed Users File", Type: schema.FieldTypeString, Description: "Path to JSON file with initial user accounts", Placeholder: "data/users.json"},
203206
{Key: "responseFormat", Label: "Response Format", Type: schema.FieldTypeSelect, Options: []string{"standard", "oauth2"}, Description: "Format of authentication response payloads"},
207+
{Key: "allowRegistration", Label: "Allow Open Registration", Type: schema.FieldTypeBool, DefaultValue: false, Description: "When true, any visitor may register without admin intervention"},
204208
},
205209
DefaultConfig: map[string]any{"tokenExpiry": "24h", "issuer": "workflow"},
206210
},

schema/module_schema.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,7 @@ func (r *ModuleSchemaRegistry) registerBuiltins() {
580580
{Key: "issuer", Label: "Issuer", Type: FieldTypeString, DefaultValue: "workflow", Description: "Token issuer claim", Placeholder: "workflow"},
581581
{Key: "seedFile", Label: "Seed Users File", Type: FieldTypeString, Description: "Path to JSON file with initial user accounts", Placeholder: "data/users.json"},
582582
{Key: "responseFormat", Label: "Response Format", Type: FieldTypeSelect, Options: []string{"standard", "oauth2"}, Description: "Format of authentication response payloads"},
583+
{Key: "allowRegistration", Label: "Allow Open Registration", Type: FieldTypeBool, DefaultValue: false, Description: "When true, any visitor may register without admin intervention"},
583584
},
584585
DefaultConfig: map[string]any{"tokenExpiry": "24h", "issuer": "workflow"},
585586
})

schema/module_schema_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func TestModuleSchemaRegistry_ConfigFieldsMatchEngine(t *testing.T) {
6565
{"api.handler", []string{"resourceName", "workflowType", "workflowEngine", "initialTransition", "seedFile", "sourceResourceName", "stateFilter", "fieldMapping", "transitionMap", "summaryFields"}},
6666
{"database.workflow", []string{"driver", "dsn", "maxOpenConns", "maxIdleConns"}},
6767
{"messaging.kafka", []string{"brokers", "groupId"}},
68-
{"auth.jwt", []string{"secret", "tokenExpiry", "issuer", "seedFile", "responseFormat"}},
68+
{"auth.jwt", []string{"secret", "tokenExpiry", "issuer", "seedFile", "responseFormat", "allowRegistration"}},
6969
{"static.fileserver", []string{"root", "prefix", "spaFallback", "cacheMaxAge", "router"}},
7070
{"processing.step", []string{"componentId", "successTransition", "compensateTransition", "maxRetries", "retryBackoffMs", "timeoutSeconds"}},
7171
{"http.middleware.securityheaders", []string{"contentSecurityPolicy", "frameOptions", "contentTypeOptions", "hstsMaxAge", "referrerPolicy", "permissionsPolicy"}},

0 commit comments

Comments
 (0)