Skip to content

Commit

Permalink
refactor(api): change HTTP method for nonce endpoint to POST for secu…
Browse files Browse the repository at this point in the history
…rity

feat(api): add address field to nonce generation for targeted nonce use
refactor(api): replace SignedMessage with Web3GrantParams for clarity
feat(api): implement nonce verification in Web3Grant flow for security
refactor(api): remove unused logging and clean up code for clarity
fix(api): correct nonce storage to include address for targeted validation
chore(api): remove Address field from Web3GrantParams as it's parsed from message

feat(crypto.go): add base32 encoding and secure alphanumeric generation
refactor(crypto.go): replace ethereum and btc libraries with uuid and internal storage for better modularity
feat(crypto.go): implement nonce verification and consumption logic for enhanced security
feat(migrations): enforce non-null constraint on address in nonces table for data integrity
  • Loading branch information
Bewinxed committed Jan 29, 2025
1 parent efb21e7 commit 15cbbe7
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 116 deletions.
2 changes: 1 addition & 1 deletion internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne

r.Use(api.isValidExternalHost)

r.Get("/nonce", api.GetNonce)
r.Post("/nonce", api.GetNonce)

r.Get("/settings", api.Settings)

Expand Down
1 change: 1 addition & 0 deletions internal/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/security"

"github.com/supabase/auth/internal/utilities"
)

Expand Down
66 changes: 38 additions & 28 deletions internal/api/provider/eip4361.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"strings"
"time"
"log"

"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/crypto"
"github.com/supabase/auth/internal/storage"
siws "github.com/supabase/auth/internal/utilities/solana"
"golang.org/x/oauth2"
)
Expand All @@ -26,10 +28,9 @@ type Web3Provider struct {
defaultChain string
}

