auth icon indicating copy to clipboard operation
auth copied to clipboard

feat: add sign in with solana (EIP-4361) support

Open Bewinxed opened this issue 1 year ago • 9 comments

What kind of change does this PR introduce?

EIP4361 Auth on the backend via SIWS That can easily be extended further for any other EIP4361 compliant sign-in method.

What is the current behavior?

No onchain auth available.

What is the new behavior?

I've added a new grant type ?grant_type=eip4361, which takes the siws message and uses that to create/authenticate users using the existing methods, I've avoided modifying existing structures as much as possible unless necessary.

In the root folder, there is a external_eip4361_siws_example.go uncomment the last 3 lines and run it using go run and it will provide an example SIWS message you can test.

built it, spun up the server:

await fetch("http://localhost:9999/token?grant_type=eip4361", {
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify({"address":"J79fnBJGPeizHNYR1AGqRFRiWMQAEJLNu3ePoSWA7Zb3","chain":"solana:mainnet","grant_type":"eip4361","message":"localhost:9999 wants you to sign in with your Solana account:\nJ79fnBJGPeizHNYR1AGqRFRiWMQAEJLNu3ePoSWA7Zb3\n\nSign in with your Solana account\nURI: https://example.com\nVersion: 1\nNonce: 90cf4f06e021297d80363477774ce7f7\nIssued At: 2025-01-17T18:30:31Z\n","signature":"iWW0I/rbGwXwxj6v8oBTraQ39f84Oewo7bk7OyHwkwTx8FmswtpU+eR4gAZGrqRtrEDtXyFPuDbmYZzoEz2DDg=="})})
    .then(response => response.json())
    .then(data => console.log("Response:", data))
    .catch(error => console.error("Error:", error));

Response:

    {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzJiMmZiYi02YTM4LTQ4ZTUtOWQ0ZC0xOGJlMzMwMjFjYTIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzM3MTQyMzc2LCJpYXQiOjE3MzcxMzg3NzYsImVtYWlsIjoiIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlaXA0MzYxIiwicHJvdmlkZXJzIjpbImVpcDQzNjEiXX0sInVzZXJfbWV0YWRhdGEiOnsiY3VzdG9tX2NsYWltcyI6eyJhZGRyZXNzIjoiSjc5Zm5CSkdQZWl6SE5ZUjFBR3FSRlJpV01RQUVKTE51M2VQb1NXQTdaYjMiLCJjaGFpbiI6InNvbGFuYTptYWlubmV0Iiwicm9sZSI6ImF1dGhlbnRpY2F0ZWQifSwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwaG9uZV92ZXJpZmllZCI6ZmFsc2UsInN1YiI6InNvbGFuYTptYWlubmV0Oko3OWZuQkpHUGVpekhOWVIxQUdxUkZSaVdNUUFFSkxOdTNlUG9TV0E3WmIzIn0sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoid2ViMyIsInRpbWVzdGFtcCI6MTczNzEzODc3Nn1dLCJzZXNzaW9uX2lkIjoiMTY0NWU4MTItMzE0OC00Mjg2LTljOTItNzQyNGE0OGM3OWMzIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.yizc-IeK73DDz8xw3UBO_pLI8a_ttjldAuDokCF8y_k",
    "token_type": "bearer",
    "expires_in": 3600,
    "expires_at": 1737142376,
    "refresh_token": "J2EY1jvVd2SF343fIgVwyw",
    "user": {
        "id": "372b2fbb-6a38-48e5-9d4d-18be33021ca2",
        "aud": "authenticated",
        "role": "authenticated",
        "email": "",
        "email_confirmed_at": "2025-01-17T21:30:41.607863+03:00",
        "phone": "",
        "confirmed_at": "2025-01-17T21:30:41.607863+03:00",
        "last_sign_in_at": "2025-01-17T21:32:56.1850242+03:00",
        "app_metadata": {
            "provider": "eip4361",
            "providers": [
                "eip4361"
            ]
        },
        "user_metadata": {
            "custom_claims": {
                "address": "J79fnBJGPeizHNYR1AGqRFRiWMQAEJLNu3ePoSWA7Zb3",
                "chain": "solana:mainnet",
                "role": "authenticated"
            },
            "email_verified": false,
            "phone_verified": false,
            "sub": "solana:mainnet:J79fnBJGPeizHNYR1AGqRFRiWMQAEJLNu3ePoSWA7Zb3"
        },
        "identities": [
            {
                "identity_id": "567bf4c0-65f4-4917-aa35-96a80b6fcc5c",
                "id": "solana:mainnet:J79fnBJGPeizHNYR1AGqRFRiWMQAEJLNu3ePoSWA7Zb3",
                "user_id": "372b2fbb-6a38-48e5-9d4d-18be33021ca2",
                "identity_data": {
                    "custom_claims": {
                        "address": "J79fnBJGPeizHNYR1AGqRFRiWMQAEJLNu3ePoSWA7Zb3",
                        "chain": "solana:mainnet",
                        "role": "authenticated"
                    },
                    "email_verified": false,
                    "phone_verified": false,
                    "sub": "solana:mainnet:J79fnBJGPeizHNYR1AGqRFRiWMQAEJLNu3ePoSWA7Zb3"
                },
                "provider": "eip4361",
                "last_sign_in_at": "2025-01-17T21:30:41.589565+03:00",
                "created_at": "2025-01-17T21:30:41.590111+03:00",
                "updated_at": "2025-01-17T21:30:41.590111+03:00"
            }
        ],
        "created_at": "2025-01-17T21:30:41.579424+03:00",
        "updated_at": "2025-01-17T21:32:56.19047+03:00",
        "is_anonymous": false
    }
}

