-
Notifications
You must be signed in to change notification settings - Fork 372
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
Changes from all commits
af59c7a
3ed3025
e2a64b4
c08fce6
f8997b8
8fe0e9a
25a0e1c
66246ba
30a0b1f
3685c8c
9fe34cf
2a4efea
c1f88b7
a7e68fa
3c1f18a
645a642
0db2c53
eca0371
e7c9720
1bd3fa9
4184e15
84cdfa2
b75c60a
2177ef0
5be052c
95b78da
2e2954b
96b0665
9c6aa97
57cfd08
2ad9664
55cf1d8
ddab9b9
8d97a05
cad6ae6
0815284
a038dd2
7290cf0
7fb9875
b081a15
7967abc
1f3945c
b625e60
653da97
e423dcd
b123739
ec1c086
17b6316
844bf6b
9185e33
1cd8993
a04462d
8d3180c
816a647
0fa4b5d
fbd0466
8c3115e
67a0c08
32accf8
977d8d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ coverage.out | |
|
||
.DS_Store | ||
.vscode | ||
.idea | ||
www/dist/ | ||
www/.DS_Store | ||
www/node_modules | ||
|
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" | ||
"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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use the |
||
|
||
// 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
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) | ||
} | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably |
||
|
||
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 | ||
} |
There was a problem hiding this comment.
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?