Skip to content

Commit 891c7b1

Browse files
committed
fix: handle passkey like captcha
1 parent 50614b9 commit 891c7b1

4 files changed

Lines changed: 122 additions & 21 deletions

File tree

pkg/config/auth.go

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -441,17 +441,7 @@ func (a *auth) FromRemoteAuthConfig(remoteConfig v1API.AuthConfigResponse) {
441441
a.MinimumPasswordLength = cast.IntToUint(ValOrDefault(remoteConfig.PasswordMinLength, 0))
442442
prc := ValOrDefault(remoteConfig.PasswordRequiredCharacters, "")
443443
a.PasswordRequirements = NewPasswordRequirement(v1API.UpdateAuthConfigBodyPasswordRequiredCharacters(prc))
444-
passkeyDisplayName := ValOrDefault(remoteConfig.WebauthnRpDisplayName, "")
445-
passkeyId := ValOrDefault(remoteConfig.WebauthnRpId, "")
446-
passkeyOrigins := ValOrDefault(remoteConfig.WebauthnRpOrigins, "")
447-
if remoteConfig.PasskeyEnabled || passkeyDisplayName != "" || passkeyId != "" || passkeyOrigins != "" {
448-
if a.Passkey == nil {
449-
a.Passkey = &passkey{}
450-
}
451-
a.Passkey.fromAuthConfig(remoteConfig)
452-
} else {
453-
a.Passkey = nil
454-
}
444+
a.Passkey.fromAuthConfig(remoteConfig)
455445
a.RateLimit.fromAuthConfig(remoteConfig)
456446
if s := a.Email.Smtp; s != nil && s.Enabled {
457447
a.RateLimit.EmailSent = cast.IntToUint(ValOrDefault(remoteConfig.RateLimitEmailSent, 0))
@@ -512,17 +502,25 @@ func (c *captcha) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
512502
}
513503

514504
func (p passkey) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
515-
body.PasskeyEnabled = cast.Ptr(p.Enabled)
516-
body.WebauthnRpDisplayName = nullable.NewNullableWithValue(p.RpDisplayName)
517-
body.WebauthnRpId = nullable.NewNullableWithValue(p.RpId)
518-
body.WebauthnRpOrigins = nullable.NewNullableWithValue(strings.Join(p.RpOrigins, ","))
505+
if body.PasskeyEnabled = cast.Ptr(p.Enabled); p.Enabled {
506+
body.WebauthnRpDisplayName = nullable.NewNullableWithValue(p.RpDisplayName)
507+
body.WebauthnRpId = nullable.NewNullableWithValue(p.RpId)
508+
body.WebauthnRpOrigins = nullable.NewNullableWithValue(strings.Join(p.RpOrigins, ","))
509+
}
519510
}
520511

521512
func (p *passkey) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
513+
// When local config is not set, we assume platform defaults should not change
514+
if p == nil {
515+
return
516+
}
517+
// Ignore disabled passkey fields to minimise config diff
518+
if p.Enabled {
519+
p.RpDisplayName = ValOrDefault(remoteConfig.WebauthnRpDisplayName, "")
520+
p.RpId = ValOrDefault(remoteConfig.WebauthnRpId, "")
521+
p.RpOrigins = strToArr(ValOrDefault(remoteConfig.WebauthnRpOrigins, ""))
522+
}
522523
p.Enabled = remoteConfig.PasskeyEnabled
523-
p.RpDisplayName = ValOrDefault(remoteConfig.WebauthnRpDisplayName, "")
524-
p.RpId = ValOrDefault(remoteConfig.WebauthnRpId, "")
525-
p.RpOrigins = strToArr(ValOrDefault(remoteConfig.WebauthnRpOrigins, ""))
526524
}
527525

528526
func (h hook) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {

pkg/config/auth_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,33 @@ func TestPasskeyConfigMapping(t *testing.T) {
235235
assert.Equal(t, "http://127.0.0.1:3000,https://localhost:3000", ValOrDefault(body.WebauthnRpOrigins, ""))
236236
})
237237

