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 all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ coverage.out

.DS_Store
.vscode
.idea
www/dist/
www/.DS_Store
www/node_modules
Expand Down
8 changes: 8 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
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).Post("/crypto", api.Crypto)
r.With(api.limitHandler(
// Allow requests at a rate of 30 per 5 minutes.
tollbooth.NewLimiter(30.0/(60*5), &limiter.ExpirableOptions{
DefaultExpirationTTL: time.Hour,
}).SetBurst(30),
)).With(api.verifyCaptcha).Post("/nonce", api.Nonce)

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

r.With(api.limitHandler(
Expand Down
166 changes: 166 additions & 0 deletions api/crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package api

import (
"bytes"
"encoding/json"
"github.com/gofrs/uuid"
"github.com/netlify/gotrue/api/crypto_provider"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This style of naming packages is not common in Go code. Can you rename it to crypto-provider or something else?

"github.com/netlify/gotrue/metering"
"github.com/netlify/gotrue/models"
"github.com/netlify/gotrue/storage"
"io/ioutil"
"net/http"
"strings"
)

// CryptoParams contains the request body params for the eth endpoint, all values hex encoded
type CryptoParams struct {
Provider string `json:"provider"`
NonceId *string `json:"nonce_id"`
Signature *string `json:"signature"`
}

func (a *API) Crypto(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
config := a.getConfig(ctx)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK this method is no longer there.

instanceID := getInstanceID(ctx)
useCookie := r.Header.Get(useCookieHeader)

// Get the params
params := &CryptoParams{}
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)
}
Comment on lines +31 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the getBody function instead.


// Get the crypto provider
provider, err := crypto_provider.GetCryptoProvider(config, params.Provider)
if err != nil {
return badRequestError(err.Error())
}

var nonce *models.Nonce = nil
if provider.RequiresNonce() {
if params.NonceId == nil {
return badRequestError("Missing `nonce_id` which is required by %s provider", strings.ToLower(params.Provider))
}

if params.Signature == nil {
return badRequestError("Missing `signature` which is required by %s provider", strings.ToLower(params.Provider))
}

// Get the nonce from the id in params
nonce, err = models.GetNonceById(a.db, instanceID, uuid.FromStringOrNil(*params.NonceId))
if err != nil {
return badRequestError("Failed to find nonce: %v", err)
}

safe, err := provider.ValidateNonce(nonce, *params.Signature)
if !safe {
return badRequestError(err.Error())
}

if err != nil {
return internalServerError(err.Error())
}
}

didUserExist := true

aud := a.requestAud(ctx, r)
user, uerr := provider.FetchUser(a.db, instanceID, aud, nonce)

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 {
accountInfo, uerr := provider.FetchAccountInformation(nonce)
if uerr != nil {
return uerr
}

user, uerr = a.signupNewUser(ctx, tx, &SignupParams{
CryptoAddress: accountInfo.Address,
Provider: "crypto",
CryptoProvider: params.Provider,
Aud: aud,
})
didUserExist = false

identity, terr := a.createNewIdentity(tx, user, params.Provider, map[string]interface{}{"sub": accountInfo.Address, "address": accountInfo.Address})
if terr != nil {
return terr
}

user.Identities = []models.Identity{*identity}

if uerr = user.Confirm(tx); uerr != nil {
return uerr
}

if uerr = models.NewAuditLogEntry(r, 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
Comment on lines +85 to +111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This account creation logic does not take many edge cases into account which we're trying to resolve systematically in GoTrue. Accounts may need to be linked -- assigned a linking domain and linking rules -- or be identified separately (which today is only possible with SSO accounts).

})

if uerr != nil {
return uerr
}
}

err = a.db.Transaction(func(tx *storage.Connection) error {
// Consume the nonce
if terr := nonce.Consume(tx); terr != nil {
return terr
}

// Add audit log entry for consuming nonce
return models.NewAuditLogEntry(r, tx, instanceID, user, models.NonceConsumed, "", nil)
})

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(r, 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.setCookieTokens(config, token, useCookie == useSessionCookie, w); terr != nil {
return internalServerError("Failed to set JWT cookie. %s", terr)
}
}
return nil
})
if err != nil {
return err
}
metering.RecordLogin("crypto", user.ID, instanceID)
token.User = user

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

import (
"fmt"
"github.com/gofrs/uuid"
"github.com/netlify/gotrue/conf"
"github.com/netlify/gotrue/models"
"github.com/netlify/gotrue/storage"
"net/http"
"strings"
)

