Skip to content
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

feat: Eth wallet support #282

Closed
wants to merge 60 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
af59c7a
First Try at Web3
HarryET Nov 19, 2021
3ed3025
Update go.mod
HarryET Nov 19, 2021
e2a64b4
Make `/nonce` more secure
HarryET Nov 19, 2021
c08fce6
Fix new environment variables
HarryET Nov 19, 2021
f8997b8
Remove Unnecessary ENV
HarryET Nov 19, 2021
8fe0e9a
Generate of nonces
HarryET Nov 19, 2021
25a0e1c
Update init_postgres.sql
HarryET Nov 19, 2021
66246ba
Misc Changes
HarryET Nov 20, 2021
30a0b1f
Disable endpoints when disabled in config
HarryET Nov 20, 2021
3685c8c
Fix nonce insertion
HarryET Nov 20, 2021
9fe34cf
Misc Changes
HarryET Nov 20, 2021
2a4efea
Create new users migration
HarryET Nov 20, 2021
c1f88b7
Misc Changes
HarryET Nov 23, 2021
a7e68fa
Fix Eth Validation
HarryET Nov 27, 2021
3c1f18a
Misc Changes
HarryET Nov 27, 2021
645a642
Finish Signup
HarryET Nov 27, 2021
0db2c53
Remove in favour of migrations
HarryET Nov 29, 2021
eca0371
add statement/message support
HarryET Nov 30, 2021
e7c9720
Add .idea to gitignore
HarryET Dec 12, 2021
1bd3fa9
Make Golang 1.16 compatible
HarryET Dec 12, 2021
4184e15
Comment api/nonce.go
HarryET Dec 12, 2021
84cdfa2
Comment api/eth.go
HarryET Dec 12, 2021
b75c60a
Comment models/user.go
HarryET Dec 12, 2021
2177ef0
Remove rate limits
HarryET Dec 12, 2021
5be052c
Fix statement implementation
HarryET Dec 12, 2021
95b78da
Fix the user table
HarryET Feb 20, 2022
2e2954b
Initial siwe-go migration
HarryET Feb 21, 2022
96b0665
siwe-go migration
HarryET Feb 22, 2022
9c6aa97
Update postgresd.sh
HarryET Feb 22, 2022
57cfd08
Initial working version
HarryET Feb 22, 2022
2ad9664
Merge branch 'master' into feat/web3
HarryET Feb 22, 2022
55cf1d8
Fix cookies after merge main
HarryET Feb 22, 2022
ddab9b9
Trying to fix existing nonce detection
HarryET Feb 22, 2022
8d97a05
Initial fix of nonce 1-1 mapping
HarryET Feb 24, 2022
cad6ae6
Update!
HarryET Mar 2, 2022
0815284
Fix nonce verification
HarryET Mar 11, 2022
a038dd2
Fix account persistance
HarryET Mar 11, 2022
7290cf0
Tidy api and create rate-limits
HarryET Mar 11, 2022
7fb9875
Tidy routing
HarryET Mar 11, 2022
b081a15
Merge branch 'master' into feat/web3
HarryET Mar 11, 2022
7967abc
Tidy go.mod
HarryET Mar 11, 2022
1f3945c
Fix issue where chain id can be hex
HarryET Mar 29, 2022
b625e60
Merge branch 'master' of https://github.com/supabase/gotrue into supa…
HarryET Jun 29, 2022
653da97
Merge branch 'supabase-master' into feat/web3
HarryET Jun 29, 2022
e423dcd
Fix incorrect table for comment
HarryET Jun 29, 2022
b123739
Fix API changes
HarryET Aug 2, 2022
ec1c086
Update signup.go
HarryET Aug 2, 2022
17b6316
Confirm eth accounts automatically
HarryET Aug 2, 2022
844bf6b
Merge branch 'supabase:master' into feat/web3
HarryET Aug 2, 2022
9185e33
Update eth.go
HarryET Aug 2, 2022
1cd8993
Support Multiple Providers
HarryET Aug 3, 2022
a04462d
Create identity on crypto account creation
HarryET Aug 3, 2022
8d3180c
Fix queries
HarryET Aug 3, 2022
816a647
Update api.go
HarryET Aug 3, 2022
0fa4b5d
Remove TODO
HarryET Aug 3, 2022
fbd0466
Switch `chainId` to `int`
HarryET Aug 12, 2022
8c3115e
Bump `siwe-go` version
HarryET Aug 12, 2022
67a0c08
Merge branch 'supabase:master' into feat/web3
HarryET Aug 12, 2022
32accf8
Fix identity id
HarryET Aug 12, 2022
977d8d9
misc
HarryET Aug 12, 2022
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
5 changes: 5 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,13 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
r.With(sharedLimiter).With(api.verifyCaptcha).Post("/signup", api.Signup)
r.With(sharedLimiter).With(api.verifyCaptcha).With(api.requireEmailProvider).Post("/recover", api.Recover)
r.With(sharedLimiter).With(api.verifyCaptcha).Post("/magiclink", api.MagicLink)
r.With(sharedLimiter).With(api.verifyCaptcha).Route("/nonce", func(r *router) {
r.Post("/", api.Nonce)
r.Get("/{nonce_id}", api.NonceById)
})

