@@ -12,6 +12,7 @@ import (
12
12
"github.com/supabase/auth/internal/models"
13
13
"github.com/supabase/auth/internal/storage"
14
14
"github.com/supabase/auth/internal/utilities"
15
+ "github.com/supabase/auth/internal/utilities/siwe"
15
16
"github.com/supabase/auth/internal/utilities/siws"
16
17
)
17
18
@@ -24,7 +25,7 @@ type Web3GrantParams struct {
24
25
func (a * API ) Web3Grant (ctx context.Context , w http.ResponseWriter , r * http.Request ) error {
25
26
config := a .config
26
27
27
- if ! config .External .Web3Solana .Enabled {
28
+ if ! config .External .Web3Solana .Enabled && ! config . External . Web3Ethereum . Enabled {
28
29
return apierrors .NewUnprocessableEntityError (apierrors .ErrorCodeWeb3ProviderDisabled , "Web3 provider is disabled" )
29
30
}
30
31
@@ -33,11 +34,21 @@ func (a *API) Web3Grant(ctx context.Context, w http.ResponseWriter, r *http.Requ
33
34
return err
34
35
}
35
36
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 :
37
49
return apierrors .NewBadRequestError (apierrors .ErrorCodeWeb3UnsupportedChain , "Unsupported chain" )
38
50
}
39
51
40
- return a .web3GrantSolana (ctx , w , r , params )
41
52
}
42
53
43
54
func (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
181
192
182
193
return sendJSON (w , http .StatusOK , token )
183
194
}
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