238+
t.Run("does not serialize rp fields when passkey is disabled", func(t *testing.T) {
239+
c := newWithDefaults()
240+
c.Passkey = &passkey{
241+
Enabled: false,
242+
RpDisplayName: "Supabase CLI",
243+
RpId: "localhost",
244+
RpOrigins: []string{"http://127.0.0.1:3000"},
245+
}
246+
// Run test
247+
body := c.ToUpdateAuthConfigBody()
248+
// Check result
249+
if assert.NotNil(t, body.PasskeyEnabled) {
250+
assert.False(t, *body.PasskeyEnabled)
251+
}
252+
_, err := body.WebauthnRpDisplayName.Get()
253+
assert.Error(t, err)
254+
_, err = body.WebauthnRpId.Get()
255+
assert.Error(t, err)
256+
_, err = body.WebauthnRpOrigins.Get()
257+
assert.Error(t, err)
258+
})
259+
238260
t.Run("hydrates passkey config from remote", func(t *testing.T) {
239261
c := newWithDefaults()
262+
c.Passkey = &passkey{
263+
Enabled: true,
264+
}
240265
// Run test
241266
c.FromRemoteAuthConfig(v1API.AuthConfigResponse{
242267
PasskeyEnabled: true,
@@ -255,6 +280,35 @@ func TestPasskeyConfigMapping(t *testing.T) {
255280
}, c.Passkey.RpOrigins)
256281
}
257282
})
283+
284+
t.Run("ignores remote settings when local passkey config is undefined", func(t *testing.T) {
285+
c := newWithDefaults()
286+
// Run test
287+
c.FromRemoteAuthConfig(v1API.AuthConfigResponse{
288+
PasskeyEnabled: true,
289+
WebauthnRpDisplayName: nullable.NewNullableWithValue("Supabase CLI"),
290+
WebauthnRpId: nullable.NewNullableWithValue("localhost"),
291+
WebauthnRpOrigins: nullable.NewNullableWithValue("http://127.0.0.1:3000"),
292+
})
293+
// Check result
294+
assert.Nil(t, c.Passkey)
295+
})
296+
}
297+
298+
func TestPasskeyDiff(t *testing.T) {
299+
t.Run("ignores undefined config", func(t *testing.T) {
300+
c := newWithDefaults()
301+
// Run test
302+
diff, err := c.DiffWithRemote(v1API.AuthConfigResponse{
303+
PasskeyEnabled: true,
304+
WebauthnRpDisplayName: nullable.NewNullableWithValue("Supabase CLI"),
305+
WebauthnRpId: nullable.NewNullableWithValue("localhost"),
306+
WebauthnRpOrigins: nullable.NewNullableWithValue("http://127.0.0.1:3000"),
307+
})
308+
// Check error
309+
assert.NoError(t, err)
310+
assert.Empty(t, string(diff))
311+
})
258312
}
259313

260314
func TestHookDiff(t *testing.T) {

pkg/config/config.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -922,9 +922,20 @@ func (c *config) Validate(fsys fs.FS) error {
922922
}
923923
}
924924
if c.Auth.Passkey != nil {
925-
for i, origin := range c.Auth.Passkey.RpOrigins {
926-
if err := assertEnvLoaded(origin); err != nil {
927-
return errors.Errorf("Invalid config for auth.passkey.rp_origins[%d]: %v", i, err)
925+
if c.Auth.Passkey.Enabled {
926+
if len(c.Auth.Passkey.RpId) == 0 {
927+
return errors.New("Missing required field in config: auth.passkey.rp_id")
928+
}
929+
if len(c.Auth.Passkey.RpOrigins) == 0 {
930+
return errors.New("Missing required field in config: auth.passkey.rp_origins")
931+
}
932+
if err := assertEnvLoaded(c.Auth.Passkey.RpId); err != nil {
933+
return errors.Errorf("Invalid config for auth.passkey.rp_id: %v", err)
934+
}
935+
for i, origin := range c.Auth.Passkey.RpOrigins {
936+
if err := assertEnvLoaded(origin); err != nil {
937+
return errors.Errorf("Invalid config for auth.passkey.rp_origins[%d]: %v", i, err)
938+
}
928939
}
929940
}
930941
}

pkg/config/config_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,44 @@ rp_origins = ["http://127.0.0.1:3000", "https://localhost:3000"]
104104
}
105105
})
106106

107+
t.Run("passkey enabled requires rp_id", func(t *testing.T) {
108+
config := NewConfig()
109+
fsys := fs.MapFS{
110+
"supabase/config.toml": &fs.MapFile{Data: []byte(`
111+
[auth]
112+
enabled = true
113+
site_url = "http://127.0.0.1:3000"
114+
115+
[auth.passkey]
116+
enabled = true
117+
rp_origins = ["http://127.0.0.1:3000"]
118+
`)},
119+
}
120+
// Run test
121+
err := config.Load("", fsys)
122+
// Check result
123+
assert.ErrorContains(t, err, "Missing required field in config: auth.passkey.rp_id")
124+
})
125+
126+
t.Run("passkey enabled requires rp_origins", func(t *testing.T) {
127+
config := NewConfig()
128+
fsys := fs.MapFS{
129+
"supabase/config.toml": &fs.MapFile{Data: []byte(`
130+
[auth]
131+
enabled = true
132+
site_url = "http://127.0.0.1:3000"
133+
134+
[auth.passkey]
135+
enabled = true
136+
rp_id = "localhost"
137+
`)},
138+
}
139+
// Run test
140+
err := config.Load("", fsys)
141+
// Check result
142+
assert.ErrorContains(t, err, "Missing required field in config: auth.passkey.rp_origins")
143+
})
144+
107145
t.Run("parses experimental pgdelta config", func(t *testing.T) {
108146
config := NewConfig()
109147
fsys := fs.MapFS{

0 commit comments

Comments
 (0)