r.With(sharedLimiter).With(api.verifyCaptcha).Post("/otp", api.Otp)
r.With(sharedLimiter).With(api.verifyCaptcha).Post("/eth", api.Eth)

r.With(api.requireEmailProvider).With(api.limitHandler(
// Allow requests at a rate of 30 per 5 minutes.
Expand Down
163 changes: 163 additions & 0 deletions api/eth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package api

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/gofrs/uuid"
"github.com/netlify/gotrue/metering"
"github.com/netlify/gotrue/models"
"github.com/netlify/gotrue/storage"
)

// EthParams contains the request body params for the eth endpoint, all values hex encoded
type EthParams struct {
WalletAddress string `json:"wallet_address"`
NonceId string `json:"nonce_id"`
Signature string `json:"signature"`
}

func signHash(data []byte) []byte {
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
return crypto.Keccak256([]byte(msg))
}

func (a *API) Eth(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
config := a.getConfig(ctx)
instanceID := getInstanceID(ctx)
useCookie := r.Header.Get(useCookieHeader)

if !config.External.Eth.Enabled {
return badRequestError("Unsupported eth provider")
}

params := &EthParams{}
body, err := ioutil.ReadAll(r.Body)
jsonDecoder := json.NewDecoder(bytes.NewReader(body))
if err = jsonDecoder.Decode(params); err != nil {
return badRequestError("Could not read verification params: %v", err)
}

nonce, err := models.GetNonceById(a.db, uuid.FromStringOrNil(params.NonceId))
if err != nil {
return badRequestError("Failed to find nonce: %v", err)
}

// TODO (HarryET): Validate nonce expiry time

clientIP := strings.Split(r.RemoteAddr, ":")[0]
if !nonce.VerifyIp(clientIP) {
return badRequestError("IP not the same as the IP this nonce was issued too")
}

walletAddress := common.HexToAddress(params.WalletAddress)

sig, err := hexutil.Decode(params.Signature)
if err != nil {
return badRequestError("Invalid Signature: Failed to decode, not valid hex")
}

// https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442
if sig[64] != 27 && sig[64] != 28 {
return badRequestError("Invalid Signature: Invalid formatting")
}
sig[64] -= 27

nonceString, err := nonce.Build()
msg := []byte(nonceString)
pubKey, err := crypto.SigToPub(signHash(msg), sig)
if err != nil {
return badRequestError("Invalid Signature: Failed to extract public key")
}

recoveredWalletAddress := crypto.PubkeyToAddress(*pubKey)
if walletAddress != recoveredWalletAddress {
return badRequestError("Invalid Signature: Wallet address not the same as suplied address")
}

didUserExist := true

aud := a.requestAud(ctx, r)
user, uerr := models.FindUserByEthAddressAndAudience(a.db, instanceID, params.WalletAddress, aud)

if err != nil && !models.IsNotFoundError(err) {
return internalServerError("Database error finding user").WithInternalError(err)
}

if models.IsNotFoundError(uerr) {
uerr = a.db.Transaction(func(tx *storage.Connection) error {
user, uerr = a.signupNewUser(ctx, tx, &SignupParams{
EthAddress: params.WalletAddress,
Provider: "eth",
Aud: aud,
})
didUserExist = false

if uerr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, nil); uerr != nil {
return uerr
}
if uerr = triggerEventHooks(ctx, tx, SignupEvent, user, instanceID, config); uerr != nil {
return uerr
}

return uerr
})

if uerr != nil {
return uerr
}
}

err = a.db.Transaction(func(tx *storage.Connection) error {
if terr := models.NewAuditLogEntry(tx, instanceID, user, models.NonceConsumed, nil); terr != nil {
return terr
}
return nonce.Consume(tx)
})

if err != nil {
return internalServerError("Failed to consume nonce").WithInternalError(err)
}

var token *AccessTokenResponse
err = a.db.Transaction(func(tx *storage.Connection) error {
var terr error
if terr = models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, nil); terr != nil {
return terr
}
if terr = triggerEventHooks(ctx, tx, LoginEvent, user, instanceID, config); terr != nil {
return terr
}

token, terr = a.issueRefreshToken(ctx, tx, user)
if terr != nil {
return terr
}

if useCookie != "" && config.Cookie.Duration > 0 {
if terr = a.setCookieToken(config, token.Token, useCookie == useSessionCookie, w); terr != nil {
return internalServerError("Failed to set JWT cookie. %s", terr)
}
}
return nil
})
if err != nil {
return err
}
metering.RecordLogin("eth", user.ID, instanceID)
token.User = user

