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

Implement EIP-4361 support with SIWS message handling and verification #1918

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

Bewinxed
Copy link

@Bewinxed Bewinxed commented Jan 18, 2025

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 Bewinxed requested a review from a team as a code owner January 18, 2025 14:31
@@ -10,7 +10,7 @@ GOTRUE_JWT_ADMIN_ROLES="supabase_admin,service_role"
# Database & API connection details
GOTRUE_DB_DRIVER="postgres"
DB_NAMESPACE="auth"
DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5432/postgres"
DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5433/postgres"
Copy link
Contributor

Choose a reason for hiding this comment

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

Was there a reason for this change?

Copy link
Author

Choose a reason for hiding this comment

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

my bad, I changed it to use the docker postgres in the repo instead of my local pg, will revert this.

Comment on lines 12 to 20
// GenerateNonce creates a random 16-byte nonce, returning a hex-encoded string.
func GenerateNonce() (string, error) {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Move this to crypto package / re-use something in it?

Copy link
Author

Choose a reason for hiding this comment

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

the siws folder was supposed to be a separate package that could be deployed separately, I just switched it to use nonce := crypto.SecureToken() would that work?

Comment on lines 45 to 50
validNetworks := map[string]bool{
"mainnet": true,
"devnet": true,
"testnet": true,
}
return validNetworks[strings.ToLower(network)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Use a switch instead, it's a lot more efficient than maps.

"github.com/ethereum/go-ethereum/crypto"
)

func VerifySignature(message string, signature string, address string) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise, 100% coverage needed here.

Comment on lines 347 to 357
type EIP4361Configuration struct {
Enabled bool `json:"enabled" default:"false" split_words:"true"`
Domain string `json:"domain" required:"true" split_words:"true"`
Statement string `json:"statement" split_words:"true"`
Version string `json:"version" default:"1" split_words:"true"`
Timeout time.Duration `json:"timeout" default:"300s" split_words:"true"`

// Comma-separated list of supported chains (e.g. "ethereum:1,ethereum:137,solana:mainnet")
SupportedChains string `json:"supported_chains" split_words:"true"`
DefaultChain string `json:"default_chain" split_words:"true"`
}
Copy link
Contributor

Choose a reason for hiding this comment

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

So is EIP4361 fully chain independent?

Copy link
Author

Choose a reason for hiding this comment

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

So is EIP4361 fully chain independent?

Supposedly so, ETH implemented it first then SOL followed after, with very minimal deviation.

However, methods to check addresses/verify them are different per chain, I'm also expecting a future requirement to get account data so that's why i structured it this way.

Copy link
Contributor

@hf hf left a comment

Choose a reason for hiding this comment

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

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.
  • 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.
  • 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?
  • 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.

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

@Bewinxed
Copy link
Author

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.

@hf
Copy link
Contributor

hf commented Jan 18, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants