@@ -12,6 +12,7 @@ import (
1212 "github.com/supabase/auth/internal/models"
1313 "github.com/supabase/auth/internal/storage"
1414 "github.com/supabase/auth/internal/utilities"
15+ "github.com/supabase/auth/internal/utilities/siwe"
1516 "github.com/supabase/auth/internal/utilities/siws"
1617)
1718
@@ -24,7 +25,7 @@ type Web3GrantParams struct {
2425func (a * API ) Web3Grant (ctx context.Context , w http.ResponseWriter , r * http.Request ) error {
2526 config := a .config
2627
27- if ! config .External .Web3Solana .Enabled {
28+ if ! config .External .Web3Solana .Enabled && ! config . External . Web3Ethereum . Enabled {
2829 return apierrors .NewUnprocessableEntityError (apierrors .ErrorCodeWeb3ProviderDisabled , "Web3 provider is disabled" )
2930 }
3031
@@ -33,11 +34,21 @@ func (a *API) Web3Grant(ctx context.Context, w http.ResponseWriter, r *http.Requ
3334 return err
3435 }
3536
36- if params .Chain != "solana" {
37+ switch params .Chain {
38+ case "solana" :
39+ if ! config .External .Web3Solana .Enabled {
40+ return apierrors .NewUnprocessableEntityError (apierrors .ErrorCodeWeb3ProviderDisabled , "Solana Web3 provider is disabled" )
41+ }
42+ return a .web3GrantSolana (ctx , w , r , params )
43+ case "ethereum" :
44+ if ! config .External .Web3Ethereum .Enabled {
45+ return apierrors .NewUnprocessableEntityError (apierrors .ErrorCodeWeb3ProviderDisabled , "Ethereum Web3 provider is disabled" )
46+ }
47+ return a .web3GrantEthereum (ctx , w , r , params )
48+ default :
3749 return apierrors .NewBadRequestError (apierrors .ErrorCodeWeb3UnsupportedChain , "Unsupported chain" )
3850 }
3951
40- return a .web3GrantSolana (ctx , w , r , params )
4152}
4253
4354func (a * API ) web3GrantSolana (ctx context.Context , w http.ResponseWriter , r * http.Request , params * Web3GrantParams ) error {
@@ -181,3 +192,128 @@ func (a *API) web3GrantSolana(ctx context.Context, w http.ResponseWriter, r *htt
181192
182193 return sendJSON (w , http .StatusOK , token )
183194}
195+
196+ func (a * API ) web3GrantEthereum (ctx context.Context , w http.ResponseWriter , r * http.Request , params * Web3GrantParams ) error {
197+ config := a .config
198+ db := a .db .WithContext (ctx )
199+
200+ if len (params .Message ) < 64 {
201+ return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "message is too short" )
202+ } else if len (params .Message ) > 20 * 1024 {
203+ return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "message must not exceed 20KB" )
204+ }
205+
206+ if len (strings .TrimPrefix (params .Signature , "0x" )) != 130 {
207+ return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "signature must be 65 bytes long, encoded as a 130 character-long hexadecimal string" )
208+ }
209+
210+ parsedMessage , err := siwe .ParseMessage (params .Message )
211+ if err != nil {
212+ return apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , err .Error ())
213+ }
214+
215+ if ! parsedMessage .VerifySignature (params .Signature ) {
216+ return apierrors .NewOAuthError ("invalid_grant" , "Signature does not match address in message" )
217+ }
218+
219+ if parsedMessage .URI .Scheme != "https" && parsedMessage .URI .Hostname () != "localhost" {
220+ return apierrors .NewOAuthError ("invalid_grant" , "Signed Ethereum message is using URI which does not use HTTPS" )
221+ }
222+
223+ if ! utilities .IsRedirectURLValid (config , parsedMessage .URI .String ()) {
224+ return apierrors .NewOAuthError ("invalid_grant" , "Signed Ethereum message is using URI which is not allowed on this server, message was signed for another app" )
225+ }
226+
227+ if parsedMessage .URI .Hostname () != "localhost" && (parsedMessage .URI .Host != parsedMessage .Domain || ! utilities .IsRedirectURLValid (config , "https://" + parsedMessage .Domain + "/" )) {
228+ return apierrors .NewOAuthError ("invalid_grant" , "Signed Ethereum message is using a Domain that does not match the one in URI which is not allowed on this server" )
229+ }
230+
231+ now := a .Now ()
232+
233+ if parsedMessage .NotBefore != nil && ! parsedMessage .NotBefore .IsZero () && now .Before (* parsedMessage .NotBefore ) {
234+ return apierrors .NewOAuthError ("invalid_grant" , "Signed Ethereum message becomes valid in the future" )
235+ }
236+
237+ if parsedMessage .NotBefore != nil && parsedMessage .ExpirationTime != nil && ! parsedMessage .ExpirationTime .IsZero () && now .After (* parsedMessage .ExpirationTime ) {
238+ return apierrors .NewOAuthError ("invalid_grant" , "Signed Ethereum message is expired" )
239+ }
240+
241+ latestExpiryAt := parsedMessage .IssuedAt .Add (config .External .Web3Ethereum .MaximumValidityDuration )
242+
243+ if now .After (latestExpiryAt ) {
244+ return apierrors .NewOAuthError ("invalid_grant" , "Ethereum message was issued too long ago" )
245+ }
246+
247+ earliestIssuedAt := parsedMessage .IssuedAt .Add (- config .External .Web3Ethereum .MaximumValidityDuration )
248+
249+ if now .Before (earliestIssuedAt ) {
250+ return apierrors .NewOAuthError ("invalid_grant" , "Ethereum message was issued too far in the future" )
251+ }
252+
253+ const providerType = "web3"
254+ providerId := strings .Join ([]string {
255+ providerType ,
256+ params .Chain ,
257+ parsedMessage .Address ,
258+ }, ":" )
259+
260+ userData := provider.UserProvidedData {
261+ Metadata : & provider.Claims {
262+ CustomClaims : map [string ]interface {}{
263+ "address" : parsedMessage .Address ,
264+ "chain" : params .Chain ,
265+ "network" : parsedMessage .ChainID ,
266+ "domain" : parsedMessage .Domain ,
267+ "statement" : parsedMessage .Statement ,
268+ },
269+ Subject : providerId ,
270+ },
271+ Emails : []provider.Email {},
272+ }
273+
274+ var token * AccessTokenResponse
275+ var grantParams models.GrantParams
276+ grantParams .FillGrantParams (r )
277+
278+ if err := a .triggerBeforeUserCreatedExternal (r , db , & userData , providerType ); err != nil {
279+ return err
280+ }
281+
282+ err = db .Transaction (func (tx * storage.Connection ) error {
283+ user , terr := a .createAccountFromExternalIdentity (tx , r , & userData , providerType )
284+ if terr != nil {
285+ return terr
286+ }
287+
288+ if terr := models .NewAuditLogEntry (config .AuditLog , r , tx , user , models .LoginAction , "" , map [string ]interface {}{
289+ "provider" : providerType ,
290+ "chain" : params .Chain ,
291+ "network" : parsedMessage .ChainID ,
292+ "address" : parsedMessage .Address ,
293+ "domain" : parsedMessage .Domain ,
294+ "uri" : parsedMessage .URI ,
295+ }); terr != nil {
296+ return terr
297+ }
298+
299+ token , terr = a .issueRefreshToken (r , tx , user , models .Web3 , grantParams )
300+ if terr != nil {
301+ return terr
302+ }
303+
304+ return nil
305+ })
306+
307+ if err != nil {
308+ switch err .(type ) {
309+ case * storage.CommitWithError :
310+ return err
311+ case * HTTPError :
312+ return err
313+ default :
314+ return apierrors .NewOAuthError ("server_error" , "Internal Server Error" ).WithInternalError (err )
315+ }
316+ }
317+
318+ return sendJSON (w , http .StatusOK , token )
319+ }
0 commit comments