status := http.StatusOK
if !didUserExist {
status = http.StatusCreated
}
return sendJSON(w, status, token)
}
99 changes: 99 additions & 0 deletions api/nonce.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package api

import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"strings"

"github.com/go-chi/chi"
"github.com/gofrs/uuid"
"github.com/netlify/gotrue/models"
"github.com/netlify/gotrue/storage"
)

// NonceParams contains the request body params for the nonce endpoint
type NonceParams struct {
WalletAddress string `json:"wallet_address"`
ChainId int `json:"chain_id"`
Url string `json:"url"`
}

type NonceResponse struct {
Id string `json:"id"`
Nonce string `json:"nonce"`
}

func (a *API) Nonce(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
config := a.getConfig(ctx)
instanceID := getInstanceID(ctx)

if !config.External.Eth.Enabled {
return badRequestError("Unsupported eth provider")
}

params := &NonceParams{}
body, err := ioutil.ReadAll(r.Body)
jsonDecoder := json.NewDecoder(bytes.NewReader(body))
if err = jsonDecoder.Decode(params); err != nil {
return badRequestError("Could not read verification params: %v", err)
}

clientIP := strings.Split(r.RemoteAddr, ":")[0]

nonce, err := models.NewNonce(instanceID, params.ChainId, params.Url, params.WalletAddress, clientIP)
if err != nil || nonce == nil {
return internalServerError("Failed to generate nonce")
}

err = a.db.Transaction(func(tx *storage.Connection) error {
if err := tx.Create(nonce); err != nil {
return internalServerError("Failed to save nonce")
}

return nil
})

if err != nil {
return err
}

builtNonce, err := nonce.Build()
if err != nil {
return internalServerError("Failed to build nonce")
}

return sendJSON(w, http.StatusCreated, &NonceResponse{
Id: nonce.ID.String(),
Nonce: builtNonce,
})
}

