Skip to content

Commit 2a1dadd

Browse files
committed
feat-add-signup-to-dex
1 parent e35542e commit 2a1dadd

File tree

15 files changed

+1136
-9
lines changed

15 files changed

+1136
-9
lines changed

README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,106 @@ Please see our [security policy](.github/SECURITY.md) for details about reportin
127127
[issue-1065]: https://github.com/dexidp/dex/issues/1065
128128
[release-notes]: https://github.com/dexidp/dex/releases
129129

130+
## Custom Signup Feature (Fork Addition)
131+
132+
This fork includes a custom user signup feature that allows users to register with email and password.
133+
134+
### Configuration
135+
136+
Enable the signup feature in your `config.yaml`:
137+
138+
```yaml
139+
enablePasswordDB: true # Required
140+
enableSignup: true # Enables signup
141+
```
142+
143+
### API Endpoint
144+
145+
**Create a new user:**
146+
```bash
147+
curl -X POST http://127.0.0.1:5556/dex/signup \
148+
-H "Content-Type: application/json" \
149+
-d '{
150+
"email": "[email protected]",
151+
"username": "username",
152+
"password": "password123"
153+
}'
154+
```
155+
156+
**Response:**
157+
```json
158+
{
159+
"user_id": "generated-id",
160+
"email": "[email protected]",
161+
"username": "username",
162+
"message": "User created successfully"
163+
}
164+
```
165+
166+
### UI Access
167+
168+
- Signup page: `http://127.0.0.1:5556/dex/signup`
169+
- Signup links automatically appear on login pages when enabled
170+
171+
### Validation Rules
172+
173+
- Email: Required, valid format
174+
- Password: Minimum 8 characters
175+
- Username: Required
176+
- Duplicate emails rejected with 409 Conflict
177+
178+
### Security
179+
180+
- Passwords hashed with bcrypt (cost 10)
181+
- Emails stored in lowercase
182+
- Request body size limit (1MB)
183+
- Input validation and sanitization
184+
185+
### Code Organization
186+
187+
The signup feature is modularized to minimize merge conflicts with upstream:
188+
189+
**New Files:**
190+
- `server/handlers_signup.go` - Complete signup implementation
191+
- `web/templates/signup.html` - Signup form template
192+
193+
**Modified Files (minimal changes):**
194+
- `cmd/dex/config.go` - Config field + validation
195+
- `server/server.go` - Field + route registration
196+
- `server/templates.go` - Template rendering
197+
- `web/templates/login.html` & `password.html` - Conditional signup links
198+
199+
### Testing
200+
201+
```bash
202+
# Run signup tests
203+
go test ./server -run TestHandleSignup -v
204+
205+
# Build and start
206+
go build -o bin/dex ./cmd/dex
207+
./bin/dex serve config.dev.yaml
208+
209+
# Test API
210+
curl -X POST http://127.0.0.1:5556/dex/signup \
211+
-H "Content-Type: application/json" \
212+
-d '{"email":"[email protected]","username":"test","password":"password123"}'
213+
214+
# Verify in database
215+
sqlite3 var/sqlite/dex.db "SELECT email, username FROM password;"
216+
```
217+
218+
### Syncing with Upstream
219+
220+
When syncing with upstream dexidp/dex:
221+
222+
1. **No conflict files**: `handlers_signup.go`, `signup.html` (completely custom)
223+
2. **Potential conflicts**: Config, server struct, templates
224+
3. **Resolution**: Re-add the `enableSignup` field and route registration lines
225+
226+
The modular structure isolates ~350 lines of custom code with only ~45 lines of integration changes across 6 existing files.
227+
228+
---
229+
130230
## Development
131231

132232
When all coding and testing is done, please run the test suite:

cmd/dex/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ type Config struct {
5151
// querying the storage. Cannot be specified without enabling a passwords
5252
// database.
5353
StaticPasswords []password `json:"staticPasswords"`
54+
55+
// If enabled, allows users to sign up with email and password via REST API.
56+
// Requires EnablePasswordDB to be true.
57+
EnableSignup bool `json:"enableSignup"`
58+
59+
HiddenConnectors []string `json:"hiddenConnectors"`
5460
}
5561