Additional context

To support this, and for it to be easily extendable, I've added this to the .env.example

# EIP-4361 OAuth config
GOTRUE_EXTERNAL_EIP4361_ENABLED="true"
GOTRUE_EXTERNAL_EIP4361_STATEMENT="Sign this message to verify your identity"
GOTRUE_EXTERNAL_EIP4361_VERSION="1"
GOTRUE_EXTERNAL_EIP4361_TIMEOUT="300s"
GOTRUE_EXTERNAL_EIP4361_DOMAIN="localhost:9999"

# Supported Chains Configuration
GOTRUE_EXTERNAL_EIP4361_SUPPORTED_CHAINS="ethereum:1,ethereum:137,solana:mainnet,solana:devnet"
GOTRUE_EXTERNAL_EIP4361_DEFAULT_CHAIN="ethereum:1"

since siws/siwe use eip4361, i added the grant type eip4361, which can extend eth/sol and any compatible network, which can be specified in the .env

then based on the chosen network e.g solana:mainnet the appropriate validation will be used.

I've implemented a siws package inside internal/utilities/siws that implement many of the necessary siws functions (need to review them to double check the validations).

for solana, I tried a no-dependency validation however i added the btc base58 package

as for ethereum, I'm, using the ethereum-go package, which is widely supported, but perhaps we can omit and try to implement a native validation without the dep.

I haven't worked much with GO specifically, but I'm the author of https://deauth.vercel.app/, which has won 4th place in the Infra track of the Solana Hyperdrive Hackathon before.