func (a *API) NonceById(w http.ResponseWriter, r *http.Request) error {
nonceId, err := uuid.FromString(chi.URLParam(r, "nonce_id"))
if err != nil {
return badRequestError("nonce_id must be an UUID")
}

nonce, err := models.GetNonceById(a.db, nonceId)
if err != nil {
if models.IsNotFoundError(err) {
return badRequestError("Invalid nonce_id")
}
return internalServerError("Failed to find nonce")
}

builtNonce, err := nonce.Build()
if err != nil {
return internalServerError("Failed to build nonce")
}

// TODO (HarryET): Concider checking IP?

return sendJSON(w, http.StatusCreated, &NonceResponse{
Id: nonce.ID.String(),
Nonce: builtNonce,
})
}
2 changes: 2 additions & 0 deletions api/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type ProviderSettings struct {
Slack bool `json:"slack"`
Twitch bool `json:"twitch"`
Twitter bool `json:"twitter"`
Eth bool `json:"eth"`
Email bool `json:"email"`
Phone bool `json:"phone"`
SAML bool `json:"saml"`
Expand Down Expand Up @@ -50,6 +51,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
Slack: config.External.Slack.Enabled,
Twitch: config.External.Twitch.Enabled,
Twitter: config.External.Twitter.Enabled,
Eth: config.External.Eth.Enabled,
Email: config.External.Email.Enabled,
Phone: config.External.Phone.Enabled,
SAML: config.External.Saml.Enabled,
Expand Down
18 changes: 11 additions & 7 deletions api/signup.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import (

// SignupParams are the parameters the Signup endpoint accepts
type SignupParams struct {
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password"`
Data map[string]interface{} `json:"data"`
Provider string `json:"-"`
Aud string `json:"-"`
Email string `json:"email"`
Phone string `json:"phone"`
EthAddress string `json:"-"`
Password string `json:"password"`
Data map[string]interface{} `json:"data"`
Provider string `json:"-"`
Aud string `json:"-"`
}

// Signup is the endpoint for registering a new user
Expand Down Expand Up @@ -264,6 +265,9 @@ func (a *API) signupNewUser(ctx context.Context, conn *storage.Connection, param
case "phone":
user, err = models.NewUser(instanceID, "", params.Password, params.Aud, params.Data)
user.Phone = storage.NullString(params.Phone)
case "eth":
user, err = models.NewUser(instanceID, "", "", params.Aud, params.Data)
user.EthAddress = storage.NullString(params.EthAddress)
default:
// handles external provider case
user, err = models.NewUser(instanceID, params.Email, params.Password, params.Aud, params.Data)
Expand All @@ -278,7 +282,7 @@ func (a *API) signupNewUser(ctx context.Context, conn *storage.Connection, param

user.Identities = make([]models.Identity, 0)

// TODO: Depcreate "provider" field
// TODO: Deprecate "provider" field
user.AppMetaData["provider"] = params.Provider

user.AppMetaData["providers"] = []string{params.Provider}
Expand Down
9 changes: 9 additions & 0 deletions conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ type EmailProviderConfiguration struct {
Enabled bool `json:"enabled" default:"true"`
}

type EthProviderConfiguration struct {
Enabled bool `json:"enabled" default:"false"`
}

type SamlProviderConfiguration struct {
Enabled bool `json:"enabled"`
MetadataURL string `json:"metadata_url" envconfig:"METADATA_URL"`
Expand Down Expand Up @@ -94,6 +98,7 @@ type ProviderConfiguration struct {
Twitch OAuthProviderConfiguration `json:"twitch"`
Email EmailProviderConfiguration `json:"email"`
Phone PhoneProviderConfiguration `json:"phone"`
Eth EthProviderConfiguration `json:"eth"`
Saml SamlProviderConfiguration `json:"saml"`
IosBundleId string `json:"ios_bundle_id" split_words:"true"`
RedirectURL string `json:"redirect_url"`
Expand Down Expand Up @@ -153,6 +158,10 @@ type SecurityConfiguration struct {
Captcha CaptchaConfiguration `json:"captcha"`
}

type Web3Configuration struct {
Enabled bool `json:"enabled" default:"false"`
}

// Configuration holds all the per-instance configuration.
type Configuration struct {
SiteURL string `json:"site_url" split_words:"true" required:"true"`
Expand Down
3 changes: 2 additions & 1 deletion example.env
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ GOTRUE_MAILER_SUBJECTS_EMAIL_CHANGE="Confirm Email Change"
GOTRUE_MAILER_SUBJECTS_INVITE="You have been invited"
GOTRUE_EXTERNAL_EMAIL_ENABLED="true"
GOTRUE_EXTERNAL_PHONE_ENABLED="true"
GOTRUE_EXTERNAL_ETH_ENABLED="true"
GOTRUE_EXTERNAL_GITHUB_ENABLED="false"
GOTRUE_EXTERNAL_GITHUB_CLIENT_ID=""
GOTRUE_EXTERNAL_GITHUB_SECRET=""
Expand Down Expand Up @@ -111,4 +112,4 @@ GOTRUE_WEBHOOK_EVENTS=validate,signup,login

GOTRUE_SECURITY_CAPTCHA_ENABLED="true"
GOTRUE_SECURITY_CAPTCHA_PROVIDER="hcaptcha"
GOTRUE_SECURITY_CAPTCHA_SECRET="0x0000000000000000000000000000000000000000"
GOTRUE_SECURITY_CAPTCHA_SECRET="0x0000000000000000000000000000000000000000"
Loading