Skip to content

Commit 159381e

Browse files
committed
fix JWK endpoints returning empty keys
1 parent 9b9b5a2 commit 159381e

6 files changed

Lines changed: 166 additions & 12 deletions

File tree

internal/gen/bearerjwt/bearerjwt_test.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,17 @@ func TestGenerateToken(t *testing.T) {
9696
})
9797

9898
t.Run("accepts signing key from stdin", func(t *testing.T) {
99-
utils.Config.Auth.SigningKeysPath = ""
100-
utils.Config.Auth.SigningKeys = nil
10199
claims := config.CustomClaims{
102100
Role: "service_role",
103101
}
104-
// Setup in-memory fs
102+
// Setup in-memory fs with minimal config (explicitly set signing_keys_path to empty to override template default)
105103
fsys := afero.NewMemMapFs()
104+
require.NoError(t, utils.WriteFile("supabase/config.toml", []byte(`
105+
project_id = "test"
106+
107+
[auth]
108+
signing_keys_path = ""
109+
`), fsys))
106110
testKey, err := json.Marshal(privateKeyRSA)
107111
require.NoError(t, err)
108112
t.Cleanup(fstest.MockStdin(t, string(testKey)))
@@ -128,8 +132,14 @@ func TestGenerateToken(t *testing.T) {
128132

129133
t.Run("throws error on invalid key", func(t *testing.T) {
130134
claims := jwt.MapClaims{}
131-
// Setup in-memory fs
135+
// Setup in-memory fs with minimal config (explicitly set signing_keys_path to empty to override template default)
132136
fsys := afero.NewMemMapFs()
137+
require.NoError(t, utils.WriteFile("supabase/config.toml", []byte(`
138+
project_id = "test"
139+
140+
[auth]
141+
signing_keys_path = ""
142+
`), fsys))
133143
t.Cleanup(fstest.MockStdin(t, ""))
134144
// Run test
135145
err = Run(context.Background(), claims, io.Discard, fsys)

internal/init/init.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import (
1212

1313
"github.com/go-errors/errors"
1414
"github.com/spf13/afero"
15+
"github.com/supabase/cli/internal/gen/signingkeys"
1516
"github.com/supabase/cli/internal/utils"
17+
"github.com/supabase/cli/pkg/config"
1618
"github.com/tidwall/jsonc"
1719
)
1820

@@ -34,22 +36,27 @@ var (
3436
)
3537

3638
func Run(ctx context.Context, fsys afero.Fs, interactive bool, params utils.InitParams) error {
37-
// 1. Write `config.toml`.
39+
// 1. Generate default signing key if it doesn't exist.
40+
if err := generateDefaultSigningKey(fsys); err != nil {
41+
fmt.Fprintln(os.Stderr, utils.Yellow("Warning:"), "Failed to generate signing key:", err)
42+
}
43+
44+
// 2. Write `config.toml`.
3845
if err := utils.InitConfig(params, fsys); err != nil {
3946
if errors.Is(err, os.ErrExist) {
4047
utils.CmdSuggestion = fmt.Sprintf("Run %s to overwrite existing config file.", utils.Aqua("supabase init --force"))
4148
}
4249
return err
4350
}
4451

45-
// 2. Append to `.gitignore`.
52+
// 3. Append to `.gitignore`.
4653
if utils.IsGitRepo() {
4754
if err := updateGitIgnore(utils.GitIgnorePath, fsys); err != nil {
4855
return err
4956
}
5057
}
5158

52-
// 3. Prompt for IDE settings in interactive mode.
59+
// 4. Prompt for IDE settings in interactive mode.
5360
if interactive {
5461
if err := PromptForIDESettings(ctx, fsys); err != nil {
5562
return err
@@ -170,3 +177,39 @@ func WriteIntelliJConfig(fsys afero.Fs) error {
170177
fmt.Println("Please install the Deno plugin for IntelliJ: " + utils.Bold("https://plugins.jetbrains.com/plugin/14382-deno"))
171178
return nil
172179
}
180+
181+
func generateDefaultSigningKey(fsys afero.Fs) error {
182+
signingKeysPath := filepath.Join(utils.SupabaseDirPath, "signing_keys.json")
183+
184+
exists, err := afero.Exists(fsys, signingKeysPath)
185+
if err != nil {
186+
return errors.Errorf("failed to check signing key file: %w", err)
187+
}
188+
if exists {
189+
return nil
190+
}
191+
192+
privateJWK, err := signingkeys.GeneratePrivateKey(config.AlgRS256)
193+
if err != nil {
194+
return errors.Errorf("failed to generate signing key: %w", err)
195+
}
196+
197+
if err := utils.MkdirIfNotExistFS(fsys, utils.SupabaseDirPath); err != nil {
198+
return errors.Errorf("failed to create supabase directory: %w", err)
199+
}
200+
201+
jwkArray := []config.JWK{*privateJWK}
202+
f, err := fsys.OpenFile(signingKeysPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
203+
if err != nil {
204+
return errors.Errorf("failed to create signing key file: %w", err)
205+
}
206+
defer f.Close()
207+
208+
enc := json.NewEncoder(f)
209+
enc.SetIndent("", " ")
210+
if err := enc.Encode(jwkArray); err != nil {
211+
return errors.Errorf("failed to encode signing key: %w", err)
212+
}
213+
214+
return nil
215+
}

internal/init/init_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import (
44
"context"
55
"encoding/json"
66
"os"
7+
"path/filepath"
78
"testing"
89

910
"github.com/spf13/afero"
1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
1213
"github.com/supabase/cli/internal/testing/fstest"
1314
"github.com/supabase/cli/internal/utils"
15+
"github.com/supabase/cli/pkg/config"
1416
)
1517

1618
func TestInitCommand(t *testing.T) {
@@ -24,6 +26,11 @@ func TestInitCommand(t *testing.T) {
2426
exists, err := afero.Exists(fsys, utils.ConfigPath)
2527
assert.NoError(t, err)
2628
assert.True(t, exists)
29+
// Validate generated signing key
30+
signingKeysPath := filepath.Join(utils.SupabaseDirPath, "signing_keys.json")
31+
exists, err = afero.Exists(fsys, signingKeysPath)
32+
assert.NoError(t, err)
33+
assert.True(t, exists)
2734
// Validate generated .gitignore
2835
exists, err = afero.Exists(fsys, utils.GitIgnorePath)
2936
assert.NoError(t, err)
@@ -197,3 +204,78 @@ func TestUpdateJsonFile(t *testing.T) {
197204
assert.ErrorContains(t, err, "operation not permitted")
198205
})
199206
}
207+
208+
func TestGenerateDefaultSigningKey(t *testing.T) {
209+
signingKeysPath := filepath.Join(utils.SupabaseDirPath, "signing_keys.json")
210+
211+
t.Run("generates signing key when file doesn't exist", func(t *testing.T) {
212+
// Setup in-memory fs
213+
fsys := afero.NewMemMapFs()
214+
// Run test
215+
assert.NoError(t, generateDefaultSigningKey(fsys))
216+
// Validate file exists
217+
exists, err := afero.Exists(fsys, signingKeysPath)
218+
assert.NoError(t, err)
219+
assert.True(t, exists)
220+
// Validate file contents
221+
content, err := afero.ReadFile(fsys, signingKeysPath)
222+
assert.NoError(t, err)
223+
var jwkArray []config.JWK
224+
assert.NoError(t, json.Unmarshal(content, &jwkArray))
225+
assert.Len(t, jwkArray, 1)
226+
// Validate key structure
227+
key := jwkArray[0]
228+
assert.Equal(t, "RSA", key.KeyType)
229+
assert.Equal(t, config.Algorithm("RS256"), key.Algorithm)
230+
assert.NotEmpty(t, key.KeyID)
231+
assert.NotEmpty(t, key.Modulus)
232+
assert.NotEmpty(t, key.Exponent)
233+
assert.NotEmpty(t, key.PrivateExponent)
234+
})
235+
236+
t.Run("skips generation when file already exists", func(t *testing.T) {
237+
// Setup in-memory fs with existing key file
238+
fsys := afero.NewMemMapFs()
239+
existingKey := []config.JWK{
240+
{
241+
KeyType: "RSA",
242+
KeyID: "existing-key-id",
243+
Algorithm: config.AlgRS256,
244+
},
245+
}
246+
existingContent, err := json.Marshal(existingKey)
247+
require.NoError(t, err)
248+
require.NoError(t, utils.MkdirIfNotExistFS(fsys, utils.SupabaseDirPath))
249+
require.NoError(t, afero.WriteFile(fsys, signingKeysPath, existingContent, 0600))
250+
// Run test
251+
assert.NoError(t, generateDefaultSigningKey(fsys))
252+
// Validate file wasn't modified
253+
content, err := afero.ReadFile(fsys, signingKeysPath)
254+
assert.NoError(t, err)
255+
var jwkArray []config.JWK
256+
assert.NoError(t, json.Unmarshal(content, &jwkArray))
257+
assert.Len(t, jwkArray, 1)
258+
assert.Equal(t, "existing-key-id", jwkArray[0].KeyID)
259+
})
260+
261+
t.Run("throws error on failure to create directory", func(t *testing.T) {
262+
// Setup read-only fs
263+
fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
264+
// Run test
265+
err := generateDefaultSigningKey(fsys)
266+
// Check error
267+
assert.Error(t, err)
268+
assert.ErrorContains(t, err, "failed to create supabase directory")
269+
})
270+
271+
t.Run("throws error on failure to create file", func(t *testing.T) {
272+
// Setup fs that denies file creation
273+
// OpenErrorFs will fail when trying to open/create the file
274+
fsys := &fstest.OpenErrorFs{DenyPath: signingKeysPath}
275+
// Run test
276+
err := generateDefaultSigningKey(fsys)
277+
// Check error - OpenErrorFs will fail on OpenFile call
278+
assert.Error(t, err)
279+
assert.ErrorContains(t, err, "failed to create signing key file")
280+
})
281+
}

internal/init/templates/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Supabase
22
.branches
33
.temp
4+
signing_keys.json
45

56
# dotenvx
67
.env.keys

pkg/config/config.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -882,10 +882,8 @@ func (c *config) Validate(fsys fs.FS) error {
882882
}
883883
}
884884
if len(c.Auth.SigningKeysPath) > 0 {
885-
if f, err := fsys.Open(c.Auth.SigningKeysPath); err != nil {
886-
return errors.Errorf("failed to read signing keys: %w", err)
887-
} else if c.Auth.SigningKeys, err = fetcher.ParseJSON[[]JWK](f); err != nil {
888-
return errors.Errorf("failed to decode signing keys: %w", err)
885+
if err := c.loadSigningKeys(fsys); err != nil {
886+
return err
889887
}
890888
}
891889
if err := c.Auth.Hook.validate(); err != nil {
@@ -946,6 +944,26 @@ func (c *config) Validate(fsys fs.FS) error {
946944
return nil
947945
}
948946

947+
func (c *config) loadSigningKeys(fsys fs.FS) error {
948+
f, err := fsys.Open(c.Auth.SigningKeysPath)
949+
if err != nil {
950+
if errors.Is(err, os.ErrNotExist) {
951+
fmt.Fprintf(os.Stderr, "WARN: signing keys file not found: %s - will be created during init\n", c.Auth.SigningKeysPath)
952+
return nil
953+
}
954+
return fmt.Errorf("failed to read signing keys: %w", err)
955+
}
956+
defer f.Close()
957+
958+
signingKeys, err := fetcher.ParseJSON[[]JWK](f)
959+
if err != nil {
960+
return fmt.Errorf("failed to decode signing keys: %w", err)
961+
}
962+
963+
c.Auth.SigningKeys = signingKeys
964+
return nil
965+
}
966+
949967
func assertEnvLoaded(s string) error {
950968
if matches := envPattern.FindStringSubmatch(s); len(matches) > 1 {
951969
fmt.Fprintln(os.Stderr, "WARN: environment variable is unset:", matches[1])

pkg/config/templates/config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ jwt_expiry = 3600
153153
# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:<port>/auth/v1).
154154
# jwt_issuer = ""
155155
# Path to JWT signing key. DO NOT commit your signing keys file to git.
156-
# signing_keys_path = "./signing_keys.json"
156+
signing_keys_path = "./signing_keys.json"
157157
# If disabled, the refresh token will never expire.
158158
enable_refresh_token_rotation = true
159159
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.

0 commit comments

Comments
 (0)