Skip to content

Commit a29d61d

Browse files
authored
feat: generate jwt tokens from signing key (#3969)
2 parents b3d509d + cd013f5 commit a29d61d

File tree

7 files changed

+382
-135
lines changed

7 files changed

+382
-135
lines changed

cmd/gen.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/supabase/cli/internal/gen/types"
1414
"github.com/supabase/cli/internal/utils"
1515
"github.com/supabase/cli/internal/utils/flags"
16+
"github.com/supabase/cli/pkg/config"
1617
)
1718

1819
var (
@@ -97,7 +98,7 @@ var (
9798

9899
algorithm = utils.EnumFlag{
99100
Allowed: signingkeys.GetSupportedAlgorithms(),
100-
Value: string(signingkeys.AlgES256),
101+
Value: string(config.AlgES256),
101102
}
102103
appendKeys bool
103104

internal/gen/signingkeys/signingkeys.go

Lines changed: 16 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -20,58 +20,24 @@ import (
2020
"github.com/supabase/cli/internal/utils"
2121
"github.com/supabase/cli/internal/utils/flags"
2222
"github.com/supabase/cli/pkg/cast"
23+
"github.com/supabase/cli/pkg/config"
2324
)
2425

25-
type Algorithm string
26-
27-
const (
28-
AlgRS256 Algorithm = "RS256"
29-
AlgES256 Algorithm = "ES256"
30-
)
31-
32-
type JWK struct {
33-
KeyType string `json:"kty"`
34-
KeyID string `json:"kid,omitempty"`
35-
Use string `json:"use,omitempty"`
36-
KeyOps []string `json:"key_ops,omitempty"`
37-
Algorithm string `json:"alg,omitempty"`
38-
Extractable *bool `json:"ext,omitempty"`
39-
// RSA specific fields
40-
Modulus string `json:"n,omitempty"`
41-
Exponent string `json:"e,omitempty"`
42-
// RSA private key fields
43-
PrivateExponent string `json:"d,omitempty"`
44-
FirstPrimeFactor string `json:"p,omitempty"`
45-
SecondPrimeFactor string `json:"q,omitempty"`
46-
FirstFactorCRTExponent string `json:"dp,omitempty"`
47-
SecondFactorCRTExponent string `json:"dq,omitempty"`
48-
FirstCRTCoefficient string `json:"qi,omitempty"`
49-
// EC specific fields
50-
Curve string `json:"crv,omitempty"`
51-
X string `json:"x,omitempty"`
52-
Y string `json:"y,omitempty"`
53-
}
54-
55-
type KeyPair struct {
56-
PublicKey JWK
57-
PrivateKey JWK
58-
}
59-
60-
// GenerateKeyPair generates a new key pair for the specified algorithm
61-
func GenerateKeyPair(alg Algorithm) (*KeyPair, error) {
62-
keyID := uuid.New().String()
26+
// GeneratePrivateKey generates a new private key for the specified algorithm
27+
func GeneratePrivateKey(alg config.Algorithm) (*config.JWK, error) {
28+
keyID := uuid.New()
6329

6430
switch alg {
65-
case AlgRS256:
31+
case config.AlgRS256:
6632
return generateRSAKeyPair(keyID)
67-
case AlgES256:
33+
case config.AlgES256:
6834
return generateECDSAKeyPair(keyID)
6935
default:
7036
return nil, errors.Errorf("unsupported algorithm: %s", alg)
7137
}
7238
}
7339

74-
func generateRSAKeyPair(keyID string) (*KeyPair, error) {
40+
func generateRSAKeyPair(keyID uuid.UUID) (*config.JWK, error) {
7541
// Generate RSA key pair (2048 bits for RS256)
7642
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
7743
if err != nil {
@@ -84,7 +50,7 @@ func generateRSAKeyPair(keyID string) (*KeyPair, error) {
8450
privateKey.Precompute()
8551

8652
// Convert to JWK format
87-
privateJWK := JWK{
53+
privateJWK := config.JWK{
8854
KeyType: "RSA",
8955
KeyID: keyID,
9056
Use: "sig",
@@ -101,24 +67,10 @@ func generateRSAKeyPair(keyID string) (*KeyPair, error) {
10167
FirstCRTCoefficient: base64.RawURLEncoding.EncodeToString(privateKey.Precomputed.Qinv.Bytes()),
10268
}
10369

104-
publicJWK := JWK{
105-
KeyType: "RSA",
106-
KeyID: keyID,
107-
Use: "sig",
108-
KeyOps: []string{"verify"},
109-
Algorithm: "RS256",
110-
Extractable: cast.Ptr(true),
111-
Modulus: privateJWK.Modulus,
112-
Exponent: privateJWK.Exponent,
113-
}
114-
115-
return &KeyPair{
116-
PublicKey: publicJWK,
117-
PrivateKey: privateJWK,
118-
}, nil
70+
return &privateJWK, nil
11971
}
12072

121-
func generateECDSAKeyPair(keyID string) (*KeyPair, error) {
73+
func generateECDSAKeyPair(keyID uuid.UUID) (*config.JWK, error) {
12274
// Generate ECDSA key pair (P-256 curve for ES256)
12375
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
12476
if err != nil {
@@ -128,7 +80,7 @@ func generateECDSAKeyPair(keyID string) (*KeyPair, error) {
12880
publicKey := &privateKey.PublicKey
12981

13082
// Convert to JWK format
131-
privateJWK := JWK{
83+
privateJWK := config.JWK{
13284
KeyType: "EC",
13385
KeyID: keyID,
13486
Use: "sig",
@@ -141,22 +93,7 @@ func generateECDSAKeyPair(keyID string) (*KeyPair, error) {
14193
PrivateExponent: base64.RawURLEncoding.EncodeToString(privateKey.D.Bytes()),
14294
}
14395

144-
publicJWK := JWK{
145-
KeyType: "EC",
146-
KeyID: keyID,
147-
Use: "sig",
148-
KeyOps: []string{"verify"},
149-
Algorithm: "ES256",
150-
Extractable: cast.Ptr(true),
151-
Curve: "P-256",
152-
X: privateJWK.X,
153-
Y: privateJWK.Y,
154-
}
155-
156-
return &KeyPair{
157-
PublicKey: publicJWK,
158-
PrivateKey: privateJWK,
159-
}, nil
96+
return &privateJWK, nil
16097
}
16198

16299
// Run generates a key pair and writes it to the specified file path
@@ -168,13 +105,13 @@ func Run(ctx context.Context, algorithm string, appendMode bool, fsys afero.Fs)
168105
outputPath := utils.Config.Auth.SigningKeysPath
169106

170107
// Generate key pair
171-
keyPair, err := GenerateKeyPair(Algorithm(algorithm))
108+
privateJWK, err := GeneratePrivateKey(config.Algorithm(algorithm))
172109
if err != nil {
173110
return err
174111
}
175112

176113
out := io.Writer(os.Stdout)
177-
var jwkArray []JWK
114+
var jwkArray []config.JWK
178115
if len(outputPath) > 0 {
179116
if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(outputPath)); err != nil {
180117
return err
@@ -210,7 +147,7 @@ func Run(ctx context.Context, algorithm string, appendMode bool, fsys afero.Fs)
210147
}
211148
out = f
212149
}
213-
jwkArray = append(jwkArray, keyPair.PrivateKey)
150+
jwkArray = append(jwkArray, *privateJWK)
214151

215152
// Write to file
216153
enc := json.NewEncoder(out)
@@ -245,5 +182,5 @@ signing_keys_path = "./signing_key.json"
245182

246183
// GetSupportedAlgorithms returns a list of supported algorithms
247184
func GetSupportedAlgorithms() []string {
248-
return []string{string(AlgRS256), string(AlgES256)}
185+
return []string{string(config.AlgRS256), string(config.AlgES256)}
249186
}

internal/gen/signingkeys/signingkeys_test.go

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ package signingkeys
22

33
import (
44
"testing"
5+
6+
"github.com/supabase/cli/pkg/config"
57
)
68

79
func TestGenerateKeyPair(t *testing.T) {
810
tests := []struct {
911
name string
10-
algorithm Algorithm
12+
algorithm config.Algorithm
1113
wantErr bool
1214
}{
1315
{
1416
name: "RSA key generation",
15-
algorithm: AlgRS256,
17+
algorithm: config.AlgRS256,
1618
wantErr: false,
1719
},
1820
{
1921
name: "ECDSA key generation",
20-
algorithm: AlgES256,
22+
algorithm: config.AlgES256,
2123
wantErr: false,
2224
},
2325
{
@@ -29,61 +31,62 @@ func TestGenerateKeyPair(t *testing.T) {
2931

3032
for _, tt := range tests {
3133
t.Run(tt.name, func(t *testing.T) {
32-
keyPair, err := GenerateKeyPair(tt.algorithm)
34+
privateJWK, err := GeneratePrivateKey(tt.algorithm)
3335
if (err != nil) != tt.wantErr {
3436
t.Errorf("GenerateKeyPair(%s) error = %v, wantErr %v", tt.algorithm, err, tt.wantErr)
3537
return
3638
}
3739
if !tt.wantErr {
38-
if keyPair == nil {
40+
if privateJWK == nil {
3941
t.Error("GenerateKeyPair() returned nil key pair")
4042
return
4143
}
4244

4345
// Check that both public and private keys are generated
44-
if keyPair.PublicKey.KeyType == "" {
46+
publicJWK := privateJWK.ToPublicJWK()
47+
if publicJWK.KeyType == "" {
4548
t.Error("Public key type is empty")
4649
}
47-
if keyPair.PrivateKey.KeyType == "" {
50+
if privateJWK.KeyType == "" {
4851
t.Error("Private key type is empty")
4952
}
5053

5154
// Check that key IDs match
52-
if keyPair.PublicKey.KeyID != keyPair.PrivateKey.KeyID {
55+
if publicJWK.KeyID != privateJWK.KeyID {
5356
t.Error("Public and private key IDs don't match")
5457
}
5558

5659
// Algorithm-specific checks
5760
switch tt.algorithm {
58-
case AlgRS256:
59-
if keyPair.PublicKey.KeyType != "RSA" {
60-
t.Errorf("Expected RSA key type, got %s", keyPair.PublicKey.KeyType)
61+
case config.AlgRS256:
62+
if publicJWK.KeyType != "RSA" {
63+
t.Errorf("Expected RSA key type, got %s", publicJWK.KeyType)
6164
}
62-
if keyPair.PrivateKey.Algorithm != "RS256" {
63-
t.Errorf("Expected RS256 algorithm, got %s", keyPair.PrivateKey.Algorithm)
65+
if privateJWK.Algorithm != "RS256" {
66+
t.Errorf("Expected RS256 algorithm, got %s", privateJWK.Algorithm)
6467
}
6568
// Check that RSA-specific fields are present
66-
if keyPair.PrivateKey.Modulus == "" {
69+
if privateJWK.Modulus == "" {
6770
t.Error("RSA private key missing modulus")
6871
}
69-
if keyPair.PrivateKey.PrivateExponent == "" {
72+
if privateJWK.PrivateExponent == "" {
7073
t.Error("RSA private key missing private exponent")
7174
}
72-
case AlgES256:
73-
if keyPair.PublicKey.KeyType != "EC" {
74-
t.Errorf("Expected EC key type, got %s", keyPair.PublicKey.KeyType)
75+
case config.AlgES256:
76+
if publicJWK.KeyType != "EC" {
77+
t.Errorf("Expected EC key type, got %s", publicJWK.KeyType)
7578
}
76-
if keyPair.PrivateKey.Algorithm != "ES256" {
77-
t.Errorf("Expected ES256 algorithm, got %s", keyPair.PrivateKey.Algorithm)
79+
if privateJWK.Algorithm != "ES256" {
80+
t.Errorf("Expected ES256 algorithm, got %s", privateJWK.Algorithm)
7881
}
7982
// Check that EC-specific fields are present
80-
if keyPair.PrivateKey.Curve != "P-256" {
81-
t.Errorf("Expected P-256 curve, got %s", keyPair.PrivateKey.Curve)
83+
if privateJWK.Curve != "P-256" {
84+
t.Errorf("Expected P-256 curve, got %s", privateJWK.Curve)
8285
}
83-
if keyPair.PrivateKey.X == "" {
86+
if privateJWK.X == "" {
8487
t.Error("EC private key missing X coordinate")
8588
}
86-
if keyPair.PrivateKey.Y == "" {
89+
if privateJWK.Y == "" {
8790
t.Error("EC private key missing Y coordinate")
8891
}
8992
}

0 commit comments

Comments
 (0)