-
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
Conversation
* This is the first attempt and has flaws will most likely be reverted to try again!
Use crypto secure token function
Also adds them to the example.env file
Looking great so far! A couple things to keep in mind: Nonce should regenerate on every auth request |
Initial work on getting nonces stored in the database, also did some changes to the api route with a todo to implement saving to the database.
* Validating Signature, Nonce & Wallet Address * Generating Nonce * Starting to save nonce to database * new Audit log type * New deps * Update SQL * Update nonce struct * New migration for nonce
* User signup with Wallet Address * Get nonce by ID * Remove consumed_at
I've now done as much as I can for the API without starting to make changes to https://github.com/Supabase/gotrue-js, I know @Rahat-ch said they wanted to try this. However, I think it could be done faster and with less friction If I was to start the PR and @Rahat-ch can take over and optimise it later on as its inevitable I'll make a few mistakes |
That is how everything is working! It should be even more clear now |
The initial JS lib changes are being worked on supabase/auth-js#185, afaik @Rahat-ch was going to take over so here was the start of a testing file I made. Also that PR will needs tons of changed reach out if you need perms to my branch! import { GoTrueClient } from ".";
const client = new GoTrueClient({
url: "http://localhost:9999"
});
(async () => {
const nonce = await client.getNonce();
// TODO: Sign Nonce
const {session, error} = await client.signIn({
web3: {
wallet_address: "",
nonce: nonce.data!.nonce,
signature: ""
}
});
console.log(JSON.stringify(nonce))
})() |
Create migration to add the new wallet_address on users table
Unsure as to why tests are failing currently, will review this in more detail when it's time to think about moving the PR out of the draft stage |
* Migrate Web3 -> Eth * Change nonce to support ETH standards * Misc Cleanup
* Fixes validation, sends not finished message when logging in as rest of auth needs to be fixed
Ready for review now 🎉. There is a demo in this tweet: https://twitter.com/i/status/1464678182372655110 |
Awesome job! Do you think we could also customize the message to sign there? |
No, as the nonce conforms to standards and it would be hard to track if you allowed custom "nonces" |
Yup, the server has to know this message too. Would be awesome if it could be somehow integrated in the supabase UI somewhere - ie if we could define the message we want on the app with a placeholder for the nonce like "hey, please sign this message. Nonce: {nonce}". |
Can you elaborate on what you mean? The client dose receive the nonce btw! Also this was made with help from the wallet connect team. Originally the nonce was just a random value but after recommendation from them it was moved to how it is now and it follows this: https://github.com/ethereum/EIPs/blob/75e2bd7f6afc6b6ed3f5a064e3ddf8c60a562e00/EIPS/eip-4361.md You can see the reference implementation here: https://github.com/HarryET/walletconnect-supabase-example |
This can now be tested on the demo, the only thing missing in this PR are tests. Any other comments are appreciated in regards to parts of the PR that are done incorrectly or are missing. |
Flagged as issue by SpruceID as not conformant to spec
Should this be marked as ready for review? Thanks for your work btw! |
No @Xanewok, this will stay as draft till the Supabase team are happy with the security side. There open internal channels of communication to keep this PR moving. Refer too @kiwicopple's reason for closing this issue here for an explanation from the Supabase team: supabase/supabase#6474 (comment) |
Really need this. What can we do to merge this? @kiwicopple Even though the eip is still in draft, it is already widely used. I need the magic.link wallet though this also means that I cannot use magic.link to authenticate with Supabase while having access to the wallet. This pr would allow me to use the magic.link wallet while using this wallet to sign the user into Supabase. |
Hello @byterose, I see you have also commented on the demo repo. This PR changes the core of GoTrue by adding a brand new method of auth that could have added vulnerabilities to GoTrue; as such this PR may take some time. Having people keep replying here with some version of "I need this" will not help. Also pinging the Supabase team will not help speed this up. As I have previously started, there are internal communication tracks between myself, Supabase and other parties. And updates will be posted here as it is applicable for people to know. |
Hey all, dropping a note here so that everyone knows where we're at. At this stage we are putting this on hold for the following reasons:
We'll revisit this once we have completed |
Fix audit entries
Migrate the implementation to support multiple crypto providers
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.
Hey @HarryET, here's my feedback (I'm back from holiday!). Let me know on Slack or here if you want to discuss more about it.
"bytes" | ||
"encoding/json" | ||
"github.com/gofrs/uuid" | ||
"github.com/netlify/gotrue/api/crypto_provider" |
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?
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) | ||
} |
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.
Please use the getBody
function instead.
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK this method is no longer there.
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 |
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 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).
} | ||
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Probably ParseRequestURI
is required here?
case "crypto": | ||
user, err = models.NewUser(instanceID, "", "", params.Password, params.Aud, params.Data) | ||
cryptoAddress, err = models.NewCryptoAddress(instanceID, user.ID, params.CryptoProvider, params.CryptoAddress) |
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.
I would not put crypto "sign up" in the sign up method as it's getting complex. Can you pull this out in another API that can be easily turned on / off instead? Does that make sense?
Are you sure we want users with no email and phone address to be added here? Sounds like a dangerous thing if someone can just pile data in the database without any identifying information?
CREATE TABLE IF NOT EXISTS auth.identities ( | ||
id text NOT NULL, | ||
user_id uuid NOT NULL, | ||
identity_data JSONB NOT NULL, | ||
provider text NOT NULL, | ||
CREATE TABLE IF NOT EXISTS auth.identities | ||
( | ||
id text NOT NULL, | ||
user_id uuid NOT NULL, | ||
identity_data JSONB NOT NULL, | ||
provider text NOT NULL, | ||
last_sign_in_at timestamptz NULL, | ||
created_at timestamptz NULL, | ||
updated_at timestamptz NULL, | ||
created_at timestamptz NULL, | ||
updated_at timestamptz NULL, | ||
CONSTRAINT identities_pkey PRIMARY KEY (provider, id), | ||
CONSTRAINT identities_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE | ||
CONSTRAINT identities_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE | ||
); | ||
COMMENT ON TABLE auth.identities is 'Auth: Stores identities associated to a user.'; |
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.
Existing migration files must not have any changes.
CREATE TABLE auth.crypto_addresses | ||
( | ||
instance_id uuid NULL, | ||
id uuid NOT NULL, | ||
|
||
account_id uuid NOT NULL REFERENCES auth.users (id), | ||
address varchar(255) NOT NULL UNIQUE, | ||
provider varchar(255) NOT NULL, | ||
|
||
created_at timestamptz NULL DEFAULT now() | ||
); | ||
|
||
CREATE INDEX IF NOT EXISTS crypto_addresses_instance_id_idx ON auth.crypto_addresses USING btree (instance_id); | ||
comment on table auth.crypto_addresses is 'Auth: Stored Cryptocurrency addresses for web3 authentication.'; |
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.
To me, these seem to belong better inside auth.identities
as a (id
, provider
) pair. For example provider could be web3:<chain>
and ID could be the 0x...
address.
type CryptoAddress struct { | ||
InstanceID uuid.UUID `json:"-" db:"instance_id"` | ||
ID uuid.UUID `json:"id" db:"id"` | ||
|
||
AccountId uuid.UUID `json:"account_id" db:"account_id"` | ||
Address string `json:"address" db:"address"` | ||
Provider string `json:"provider" db:"provider"` | ||
|
||
CreatedAt time.Time `json:"created_at" db:"created_at"` | ||
} |
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.
Ditto. This whole logic seems to be a specialization of the auth.identities
table.
type Nonce struct { | ||
InstanceID uuid.UUID `json:"-" db:"instance_id"` | ||
ID uuid.UUID `json:"id" db:"id"` | ||
Provider string `json:"provider" db:"provider"` | ||
|
||
Url string `json:"url" db:"uri"` | ||
Hostname string `json:"hostname" db:"hostname"` | ||
|
||
ChainId int `json:"chain_id" db:"chain_id"` | ||
Address string `json:"address" db:"address"` | ||
Namespace string `json:"namespace" db:"namespace"` | ||
Nonce string `json:"nonce" db:"nonce"` | ||
|
||
CreatedAt time.Time `json:"created_at" db:"created_at"` | ||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"` | ||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"` | ||
} |
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.
Why are nonces stored in the database? Can't they be cryptographically signed and verified without storing anything in the database?
What kind of change does this PR introduce?
Adds new endpoints:
/nonce
/nonce/{id}
/eth
What is the current behavior?
What is the new behavior?
Additional context
https://goethereumbook.org/signatures/