5662
// Validate the configuration
@@ -62,6 +68,7 @@ func (c Config) Validate() error {
6268
}{
6369
{c.Issuer == "", "no issuer specified in config file"},
6470
{!c.EnablePasswordDB && len(c.StaticPasswords) != 0, "cannot specify static passwords without enabling password db"},
71+
{c.EnableSignup && !c.EnablePasswordDB, "cannot enable signup without enabling password db"},
6572
{c.Storage.Config == nil, "no storage supplied in config file"},
6673
{c.Web.HTTP == "" && c.Web.HTTPS == "", "must supply a HTTP/HTTPS address to listen on"},
6774
{c.Web.HTTPS != "" && c.Web.TLSCert == "", "no cert specified for HTTPS"},

cmd/dex/serve.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ func runServe(options serveOptions) error {
296296
SkipApprovalScreen: c.OAuth2.SkipApprovalScreen,
297297
AlwaysShowLoginScreen: c.OAuth2.AlwaysShowLoginScreen,
298298
PasswordConnector: c.OAuth2.PasswordConnector,
299+
EnableSignup: c.EnableSignup,
299300
Headers: c.Web.Headers.ToHTTPHeader(),
300301
AllowedOrigins: c.Web.AllowedOrigins,
301302
AllowedHeaders: c.Web.AllowedHeaders,

config.dev.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ connectors:
2828

2929
enablePasswordDB: true
3030

31+
# Enable user signup via REST API (POST /signup)
32+
# Requires enablePasswordDB to be true
33+
enableSignup: true
34+
3135
staticPasswords:
3236
- email: "[email protected]"
3337
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"

config.test.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
issuer: http://127.0.0.1:5556/dex
2+
3+
storage:
4+
type: sqlite3
5+
config:
6+
file: var/sqlite/dex.db
7+
8+
web:
9+
http: 127.0.0.1:5556
10+
11+
telemetry:
12+
http: 127.0.0.1:5558
13+
14+
grpc:
15+
addr: 127.0.0.1:5557
16+
17+
staticClients:
18+
- id: example-app
19+
redirectURIs:
20+
- 'http://127.0.0.1:5555/callback'
21+
name: 'Example App'
22+
secret: ZXhhbXBsZS1hcHAtc2VjcmV0
23+
24+
connectors:
25+
- type: mockCallback
26+
id: mock
27+
name: Example
28+
29+
enablePasswordDB: true
30+
31+
# Enable user signup via REST API (POST /signup)
32+
# Requires enablePasswordDB to be true
33+
enableSignup: false
34+
35+
staticPasswords:
36+
- email: "[email protected]"
37+
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
38+
username: "admin"
39+
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"

config.yaml.dist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ web:
131131
# login credentials in Dex's store.
132132
enablePasswordDB: true
133133

134+
# Enable user signup via REST API endpoint (POST /signup).
135+
# This allows users to register with email and password.
136+
# Requires enablePasswordDB to be true.
137+
# enableSignup: false
138+
134139
# If this option isn't chosen users may be added through the gRPC API.
135140
# A static list of passwords for the password connector.
136141
#

server/handlers.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ type discovery struct {
8080
UserInfo string `json:"userinfo_endpoint"`
8181
DeviceEndpoint string `json:"device_authorization_endpoint"`
8282
Introspect string `json:"introspection_endpoint"`
83+
Registration string `json:"registration_endpoint"`
8384
GrantTypes []string `json:"grant_types_supported"`
8485
ResponseTypes []string `json:"response_types_supported"`
8586
Subjects []string `json:"subject_types_supported"`
@@ -114,6 +115,7 @@ func (s *Server) constructDiscovery() discovery {
114115
UserInfo: s.absURL("/userinfo"),
115116
DeviceEndpoint: s.absURL("/device/code"),
116117
Introspect: s.absURL("/token/introspect"),
118+
Registration: s.absURL("/register"),
117119
Subjects: []string{"public"},
118120
IDTokenAlgs: []string{string(jose.RS256)},
119121
CodeChallengeAlgs: []string{codeChallengeMethodS256, codeChallengeMethodPlain},
@@ -190,7 +192,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
190192
}
191193
}
192194

193-
if err := s.templates.login(r, w, connectorInfos); err != nil {
195+
if err := s.templates.login(r, w, connectorInfos, s.enableSignup); err != nil {
194196
s.logger.ErrorContext(r.Context(), "server template error", "err", err)
195197
}
196198
}
@@ -363,7 +365,7 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
363365

364366
switch r.Method {
365367
case http.MethodGet:
366-
if err := s.templates.password(r, w, r.URL.String(), "", usernamePrompt(pwConn), false, backLink); err != nil {
368+
if err := s.templates.password(r, w, r.URL.String(), "", usernamePrompt(pwConn), false, backLink, s.enableSignup); err != nil {
367369
s.logger.ErrorContext(r.Context(), "server template error", "err", err)
368370
}
369371
case http.MethodPost:
@@ -378,7 +380,7 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
378380
return
379381
}
380382
if !ok {
381-
if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(pwConn), true, backLink); err != nil {
383+
if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(pwConn), true, backLink, s.enableSignup); err != nil {
382384
s.logger.ErrorContext(r.Context(), "server template error", "err", err)
383385
}
384386
s.logger.ErrorContext(r.Context(), "failed login attempt: Invalid credentials.", "user", username)

0 commit comments

Comments
 (0)