Skip to content

feat: generate jwt tokens from signing key #3969

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/supabase/cli/internal/gen/types"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/config"
)

var (
Expand Down Expand Up @@ -97,7 +98,7 @@ var (

algorithm = utils.EnumFlag{
Allowed: signingkeys.GetSupportedAlgorithms(),
Value: string(signingkeys.AlgES256),
Value: string(config.AlgES256),
}
appendKeys bool

Expand Down
95 changes: 16 additions & 79 deletions internal/gen/signingkeys/signingkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,58 +20,24 @@ import (
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/cast"
"github.com/supabase/cli/pkg/config"
)

type Algorithm string

const (
AlgRS256 Algorithm = "RS256"
AlgES256 Algorithm = "ES256"
)

type JWK struct {
KeyType string `json:"kty"`
KeyID string `json:"kid,omitempty"`
Use string `json:"use,omitempty"`
KeyOps []string `json:"key_ops,omitempty"`
Algorithm string `json:"alg,omitempty"`
Extractable *bool `json:"ext,omitempty"`
// RSA specific fields
Modulus string `json:"n,omitempty"`
Exponent string `json:"e,omitempty"`
// RSA private key fields
PrivateExponent string `json:"d,omitempty"`
FirstPrimeFactor string `json:"p,omitempty"`
SecondPrimeFactor string `json:"q,omitempty"`
FirstFactorCRTExponent string `json:"dp,omitempty"`
SecondFactorCRTExponent string `json:"dq,omitempty"`
FirstCRTCoefficient string `json:"qi,omitempty"`
// EC specific fields
Curve string `json:"crv,omitempty"`
X string `json:"x,omitempty"`
Y string `json:"y,omitempty"`
}

type KeyPair struct {
PublicKey JWK
PrivateKey JWK
}

// GenerateKeyPair generates a new key pair for the specified algorithm
func GenerateKeyPair(alg Algorithm) (*KeyPair, error) {
keyID := uuid.New().String()
// GeneratePrivateKey generates a new private key for the specified algorithm
func GeneratePrivateKey(alg config.Algorithm) (*config.JWK, error) {
keyID := uuid.New()

switch alg {
case AlgRS256:
case config.AlgRS256:
return generateRSAKeyPair(keyID)
case AlgES256:
case config.AlgES256:
return generateECDSAKeyPair(keyID)
default:
return nil, errors.Errorf("unsupported algorithm: %s", alg)
}
}

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

// Convert to JWK format
privateJWK := JWK{
privateJWK := config.JWK{
KeyType: "RSA",
KeyID: keyID,
Use: "sig",
Expand All @@ -101,24 +67,10 @@ func generateRSAKeyPair(keyID string) (*KeyPair, error) {
FirstCRTCoefficient: base64.RawURLEncoding.EncodeToString(privateKey.Precomputed.Qinv.Bytes()),
}

publicJWK := JWK{
KeyType: "RSA",
KeyID: keyID,
Use: "sig",
KeyOps: []string{"verify"},
Algorithm: "RS256",
Extractable: cast.Ptr(true),
Modulus: privateJWK.Modulus,
Exponent: privateJWK.Exponent,
}

return &KeyPair{
PublicKey: publicJWK,
PrivateKey: privateJWK,
}, nil
return &privateJWK, nil
}

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

// Convert to JWK format
privateJWK := JWK{
privateJWK := config.JWK{
KeyType: "EC",
KeyID: keyID,
Use: "sig",
Expand All @@ -141,22 +93,7 @@ func generateECDSAKeyPair(keyID string) (*KeyPair, error) {
PrivateExponent: base64.RawURLEncoding.EncodeToString(privateKey.D.Bytes()),
}

publicJWK := JWK{
KeyType: "EC",
KeyID: keyID,
Use: "sig",
KeyOps: []string{"verify"},
Algorithm: "ES256",
Extractable: cast.Ptr(true),
Curve: "P-256",
X: privateJWK.X,
Y: privateJWK.Y,
}

return &KeyPair{
PublicKey: publicJWK,
PrivateKey: privateJWK,
}, nil
return &privateJWK, nil
}

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

// Generate key pair
keyPair, err := GenerateKeyPair(Algorithm(algorithm))
privateJWK, err := GeneratePrivateKey(config.Algorithm(algorithm))
if err != nil {
return err
}

out := io.Writer(os.Stdout)
var jwkArray []JWK
var jwkArray []config.JWK
if len(outputPath) > 0 {
if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(outputPath)); err != nil {
return err
Expand Down Expand Up @@ -210,7 +147,7 @@ func Run(ctx context.Context, algorithm string, appendMode bool, fsys afero.Fs)
}
out = f
}
jwkArray = append(jwkArray, keyPair.PrivateKey)
jwkArray = append(jwkArray, *privateJWK)

// Write to file
enc := json.NewEncoder(out)
Expand Down Expand Up @@ -245,5 +182,5 @@ signing_keys_path = "./signing_key.json"

// GetSupportedAlgorithms returns a list of supported algorithms
func GetSupportedAlgorithms() []string {
return []string{string(AlgRS256), string(AlgES256)}
return []string{string(config.AlgRS256), string(config.AlgES256)}
}
51 changes: 27 additions & 24 deletions internal/gen/signingkeys/signingkeys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ package signingkeys

import (
"testing"

"github.com/supabase/cli/pkg/config"
)

func TestGenerateKeyPair(t *testing.T) {
tests := []struct {
name string
algorithm Algorithm
algorithm config.Algorithm
wantErr bool
}{
{
name: "RSA key generation",
algorithm: AlgRS256,
algorithm: config.AlgRS256,
wantErr: false,
},
{
name: "ECDSA key generation",
algorithm: AlgES256,
algorithm: config.AlgES256,
wantErr: false,
},
{
Expand All @@ -29,61 +31,62 @@ func TestGenerateKeyPair(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
keyPair, err := GenerateKeyPair(tt.algorithm)
privateJWK, err := GeneratePrivateKey(tt.algorithm)
if (err != nil) != tt.wantErr {
t.Errorf("GenerateKeyPair(%s) error = %v, wantErr %v", tt.algorithm, err, tt.wantErr)
return
}
if !tt.wantErr {
if keyPair == nil {
if privateJWK == nil {
t.Error("GenerateKeyPair() returned nil key pair")
return
}

// Check that both public and private keys are generated
if keyPair.PublicKey.KeyType == "" {
publicJWK := privateJWK.ToPublicJWK()
if publicJWK.KeyType == "" {
t.Error("Public key type is empty")
}
if keyPair.PrivateKey.KeyType == "" {
if privateJWK.KeyType == "" {
t.Error("Private key type is empty")
}

// Check that key IDs match
if keyPair.PublicKey.KeyID != keyPair.PrivateKey.KeyID {
if publicJWK.KeyID != privateJWK.KeyID {
t.Error("Public and private key IDs don't match")
}

// Algorithm-specific checks
switch tt.algorithm {
case AlgRS256:
if keyPair.PublicKey.KeyType != "RSA" {
t.Errorf("Expected RSA key type, got %s", keyPair.PublicKey.KeyType)
case config.AlgRS256:
if publicJWK.KeyType != "RSA" {
t.Errorf("Expected RSA key type, got %s", publicJWK.KeyType)
}
if keyPair.PrivateKey.Algorithm != "RS256" {
t.Errorf("Expected RS256 algorithm, got %s", keyPair.PrivateKey.Algorithm)
if privateJWK.Algorithm != "RS256" {
t.Errorf("Expected RS256 algorithm, got %s", privateJWK.Algorithm)
}
// Check that RSA-specific fields are present
if keyPair.PrivateKey.Modulus == "" {
if privateJWK.Modulus == "" {
t.Error("RSA private key missing modulus")
}
if keyPair.PrivateKey.PrivateExponent == "" {
if privateJWK.PrivateExponent == "" {
t.Error("RSA private key missing private exponent")
}
case AlgES256:
if keyPair.PublicKey.KeyType != "EC" {
t.Errorf("Expected EC key type, got %s", keyPair.PublicKey.KeyType)
case config.AlgES256:
if publicJWK.KeyType != "EC" {
t.Errorf("Expected EC key type, got %s", publicJWK.KeyType)
}
if keyPair.PrivateKey.Algorithm != "ES256" {
t.Errorf("Expected ES256 algorithm, got %s", keyPair.PrivateKey.Algorithm)
if privateJWK.Algorithm != "ES256" {
t.Errorf("Expected ES256 algorithm, got %s", privateJWK.Algorithm)
}
// Check that EC-specific fields are present
if keyPair.PrivateKey.Curve != "P-256" {
t.Errorf("Expected P-256 curve, got %s", keyPair.PrivateKey.Curve)
if privateJWK.Curve != "P-256" {
t.Errorf("Expected P-256 curve, got %s", privateJWK.Curve)
}
if keyPair.PrivateKey.X == "" {
if privateJWK.X == "" {
t.Error("EC private key missing X coordinate")
}
if keyPair.PrivateKey.Y == "" {
if privateJWK.Y == "" {
t.Error("EC private key missing Y coordinate")
}
}
Expand Down
Loading