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
10 changes: 10 additions & 0 deletions internal/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type kongConfig struct {
PoolerId string
ApiHost string
ApiPort uint16
BearerToken string
}

var (
Expand Down Expand Up @@ -345,6 +346,15 @@ EOF
PoolerId: utils.PoolerId,
ApiHost: utils.Config.Hostname,
ApiPort: utils.Config.Api.Port,
BearerToken: fmt.Sprintf(
// Pass down apikey as Authorization header for backwards compatibility with legacy JWT.
// If Authorization header is already set, Kong simply skips evaluating this Lua script.
`$((function() return (headers.apikey == '%s' and 'Bearer %s') or (headers.apikey == '%s' and 'Bearer %s') or headers.apikey end)())`,
utils.Config.Auth.SecretKey.Value,
utils.Config.Auth.ServiceRoleKey.Value,
utils.Config.Auth.PublishableKey.Value,
utils.Config.Auth.AnonKey.Value,
),
}); err != nil {
return errors.Errorf("failed to exec template: %w", err)
}
Expand Down
71 changes: 70 additions & 1 deletion internal/start/templates/kong.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ services:
- /auth/v1/verify
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: auth-v1-open-callback
_comment: "GoTrue: /auth/v1/callback* -> http://auth:9999/callback*"
url: http://{{ .GotrueId }}:9999/callback
Expand All @@ -20,6 +25,11 @@ services:
- /auth/v1/callback
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: auth-v1-open-authorize
_comment: "GoTrue: /auth/v1/authorize* -> http://auth:9999/authorize*"
url: http://{{ .GotrueId }}:9999/authorize
Expand All @@ -30,6 +40,11 @@ services:
- /auth/v1/authorize
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: auth-v1
_comment: "GoTrue: /auth/v1/* -> http://auth:9999/*"
url: http://{{ .GotrueId }}:9999/
Expand All @@ -40,6 +55,11 @@ services:
- /auth/v1/
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: rest-v1
_comment: "PostgREST: /rest/v1/* -> http://rest:3000/*"
url: http://{{ .RestId }}:3000/
Expand All @@ -50,6 +70,11 @@ services:
- /rest/v1/
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: rest-admin-v1
_comment: "PostgREST: /rest-admin/v1/* -> http://rest:3001/*"
url: http://{{ .RestId }}:3001/
Expand All @@ -60,6 +85,11 @@ services:
- /rest-admin/v1/
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: graphql-v1
_comment: "PostgREST: /graphql/v1 -> http://rest:3000/rpc/graphql"
url: http://{{ .RestId }}:3000/rpc/graphql
Expand All @@ -75,6 +105,7 @@ services:
add:
headers:
- "Content-Profile: graphql_public"
- "Authorization: {{ .BearerToken }}"
- name: realtime-v1-ws
_comment: "Realtime: /realtime/v1/* -> ws://realtime:4000/socket/websocket"
url: http://{{ .RealtimeId }}:4000/socket
Expand All @@ -86,6 +117,11 @@ services:
- /realtime/v1/
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: realtime-v1-longpoll
_comment: "Realtime: /realtime/v1/* -> ws://realtime:4000/socket/longpoll"
url: http://{{ .RealtimeId }}:4000/socket
Expand All @@ -97,6 +133,11 @@ services:
- /realtime/v1/
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: realtime-v1-rest
_comment: "Realtime: /realtime/v1/* -> http://realtime:4000/api/*"
url: http://{{ .RealtimeId }}:4000/api
Expand All @@ -108,7 +149,11 @@ services:
- /realtime/v1/api
plugins:
- name: cors

- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: storage-v1
_comment: "Storage: /storage/v1/* -> http://storage-api:5000/*"
url: http://{{ .StorageId }}:5000/
Expand All @@ -119,6 +164,11 @@ services:
- /storage/v1/
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: pg-meta
_comment: "pg-meta: /pg/* -> http://pg-meta:8080/*"
url: http://{{ .PgmetaId }}:8080/
Expand All @@ -138,6 +188,13 @@ services:
strip_path: true
paths:
- /functions/v1/
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: analytics-v1
_comment: "Analytics: /analytics/v1/* -> http://logflare:4000/*"
url: http://{{ .LogflareId }}:4000/
Expand All @@ -146,6 +203,13 @@ services:
strip_path: true
paths:
- /analytics/v1/
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- name: pooler-v2-ws
_comment: "Pooler: /pooler/v2/* -> ws://pooler:4000/v2/*"
url: http://{{ .PoolerId }}:4000/v2
Expand All @@ -157,3 +221,8 @@ services:
- /pooler/v2/
plugins:
- name: cors
- name: request-transformer
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
35 changes: 29 additions & 6 deletions internal/status/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/url"
"os"
"reflect"
"strings"
"sync"
"time"

Expand All @@ -29,10 +30,13 @@ type CustomName struct {
StorageS3URL string `env:"api.storage_s3_url,default=STORAGE_S3_URL"`
DbURL string `env:"db.url,default=DB_URL"`
StudioURL string `env:"studio.url,default=STUDIO_URL"`
InbucketURL string `env:"inbucket.url,default=INBUCKET_URL"`
JWTSecret string `env:"auth.jwt_secret,default=JWT_SECRET"`
AnonKey string `env:"auth.anon_key,default=ANON_KEY"`
ServiceRoleKey string `env:"auth.service_role_key,default=SERVICE_ROLE_KEY"`
InbucketURL string `env:"inbucket.url,default=INBUCKET_URL,deprecated"`
MailpitURL string `env:"mailpit.url,default=MAILPIT_URL"`
PublishableKey string `env:"auth.publishable_key,default=PUBLISHABLE_KEY"`
SecretKey string `env:"auth.secret_key,default=SECRET_KEY"`
JWTSecret string `env:"auth.jwt_secret,default=JWT_SECRET,deprecated"`
AnonKey string `env:"auth.anon_key,default=ANON_KEY,deprecated"`
ServiceRoleKey string `env:"auth.service_role_key,default=SERVICE_ROLE_KEY,deprecated"`
StorageS3AccessKeyId string `env:"storage.s3_access_key_id,default=S3_PROTOCOL_ACCESS_KEY_ID"`
StorageS3SecretAccessKey string `env:"storage.s3_secret_access_key,default=S3_PROTOCOL_ACCESS_KEY_SECRET"`
StorageS3Region string `env:"storage.s3_region,default=S3_PROTOCOL_REGION"`
Expand All @@ -50,11 +54,14 @@ func (c *CustomName) toValues(exclude ...string) map[string]string {
values[c.StudioURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Studio.Port)
}
if utils.Config.Auth.Enabled && !utils.SliceContains(exclude, utils.GotrueId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Auth.Image)) {
values[c.PublishableKey] = utils.Config.Auth.PublishableKey.Value
values[c.SecretKey] = utils.Config.Auth.SecretKey.Value
values[c.JWTSecret] = utils.Config.Auth.JwtSecret.Value
values[c.AnonKey] = utils.Config.Auth.AnonKey.Value
values[c.ServiceRoleKey] = utils.Config.Auth.ServiceRoleKey.Value
}
if utils.Config.Inbucket.Enabled && !utils.SliceContains(exclude, utils.InbucketId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Inbucket.Image)) {
values[c.MailpitURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Inbucket.Port)
values[c.InbucketURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Inbucket.Port)
}
if utils.Config.Storage.Enabled && !utils.SliceContains(exclude, utils.StorageId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image)) {
Expand Down Expand Up @@ -171,7 +178,7 @@ func checkHTTPHead(ctx context.Context, path string) error {
healthOnce.Do(func() {
healthClient = fetcher.NewServiceGateway(
utils.Config.Api.ExternalUrl,
utils.Config.Auth.AnonKey.Value,
utils.Config.Auth.SecretKey.Value,
fetcher.WithHTTPClient(NewKongClient()),
fetcher.WithUserAgent("SupabaseCLI/"+utils.Version),
)
Expand All @@ -195,9 +202,12 @@ func PrettyPrint(w io.Writer, exclude ...string) {
ApiURL: " " + utils.Aqua("API URL"),
GraphqlURL: " " + utils.Aqua("GraphQL URL"),
StorageS3URL: " " + utils.Aqua("S3 Storage URL"),
DbURL: " " + utils.Aqua("DB URL"),
DbURL: " " + utils.Aqua("Database URL"),
StudioURL: " " + utils.Aqua("Studio URL"),
InbucketURL: " " + utils.Aqua("Inbucket URL"),
MailpitURL: " " + utils.Aqua("Mailpit URL"),
PublishableKey: " " + utils.Aqua("Publishable key"),
SecretKey: " " + utils.Aqua("Secret key"),
JWTSecret: " " + utils.Aqua("JWT secret"),
AnonKey: " " + utils.Aqua("anon key"),
ServiceRoleKey: "" + utils.Aqua("service_role key"),
Expand All @@ -207,11 +217,24 @@ func PrettyPrint(w io.Writer, exclude ...string) {
}
values := names.toValues(exclude...)
// Iterate through map in order of declared struct fields
t := reflect.TypeOf(names)
val := reflect.ValueOf(names)
for i := 0; i < val.NumField(); i++ {
k := val.Field(i).String()
if tag := t.Field(i).Tag.Get("env"); isDeprecated(tag) {
continue
}
if v, ok := values[k]; ok {
fmt.Fprintf(w, "%s: %s\n", k, v)
}
}
}

func isDeprecated(tag string) bool {
for part := range strings.SplitSeq(tag, ",") {
if strings.EqualFold(part, "deprecated") {
return true
}
}
return false
}
41 changes: 38 additions & 3 deletions pkg/config/apikeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,39 @@ import (
"github.com/google/uuid"
)

const (
defaultJwtSecret = "super-secret-jwt-token-with-at-least-32-characters-long"
defaultJwtExpiry = 1983812996
defaultPublishableKey = "sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH"
defaultSecretKey = "sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz"
)

type CustomClaims struct {
// Overrides Issuer to maintain json order when marshalling
Issuer string `json:"iss,omitempty"`
Ref string `json:"ref,omitempty"`
Role string `json:"role"`
IsAnon bool `json:"is_anonymous,omitempty"`
jwt.RegisteredClaims
}

func (c CustomClaims) NewToken() *jwt.Token {
if c.ExpiresAt == nil {
c.ExpiresAt = jwt.NewNumericDate(time.Unix(defaultJwtExpiry, 0))
}
if len(c.Issuer) == 0 {
c.Issuer = "supabase-demo"
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, c)
}

// generateAPIKeys generates JWT tokens using the appropriate signing method
func (a *auth) generateAPIKeys() error {
if len(a.JwtSecret.Value) == 0 {
a.JwtSecret.Value = defaultJwtSecret
} else if len(a.JwtSecret.Value) < 16 {
return errors.Errorf("Invalid config for auth.jwt_secret. Must be at least 16 characters")
}
// Generate anon key if not provided
if len(a.AnonKey.Value) == 0 {
signed, err := a.generateJWT("anon")
Expand All @@ -32,6 +63,13 @@ func (a *auth) generateAPIKeys() error {
}
a.ServiceRoleKey.Value = signed
}
// Set hardcoded opaque keys
if len(a.PublishableKey.Value) == 0 {
a.PublishableKey.Value = defaultPublishableKey
}
if len(a.SecretKey.Value) == 0 {
a.SecretKey.Value = defaultSecretKey
}
return nil
}

Expand All @@ -42,9 +80,6 @@ func (a auth) generateJWT(role string) (string, error) {
return GenerateAsymmetricJWT(a.SigningKeys[0], claims)
}
// Fallback to generating symmetric keys
if len(a.JwtSecret.Value) < 16 {
return "", errors.Errorf("Invalid config for auth.jwt_secret. Must be at least 16 characters")
}
signed, err := claims.NewToken().SignedString([]byte(a.JwtSecret.Value))
if err != nil {
return "", errors.Errorf("failed to generate JWT: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ type (
Web3 web3 `toml:"web3"`

// Custom secrets can be injected from .env file
PublishableKey Secret `toml:"publishable_key"`
SecretKey Secret `toml:"secret_key"`
JwtSecret Secret `toml:"jwt_secret"`
AnonKey Secret `toml:"anon_key"`
ServiceRoleKey Secret `toml:"service_role_key"`
Expand Down
Loading