Further considerations

  • Add more handlers and add more strict verification for the message, as the SIWS protocol uses an ABNF message protocol, we can use a parser lib like https://github.com/pandatix/go-abnf which is much cleaner than regex.
  • Add account fetching/onchain verification (for example, perhaps you don't want users who make fresh, unfunded accounts) so you can probably get the balance/history of the user, and avoid blacklisted wallets via a 3rd party check).

Willing to hear your opinions, and revise the conventions. 🙏

Bewinxed avatar Jan 18 '25 14:01 Bewinxed

This is really good! Great direction!

A few general notes:

  • We need to think a bit about how the config is encoded. I get that EIP<numbers> makes a lot of sense for people deep in this matter, but it's not very descriptive for regular programmers that are self-hosting Supabase Auth. I'd say let's just do GOTRUE_EXTERNAL_WEB3_<chain> or something. If there's a new EIP later on, we'll think about its config options then.

This makes sense, originally i thought of this heirarchy:

Web3 > EIP4361 > Sign in with solana /sign in with ethereum, as there's maybe other web3 modes of auth that aren't eip4361 compliant, eth/sol go under eip4361, but what you said makes sense, I think web3 is friendly here.

  • Similarly POST /token?grant_type=<this should be readable by regular programmers>. siws, web3, siwe, things of this sort are better than eip<numbers>. This value will also be mapped into auth-js as well. Agreed
  • POST /token?grant_type=<web3 type> should only take in JSON, not URL forms. All APIs currently only use JSON, so this is a weird exception. Is there a particular reason we can't use JSON here?

I'm using JSON, is there something i missed?

func (a *API) EIP4361Grant(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
	db := a.db.WithContext(ctx)

	params := &Web3GrantParams{}
	if err := retrieveRequestParams(r, params); err != nil {
		return err
	}
...
  • utilities/siws is a weird name for a package that does eip<numbers>. Maybe just move this under /internal/web3 and it will house this and any future additions. This package is verifying Solana mainly for now, Perhaps we can do utlities/web3/[siws/siwe] ? They are both eip4361 compliant but each have different address formats, and divulge slightly in implementation.

What is the best way to test this? Any simple test app you can host somewhere?

I'll look into it and get back to you.

Bewinxed avatar Jan 18 '25 17:01 Bewinxed

I'm using JSON, is there something i missed?

I saw this which caught my eye. https://github.com/supabase/auth/pull/1918/files#diff-db4fba83e08f520d74d66c9283e5dcd938e1746de1c4e7c481cce8aff142b5a1R71-R73

This package is verifying Solana mainly for now, Perhaps we can do utlities/web3/[siws/siwe] ? They are both eip4361 compliant but each have different address formats, and divulge slightly in implementation.

No need to go into utilities IMO. Just have it be toplevel under /internal.

hf avatar Jan 18 '25 17:01 hf

Hey guys, I pushed an update a few days ago with some things I missed:

  • Added a db table for nonces, these are required to prevent Replay Attacks.
  • Added robust verifications for each part of the SIWS message.
  • I've covered the SIWS handler in the crypto package with multiple test cases.
  • I initiated the error types in the helpers to avoid initiating them inline, to prevent overhead.

Bewinxed avatar Jan 23 '25 14:01 Bewinxed

hey @Bewinxed 👋🏼 nice work! I gave this a first pass and left some comments. The replay detection looks correct, but it would be good to have a test that ensures it stays correct.

Thanks for your reviews, I’ve replied to the comments and adjusted the code accordingly.

Thanks!

Bewinxed avatar Feb 09 '25 08:02 Bewinxed

Hello! Just resolved all issues, please review one more time.

as per our discussion I’ve removed serverside nonce processing, in favor of a nonce that applies to all oidc providers, which will be implemented later.

Bewinxed avatar Feb 16 '25 06:02 Bewinxed

Before merge remember to change title into conventional commits format.

hf avatar Feb 25 '25 09:02 hf

OK we're getting very close!

Would you mind updating the openapi.yaml file with the changes to the API as well!

Also what happened with the nonce verification per the agreement to check it via the identities table? I don't think I'm able to see it in the code.

Resolved all issues, wrt nonce verification, the team has mentioned that they're going to implement nonce verification for oidc based providers and web3 later on, so simple verification is in place for now, the team is going to implement this.

Bewinxed avatar Mar 07 '25 22:03 Bewinxed

Seems good enough to go in from my POV. Still need to test E2E though and potential adjustments might come in future PRs.

Would you mind making sure the comments about validation and OpenAPI are addressed before merging?

OpenAPI should be in already, I resolved the validation stuff as well.

Bewinxed avatar Mar 11 '25 20:03 Bewinxed

Can we get an update on this? Anything we can do to help move this one?

burggraf avatar Mar 27 '25 13:03 burggraf

Pull Request Test Coverage Report for Build 14193551617

Details

  • 259 of 278 (93.17%) changed or added relevant lines in 9 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.5%) to 68.057%

Changes Missing Coverage Covered Lines Changed/Added Lines %
internal/crypto/crypto.go 9 11 81.82%
internal/models/factor.go 2 4 50.0%
internal/api/web3.go 106 121 87.6%
<!-- Total: 259 278
Totals Coverage Status
Change from base Build 14133011190: 0.5%
Covered Lines: 10489
Relevant Lines: 15412

💛 - Coveralls

coveralls avatar Apr 01 '25 10:04 coveralls