type SignedMessage struct {
type Web3GrantParams struct {
Message string `json:"message"`
Signature string `json:"signature"`
Address string `json:"address"`
Chain string `json:"chain"`
}

Expand Down Expand Up @@ -71,35 +72,52 @@ func (p *Web3Provider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use
}

// VerifySignedMessage verifies a signed Web3 message based on the blockchain
func (p *Web3Provider) VerifySignedMessage(msg *SignedMessage) (*UserProvidedData, error) {
chain, ok := p.chains[msg.Chain]
log.Printf("Verifying supported blockchain: %s", msg.Chain)
if !ok {
return nil, fmt.Errorf("unsupported blockchain: %s", msg.Chain)
func (p *Web3Provider) VerifySignedMessage(db *storage.Connection, params *Web3GrantParams) (*UserProvidedData, error) {
var err error

parsedMessage, err := siws.ParseSIWSMessage(params.Message)

if err != nil {
return nil, siws.ErrorMalformedMessage
}



var err error
switch chain.NetworkName {
// Verify and consume nonce first
if err := crypto.VerifyAndConsumeNonce(db, parsedMessage.Nonce, parsedMessage.Address); err != nil {
return nil, siws.ErrorCodeInvalidNonce
}
network := strings.Split(params.Chain, ":")
if len(network) != 2 {
return nil, siws.ErrInvalidChainID
}
chain := network[0]
if chain == "" {
return nil, siws.ErrInvalidChainID
}

switch chain {
case BlockchainEthereum:
err = p.verifyEthereumSignature(msg)
return nil, httpError(http.StatusNotImplemented, "signature verification not implemented for %s", network)

case BlockchainSolana:
err = p.verifySolanaSignature(msg)
err = p.verifySolanaSignature(params.Signature, params.Message, parsedMessage)
default:
return nil, fmt.Errorf("signature verification not implemented for %s", chain.NetworkName)
return nil, httpError(http.StatusNotImplemented, "signature verification not implemented for %s", network)
}

if err != nil {
return nil, fmt.Errorf("signature verification failed: %w", err)
}

// Construct the provider_id as chain:address to make it unique
providerId := fmt.Sprintf("%s:%s", msg.Chain, msg.Address)
// Construct the provider_id as network:chain:address to make it unique
providerId := fmt.Sprintf("%s:%s", params.Chain, parsedMessage.Address)

return &UserProvidedData{
Metadata: &Claims{
CustomClaims: map[string]interface{}{
"address": msg.Address,
"chain": msg.Chain,
"address": parsedMessage.Address,
"chain": parsedMessage.ChainID,
"role": "authenticated",
},
Subject: providerId, // This becomes the provider_id in the identity
Expand All @@ -108,18 +126,10 @@ func (p *Web3Provider) VerifySignedMessage(msg *SignedMessage) (*UserProvidedDat
}, nil
}

func (p *Web3Provider) verifyEthereumSignature(msg *SignedMessage) error {
return crypto.VerifyEthereumSignature(msg.Message, msg.Signature, msg.Address)
}

func (p *Web3Provider) verifySolanaSignature(msg *SignedMessage) error {
parsedMessage, err := siws.ParseSIWSMessage(msg.Message)
if err != nil {
return fmt.Errorf("failed to parse SIWS message: %w", err)
}

func (p *Web3Provider) verifySolanaSignature(signature string, rawMessage string, msg *siws.SIWSMessage) error {
// Decode base64 signature into bytes
sigBytes, err := base64.StdEncoding.DecodeString(msg.Signature)
sigBytes, err := base64.StdEncoding.DecodeString(string(signature))
if err != nil {
return fmt.Errorf("invalid signature encoding: %w", err)
}
Expand All @@ -130,7 +140,7 @@ func (p *Web3Provider) verifySolanaSignature(msg *SignedMessage) error {
TimeDuration: p.config.Timeout,
}

if err := crypto.VerifySIWS(msg.Message, sigBytes, parsedMessage, params); err != nil {
if err := crypto.VerifySIWS(rawMessage, sigBytes, msg, params); err != nil {
return err
}

Expand Down
92 changes: 23 additions & 69 deletions internal/api/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package api

import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"strconv"
Expand All @@ -21,7 +20,6 @@ import (
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/observability"
"github.com/supabase/auth/internal/storage"
siws "github.com/supabase/auth/internal/utilities/solana"
)

// AccessTokenClaims is a struct thats used for JWT claims
Expand Down Expand Up @@ -317,7 +315,7 @@ func (a *API) PKCE(ctx context.Context, w http.ResponseWriter, r *http.Request)
type StoredNonce struct {
ID uuid.UUID `db:"id"`
Nonce string `db:"nonce"`
Address sql.NullString `db:"address"` // Changed this line
Address string `db:"address"`
CreatedAt time.Time `db:"created_at"`
ExpiresAt time.Time `db:"expires_at"`
Used bool `db:"used"`
Expand All @@ -330,10 +328,22 @@ func (a *API) GetNonce(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)

nonce := crypto.GenerateOtp(12)
var body struct {
Address string `json:"address"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return badRequestError(ErrorCodeBadJSON, "Invalid request body: %v", err)
}

if body.Address == "" {
return badRequestError(ErrorCodeBadJSON, "Missing required field: address")
}

nonce := crypto.SecureAlphanumeric(12)

storedNonce := &StoredNonce{
ID: uuid.Must(uuid.NewV4()),
Address: body.Address,
Nonce: nonce,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(NonceExpiration),
Expand All @@ -343,66 +353,24 @@ func (a *API) GetNonce(w http.ResponseWriter, r *http.Request) error {
err := db.Transaction(func(tx *storage.Connection) error {
// Store the nonce
_, err := tx.TX.Exec(`
INSERT INTO auth.nonces (id, nonce, created_at, expires_at, used)
VALUES ($1, $2, $3, $4, $5)
`, storedNonce.ID, storedNonce.Nonce, storedNonce.CreatedAt,
storedNonce.ExpiresAt, storedNonce.Used)
INSERT INTO auth.nonces (id, address, nonce, created_at, expires_at, used)
VALUES ($1, $2, $3, $4, $5, $6)
`, storedNonce.ID, storedNonce.Address, storedNonce.Nonce,
storedNonce.CreatedAt, storedNonce.ExpiresAt, storedNonce.Used)
return err
})

if err != nil {
return internalServerError("Error storing nonce").WithInternalError(err)
return internalServerError("DB error while storing nonce: %v", err)
}

log.Printf("Generated nonce: %s", nonce)


return sendJSON(w, http.StatusOK, map[string]interface{}{
"nonce": nonce,
"expiresAt": storedNonce.ExpiresAt,
})
}

func (a *API) verifyAndConsumeNonce(ctx context.Context, nonce string, address string) error {

log.Printf("Starting nonce verification for: %s", nonce)
db := a.db.WithContext(ctx)

var storedNonce StoredNonce
err := db.Transaction(func(tx *storage.Connection) error {
// Find the nonce
log.Printf("Executing query for nonce: %s", nonce)
err := tx.TX.QueryRow(`
SELECT id, nonce, address, created_at, expires_at, used
FROM auth.nonces
WHERE nonce = $1 AND used = false
`, nonce).Scan(&storedNonce.ID, &storedNonce.Nonce,
&storedNonce.Address, &storedNonce.CreatedAt,
&storedNonce.ExpiresAt, &storedNonce.Used)
if err != nil {
log.Printf("Error scanning nonce: %v", err)
return err
}

log.Printf("Found nonce in DB: %+v", storedNonce)


// Check expiration
if time.Now().After(storedNonce.ExpiresAt) {
return fmt.Errorf("nonce expired")
}

// Mark as used
_, err = tx.TX.Exec(`
UPDATE auth.nonces
SET used = true, address = $1
WHERE id = $2
`, sql.NullString{String: address, Valid: true}, storedNonce.ID)
return err
})

return err
}


func (a *API) Web3Grant(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
Expand All @@ -413,33 +381,19 @@ func (a *API) Web3Grant(ctx context.Context, w http.ResponseWriter, r *http.Requ
return err
}

parsedMessage, err := siws.ParseSIWSMessage(params.Message)

if err != nil {
return siws.ErrorMalformedMessage
}



// Verify and consume nonce first
if err := a.verifyAndConsumeNonce(ctx, parsedMessage.Nonce, parsedMessage.Address); err != nil {
return siws.ErrorCodeInvalidNonce
}

web3Provider, err := provider.NewWeb3Provider(ctx, a.config.External.Web3)
if err != nil {
return err
}

// Convert params to SignedMessage
msg := &provider.SignedMessage{
msg := &provider.Web3GrantParams{
Message: params.Message,
Signature: params.Signature,
Address: params.Address,
Chain: params.Chain,
}

userData, err := web3Provider.VerifySignedMessage(msg)
userData, err := web3Provider.VerifySignedMessage(db, msg)
if err != nil {
return oauthError("invalid_grant", "Signature verification failed").WithInternalError(err)
}
Expand All @@ -457,7 +411,7 @@ func (a *API) Web3Grant(ctx context.Context, w http.ResponseWriter, r *http.Requ
if terr := models.NewAuditLogEntry(r, tx, user, models.LoginAction, "", map[string]interface{}{
"provider": "web3",
"chain": msg.Chain,
"address": msg.Address,
"address": userData.Metadata.CustomClaims["address"],
}); terr != nil {
return terr
}
Expand Down
2 changes: 0 additions & 2 deletions internal/api/web3.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@ package api
type Web3GrantParams struct {
Message string `json:"message"`
Signature string `json:"signature"`
Address string `json:"address"`
Chain string `json:"chain"`
Nonce string `json:"nonce"` // Added nonce field
}
Loading

0 comments on commit 15cbbe7

Please sign in to comment.