Skip to content
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
46 changes: 40 additions & 6 deletions cmd/gen.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package cmd

import (
"encoding/json"
"os"
"os/signal"
"strings"
"time"

env "github.com/Netflix/go-env"
"github.com/go-errors/errors"
"github.com/go-viper/mapstructure/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/afero"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -124,17 +127,18 @@ Supported algorithms:
claims config.CustomClaims
expiry time.Time
validFor time.Duration
payload string

genJWTCmd = &cobra.Command{
Use: "bearer-jwt",
Short: "Generate a Bearer Auth JWT for accessing Data API",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if expiry.IsZero() {
expiry = time.Now().Add(validFor)
custom := jwt.MapClaims{}
if err := parseClaims(custom); err != nil {
return err
}
claims.ExpiresAt = jwt.NewNumericDate(expiry)
return bearerjwt.Run(cmd.Context(), claims, os.Stdout, afero.NewOsFs())
return bearerjwt.Run(cmd.Context(), custom, os.Stdout, afero.NewOsFs())
},
}
)
Expand Down Expand Up @@ -166,12 +170,42 @@ func init() {
genCmd.AddCommand(genSigningKeyCmd)
tokenFlags := genJWTCmd.Flags()
tokenFlags.StringVar(&claims.Role, "role", "", "Postgres role to use.")
cobra.CheckErr(genJWTCmd.MarkFlagRequired("role"))
tokenFlags.StringVar(&claims.Subject, "sub", "", "User ID to impersonate.")
genJWTCmd.Flag("sub").DefValue = "anonymous"
tokenFlags.TimeVar(&expiry, "exp", time.Time{}, []string{time.RFC3339}, "Expiry timestamp for this token.")
tokenFlags.DurationVar(&validFor, "valid-for", time.Minute*30, "Validity duration for this token.")
genJWTCmd.MarkFlagsMutuallyExclusive("exp", "valid-for")
cobra.CheckErr(genJWTCmd.MarkFlagRequired("role"))
tokenFlags.StringVar(&payload, "payload", "{}", "Custom claims in JSON format.")
genCmd.AddCommand(genJWTCmd)
rootCmd.AddCommand(genCmd)
}

func parseClaims(custom jwt.MapClaims) error {
// Initialise default claims
now := time.Now()
if expiry.IsZero() {
expiry = now.Add(validFor)
} else {
now = expiry.Add(-validFor)
}
claims.IssuedAt = jwt.NewNumericDate(now)
claims.ExpiresAt = jwt.NewNumericDate(expiry)
// Set is_anonymous = true for authenticated role without explicit user ID
if strings.EqualFold(claims.Role, "authenticated") && len(claims.Subject) == 0 {
claims.IsAnon = true
}
// Override with custom claims
if dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
Squash: true,
Result: &custom,
}); err != nil {
return errors.Errorf("failed to init decoder: %w", err)
} else if err := dec.Decode(claims); err != nil {
return errors.Errorf("failed to decode claims: %w", err)
}
if err := json.Unmarshal([]byte(payload), &custom); err != nil {
return errors.Errorf("failed to parse payload: %w", err)
}
return nil
}
33 changes: 32 additions & 1 deletion internal/functions/download/download.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package download

import (
"bufio"
"bytes"
"context"
"fmt"
Expand Down Expand Up @@ -200,10 +201,40 @@ func extractOne(ctx context.Context, slug, eszipPath string) error {
network.NetworkingConfig{},
"",
os.Stdout,
os.Stderr,
getErrorLogger(),
)
}

func getErrorLogger() io.Writer {
if utils.Config.EdgeRuntime.DenoVersion > 1 {
return os.Stderr
}
// Additional error handling for deno v1
r, w := io.Pipe()
go func() {
logs := bufio.NewScanner(r)
for logs.Scan() {
line := logs.Text()
fmt.Fprintln(os.Stderr, line)
if strings.EqualFold(line, "invalid eszip v2") {
utils.CmdSuggestion = suggestDenoV2()
}
}
if err := logs.Err(); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}()
return w
}

func suggestDenoV2() string {
return fmt.Sprintf(`Please use deno v2 in %s to download this Function:

[edge_runtime]
deno_version = 2
`, utils.Bold(utils.ConfigPath))
}

func suggestLegacyBundle(slug string) string {
return fmt.Sprintf("\nIf your function is deployed using CLI < 1.120.0, trying running %s instead.", utils.Aqua("supabase functions download --legacy-bundle "+slug))
}
73 changes: 56 additions & 17 deletions internal/gen/bearerjwt/bearerjwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,80 @@ package bearerjwt

import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strings"

"github.com/go-errors/errors"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/config"
)

func Run(ctx context.Context, claims config.CustomClaims, w io.Writer, fsys afero.Fs) error {
func Run(ctx context.Context, claims jwt.Claims, w io.Writer, fsys afero.Fs) error {
if err := flags.LoadConfig(fsys); err != nil {
return err
}
// Set is_anonymous = true for authenticated role without explicit user ID
if strings.EqualFold(claims.Role, "authenticated") && len(claims.Subject) == 0 {
claims.IsAnon = true
}
// Use the first signing key that passes validation
for _, k := range utils.Config.Auth.SigningKeys {
fmt.Fprintln(os.Stderr, "Using signing key ID:", k.KeyID.String())
if token, err := config.GenerateAsymmetricJWT(k, claims); err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Fprintln(w, token)
return nil
}
key, err := getSigningKey(ctx)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Using legacy JWT secret...")
token, err := claims.NewToken().SignedString([]byte(utils.Config.Auth.JwtSecret.Value))
token, err := config.GenerateAsymmetricJWT(*key, claims)
if err != nil {
return errors.Errorf("failed to generate auth token: %w", err)
return err
}
fmt.Fprintln(w, token)
return nil
}

func getSigningKey(ctx context.Context) (*config.JWK, error) {
console := utils.NewConsole()
if len(utils.Config.Auth.SigningKeysPath) == 0 {
title := "Enter your signing key in JWK format: "
kid, err := console.PromptText(ctx, title)
if err != nil {
return nil, err
}
key := config.JWK{}
if err := json.Unmarshal([]byte(kid), &key); err != nil {
return nil, errors.Errorf("failed to parse JWK: %w", err)
}
return &key, nil
}
// Allow manual kid entry on CI
if !console.IsTTY {
title := "Enter the kid of your signing key (or leave blank to use the first one): "
kid, err := console.PromptText(ctx, title)
if err != nil {
return nil, err
}
for i, k := range utils.Config.Auth.SigningKeys {
if k.KeyID == kid {
return &utils.Config.Auth.SigningKeys[i], nil
}
}
if len(kid) == 0 && len(utils.Config.Auth.SigningKeys) > 0 {
return &utils.Config.Auth.SigningKeys[0], nil
}
return nil, errors.Errorf("signing key not found: %s", kid)
}
// Let user choose from a list of signing keys
items := make([]utils.PromptItem, len(utils.Config.Auth.SigningKeys))
for i, k := range utils.Config.Auth.SigningKeys {
items[i] = utils.PromptItem{
Index: i,
Summary: k.KeyID,
Details: fmt.Sprintf("%s (%s)", k.Algorithm, strings.Join(k.KeyOps, ",")),
}
}
choice, err := utils.PromptChoice(ctx, "Select a signing key:", items)
if err != nil {
return nil, err
}
fmt.Fprintln(os.Stderr, "Selected key ID:", choice.Summary)
return &utils.Config.Auth.SigningKeys[choice.Index], nil
}
Loading