type CryptoAccountInformation struct {
Address string `json:"address"`
}

type CryptoNonceOptions struct {
WalletAddress string `json:"wallet_address"` // Hex Encoded
Url string `json:"url"`
// Option as only used by EVM
ChainId *int `json:"chain_id"`
}

type CryptoProvider interface {
// RequiresNonce Used to skip nonces for crypto providers that don't use nonces
RequiresNonce() bool

// GenerateNonce Generate the nonce model for the provider
GenerateNonce(req *http.Request, instanceId uuid.UUID, options CryptoNonceOptions) (*models.Nonce, error)
// BuildNonce Build the nonce into a string
BuildNonce(nonce *models.Nonce) (string, error)
// ValidateNonce Validate the nonce against a signature
ValidateNonce(nonce *models.Nonce, signature string) (bool, error)

// FetchUser Fetch the user for a nonce
FetchUser(tx *storage.Connection, instanceId uuid.UUID, aud string, nonce *models.Nonce) (*models.User, error)

// FetchAccountInformation Fetch account information for a new account
FetchAccountInformation(nonce *models.Nonce) (*CryptoAccountInformation, error)
}

func GetCryptoProvider(config *conf.Configuration, name string) (CryptoProvider, error) {
name = strings.ToLower(name)

switch name {
case "eth":
return NewEthProvider(&config.External.Eth)
default:
return nil, fmt.Errorf("crypto provider %s could not be found", name)
}
}
90 changes: 90 additions & 0 deletions api/crypto_provider/eth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package crypto_provider

import (
"fmt"
"github.com/gofrs/uuid"
"github.com/netlify/gotrue/conf"
"github.com/netlify/gotrue/models"
"github.com/netlify/gotrue/storage"
"github.com/spruceid/siwe-go"
"net/http"
"net/url"
"strconv"
)

type EthProvider struct {
config *conf.EthProviderConfiguration
}

var _ CryptoProvider = (*EthProvider)(nil)

func NewEthProvider(config *conf.EthProviderConfiguration) (*EthProvider, error) {
return &EthProvider{
config: config,
}, nil
}

func (e *EthProvider) RequiresNonce() bool {
return true
}

func (e *EthProvider) GenerateNonce(_ *http.Request, instanceId uuid.UUID, options CryptoNonceOptions) (*models.Nonce, error) {
uri, err := url.Parse(options.Url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably ParseRequestURI is required here?


if err != nil {
return nil, err
}

if options.ChainId == nil {
return nil, fmt.Errorf("eth provider requires a `chain_id` be provided")
}

return models.NewNonce(instanceId, *options.ChainId, "eth", uri.String(), uri.Hostname(), options.WalletAddress, "eip155")
}

func (e *EthProvider) BuildNonce(n *models.Nonce) (string, error) {
msg, err := e.toSiweMessage(n)

if err != nil {
return "", err
}

return msg.String(), nil
}

func (e *EthProvider) ValidateNonce(nonce *models.Nonce, signature string) (bool, error) {
nonceMessage, err := e.toSiweMessage(nonce)
if err != nil {
return false, err
}

_, err = nonceMessage.Verify(signature, &nonce.Hostname, nil, nil)
if err != nil {
return false, err
}

return true, nil
}

// Used internally to convert to a SIWE message
func (e *EthProvider) toSiweMessage(n *models.Nonce) (*siwe.Message, error) {
return siwe.InitMessage(n.Hostname, n.Address, n.Url, n.Nonce, map[string]interface{}{
"statement": e.config.Message,
"issuedAt": n.UpdatedAt,
"nonce": n.Nonce,
"chainId": strconv.Itoa(n.ChainId),
"expirationTime": n.ExpiresAt,
})
}

func (e *EthProvider) FetchUser(tx *storage.Connection, instanceId uuid.UUID, aud string, nonce *models.Nonce) (*models.User, error) {
// Because we have the address in the nonce we can just request the using that address
return models.FindUserByCryptoAddressAndAudience(tx, instanceId, nonce.Address, aud)
}

func (e *EthProvider) FetchAccountInformation(nonce *models.Nonce) (*CryptoAccountInformation, error) {
// Eth Provider has the address in the nonce, so it can just be returned
return &CryptoAccountInformation{
Address: nonce.Address,
}, nil
}
Loading