Skip to content

Commit

Permalink
feat: add third-party auth support for local dev
Browse files Browse the repository at this point in the history
  • Loading branch information
hf committed Aug 7, 2024
1 parent f559eb7 commit 4c16c94
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 14 deletions.
9 changes: 8 additions & 1 deletion internal/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers
excluded[name] = true
}

jwks, err := utils.Config.ResolveJWKS()
if err != nil {
return err
}

// Start Postgres.
w := utils.StatusWriter{Program: p}
if dbConfig.Host == utils.DbId {
Expand Down Expand Up @@ -722,6 +727,7 @@ EOF
"DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime",
"DB_ENC_KEY=" + utils.Config.Realtime.EncryptionKey,
"API_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
fmt.Sprintf("API_JWT_JWKS=%q", jwks),
"METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
"APP_NAME=realtime",
"SECRET_KEY_BASE=" + utils.Config.Realtime.SecretKeyBase,
Expand Down Expand Up @@ -772,7 +778,7 @@ EOF
"PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(utils.Config.Api.ExtraSearchPath, ","),
fmt.Sprintf("PGRST_DB_MAX_ROWS=%d", utils.Config.Api.MaxRows),
"PGRST_DB_ANON_ROLE=anon",
"PGRST_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
fmt.Sprintf("PGRST_JWT_SECRET=%q", jwks),
"PGRST_ADMIN_SERVER_PORT=3001",
},
// PostgREST does not expose a shell for health check
Expand Down Expand Up @@ -805,6 +811,7 @@ EOF
"ANON_KEY=" + utils.Config.Auth.AnonKey,
"SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey,
"AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
fmt.Sprintf("AUTH_JWT_JWKS=%q", jwks),
fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit),
"STORAGE_BACKEND=file",
Expand Down
181 changes: 168 additions & 13 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package config
import (
"bytes"
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -114,19 +117,20 @@ func (c CustomClaims) NewToken() *jwt.Token {
// Default values for internal configs should be added to `var Config` initializer.
type (
config struct {
ProjectId string `toml:"project_id"`
Hostname string `toml:"-"`
Api api `toml:"api"`
Db db `toml:"db" mapstructure:"db"`
Realtime realtime `toml:"realtime"`
Studio studio `toml:"studio"`
Inbucket inbucket `toml:"inbucket"`
Storage storage `toml:"storage"`
Auth auth `toml:"auth" mapstructure:"auth"`
EdgeRuntime edgeRuntime `toml:"edge_runtime"`
Functions FunctionConfig `toml:"functions"`
Analytics analytics `toml:"analytics"`
Experimental experimental `toml:"experimental" mapstructure:"-"`
ProjectId string `toml:"project_id"`
Hostname string `toml:"-"`
Api api `toml:"api"`
Db db `toml:"db" mapstructure:"db"`
Realtime realtime `toml:"realtime"`
Studio studio `toml:"studio"`
Inbucket inbucket `toml:"inbucket"`
Storage storage `toml:"storage"`
Auth auth `toml:"auth" mapstructure:"auth"`
ThirdPartyAuth thirdPartyAuth `toml:"third_party_auth"`
EdgeRuntime edgeRuntime `toml:"edge_runtime"`
Functions FunctionConfig `toml:"functions"`
Analytics analytics `toml:"analytics"`
Experimental experimental `toml:"experimental" mapstructure:"-"`
}

api struct {
Expand Down Expand Up @@ -251,6 +255,18 @@ type (
ServiceRoleKey string `toml:"-" mapstructure:"service_role_key"`
}

thirdPartyAuth struct {
Enabled bool `toml:"enabled"`

FirebaseProjectID string `toml:"firebase_project_id"`

Auth0Tenant string `toml:"auth0_tenant"`
Auth0TenantRegion string `toml:"auth0_tenant_region"`

AWSCognitoUserPoolID string `toml:"aws_cognito_user_pool_id"`
AWSCognitoUserPoolRegion string `toml:"aws_cognito_user_pool_region"`
}

email struct {
EnableSignup bool `toml:"enable_signup"`
DoubleConfirmChanges bool `toml:"double_confirm_changes"`
Expand Down Expand Up @@ -837,6 +853,45 @@ func (c *config) Validate() error {
c.Auth.External[ext] = provider
}
}
// Validate Third-Party Auth config
if c.ThirdPartyAuth.Enabled {
allDisabled := true
allDisabled = allDisabled && c.ThirdPartyAuth.FirebaseProjectID == ""
allDisabled = allDisabled && c.ThirdPartyAuth.Auth0Tenant == ""
allDisabled = allDisabled && c.ThirdPartyAuth.Auth0TenantRegion == ""
allDisabled = allDisabled && c.ThirdPartyAuth.AWSCognitoUserPoolID == ""
allDisabled = allDisabled && c.ThirdPartyAuth.AWSCognitoUserPoolRegion == ""

if allDisabled {
return errors.New("Invalid config for third_party_auth.enabled. It was set to true but no Third-Party Auth provider is configured.")
}

if c.ThirdPartyAuth.Auth0TenantRegion != "" && c.ThirdPartyAuth.Auth0Tenant == "" {
return errors.New("Invalid config for third_party_auth.auth0_tenant, it must be provided if auth0_tenant_region is configured.")
}

if (c.ThirdPartyAuth.AWSCognitoUserPoolID != "") != (c.ThirdPartyAuth.AWSCognitoUserPoolRegion != "") {
return errors.New("Invalid config for third_party_auth.aws_cognito_user_pool_id and _region, both must be set.")
}

configured := 0

if c.ThirdPartyAuth.FirebaseProjectID != "" {
configured += 1
}

if c.ThirdPartyAuth.Auth0Tenant != "" {
configured += 1
}

if c.ThirdPartyAuth.AWSCognitoUserPoolID != "" {
configured += 1
}

if configured > 1 {
return errors.New("Invalid config for third_party_auth, only one of Firebase Auth, Auth0 or AWS Cognito User Pool should be configured, but there were multiple.")
}
}
// Validate functions config
if c.EdgeRuntime.Enabled {
allowed := []RequestPolicy{PolicyPerWorker, PolicyOneshot}
Expand Down Expand Up @@ -978,3 +1033,103 @@ func ValidateBucketName(name string) error {
}
return nil
}

func (tpa *thirdPartyAuth) IssuerURL() string {
if !tpa.Enabled {
return ""
}

if tpa.FirebaseProjectID != "" {
return fmt.Sprintf("https://securetoken.google.com/%s", tpa.FirebaseProjectID)
}

if tpa.Auth0Tenant != "" {
if tpa.Auth0TenantRegion != "" {
return fmt.Sprintf("https://%s.%s.auth0.com", tpa.Auth0Tenant, tpa.Auth0TenantRegion)
}

return fmt.Sprintf("https://%s.auth0.com", tpa.Auth0Tenant)
}

if tpa.AWSCognitoUserPoolID != "" || tpa.AWSCognitoUserPoolRegion != "" {
return fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s", tpa.AWSCognitoUserPoolRegion, tpa.AWSCognitoUserPoolID)
}

return ""
}

// ResolveJWKS creates the JWKS from the JWT secret and Third-Party Auth
// configs by resolving the JWKS via the OIDC discovery URL.
// It always returns a JWKS string, except when there's an error fetching.
func (c *config) ResolveJWKS() (string, error) {
var jwks struct {
Keys []json.RawMessage `json:"keys"`
}

issuerURL := c.ThirdPartyAuth.IssuerURL()
if issuerURL != "" {
discoveryURL := issuerURL + "/.well-known/oidc-configuration"

resp, err := http.Get(discoveryURL) // #nosec G107
if err != nil {
return "", fmt.Errorf("third_party_auth: failed to fetch ODIC configuration at URL %q with error: %w", discoveryURL, err)
}

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("third_party_auth: failed to fetch OIDC configuration at URL %q expected HTTP 200 got %d", discoveryURL, resp.StatusCode)
}

var oidcConfiguration struct {
JWKSURI string `json:"jwks_uri"`
}

if err := json.NewDecoder(resp.Body).Decode(&oidcConfiguration); err != nil {
return "", fmt.Errorf("third_party_auth: failed to parse OIDC configuration at URL %q as JSON with error: %w", discoveryURL, err)
}

if oidcConfiguration.JWKSURI == "" {
return "", fmt.Errorf("third_party_auth: OIDC configuration at URL %q does not expose a jwks_uri property", discoveryURL)
}

resp, err = http.Get(oidcConfiguration.JWKSURI) // #nosec G107
if err != nil {
return "", fmt.Errorf("third_party_auth: failed to fetch JWKS at URI %q as discovered from %q with error: %w", oidcConfiguration.JWKSURI, discoveryURL, err)
}

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("third_party_auth: failed to fetch JWKS at URI %q as discovered from %q expected HTTP 200 got %d", oidcConfiguration.JWKSURI, discoveryURL, resp.StatusCode)
}

if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil {
return "", fmt.Errorf("third_party_auth: failed to parse JWKS at URL %q as JSON as discovered from %q with error: %w", oidcConfiguration.JWKSURI, discoveryURL, err)
}

if len(jwks.Keys) == 0 {
return "", fmt.Errorf("third_party_auth: JWKS at URL %q as discovered from %q does not contain any JWK keys", oidcConfiguration.JWKSURI, discoveryURL)
}
}

var secretJWK struct {
KeyType string `json:"kty"`
KeyBase64URL string `json:"k"`
}

secretJWK.KeyType = "oct"
secretJWK.KeyBase64URL = base64.RawURLEncoding.EncodeToString([]byte(c.Auth.JwtSecret))

secretJWKEncoded, err := json.Marshal(&secretJWK)
if err != nil {
// must always be marshallable
panic(err)
}

jwks.Keys = append(jwks.Keys, json.RawMessage(secretJWKEncoded))

jwksEncoded, err := json.Marshal(jwks)
if err != nil {
// must always be marshallable
panic(err)
}

return string(jwksEncoded), nil
}
12 changes: 12 additions & 0 deletions pkg/config/templates/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,18 @@ url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false

# Use a Third-Party Auth provider alongside Supabas Auth. Configure only one of: Firebase Auth, Auth0 or AWS Congito User Pool.
[third_party_auth]
enabled = false
# Firebase Auth:
# firebase_project_id = "my-firebase-project"
# Auth0:
# auth0_tenant = "my-auth0-tenant"
# auth0_tenant_region = "us"
# AWS Cognito (both required):
# aws_cognito_user_pool_id = "my-user-pool-id"
# aws_cognito_user_pool_region = "us-east-1"

[edge_runtime]
enabled = true
# Configure one of the supported request policies: `oneshot`, `per_worker`.
Expand Down

0 comments on commit 4c16c94

Please sign in to comment.