Skip to content
1 change: 1 addition & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func Routes() []*router.Route {
router.NewRoute("POST", "/auth/login", handlers.AuthLogin),
router.NewRoute("POST", "/auth/login-with-password", handlers.AuthLoginWithPassword),
router.NewRoute("POST", "/auth/verify", handlers.AuthVerify),
router.NewRoute("POST", "/auth/wallet-challenge", handlers.AuthWalletChallenge),
router.NewRoute("GET", "/auth/refresh", handlers.AuthRefreshToken),
router.NewRoute("POST", "/auth/verify-send", handlers.AuthVerifySend),
router.NewRoute("POST", "/auth/password-reset", handlers.AuthPasswordReset),
Expand Down
12 changes: 12 additions & 0 deletions api/handlers/wallet_auth_challenge_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package handlers

import (
"net/http"

"github.com/urnetwork/server/controller"
"github.com/urnetwork/server/router"
)

func AuthWalletChallenge(w http.ResponseWriter, r *http.Request) {
router.WrapWithInputNoAuth(controller.AuthWalletChallenge, w, r)
}
18 changes: 18 additions & 0 deletions controller/auth_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ var SsoRedirectUrl = sync.OnceValue(func() string {
return c["web_connect"].(map[string]any)["redirect_url"].(string)
})

type AuthWalletChallengeArgs struct {
WalletAddress *string `json:"wallet_address,omitempty"`
Blockchain *string `json:"blockchain,omitempty"`
}

type AuthWalletChallengeResult = model.WalletAuthChallengeResult

func AuthWalletChallenge(
args AuthWalletChallengeArgs,
session *session.ClientSession,
) (*AuthWalletChallengeResult, error) {
result := model.CreateWalletAuthChallenge(model.WalletAuthChallengeArgs{
WalletAddress: args.WalletAddress,
Blockchain: args.Blockchain,
}, session.Ctx)
return result, nil
}

func AuthLogin(
login model.AuthLoginArgs,
session *session.ClientSession,
Expand Down
19 changes: 19 additions & 0 deletions db_migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -2803,4 +2803,23 @@ var migrations = []any{
CREATE INDEX IF NOT EXISTS transfer_contract_dispute_create_time
ON transfer_contract (dispute, outcome, create_time)
`),

newSqlMigration(`
CREATE TABLE wallet_auth_challenge (
challenge_id uuid NOT NULL,
challenge_value varchar(128) NOT NULL,
wallet_address text NULL,
blockchain varchar(32) NULL,
create_time timestamp NOT NULL DEFAULT now(),
expire_time timestamp NOT NULL,
used bool NOT NULL DEFAULT false,

PRIMARY KEY (challenge_id),
UNIQUE (challenge_value)
)
`),
newSqlMigration(`
CREATE INDEX wallet_auth_challenge_expire_time_used
ON wallet_auth_challenge (expire_time, used)
`),
}
30 changes: 18 additions & 12 deletions model/auth_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type WalletAuthArgs struct {
Signature string `json:"wallet_signature,omitempty"`
Message string `json:"wallet_message,omitempty"`
Blockchain string `json:"blockchain,omitempty"`
// new fields; kept optional for backwards compat during deploy window
Challenge string `json:"challenge,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}

type AuthLoginArgs struct {
Expand Down Expand Up @@ -432,23 +435,26 @@ func handleLoginWallet(
ctx context.Context,
) (result *AuthLoginResult, returnErr error) {
/**
* Handle wallet login
* Handle wallet login by validating the server-issued challenge.
* UseWalletAuthChallenge verifies the signature, checks the timestamp,
* and marks the challenge as used atomically.
*/

isValid, err := VerifySignature(
walletAuth.Blockchain,
walletAuth.PublicKey,
walletAuth.Message,
walletAuth.Signature,
)

useResult, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{
Blockchain: walletAuth.Blockchain,
PublicKey: walletAuth.PublicKey,
Message: walletAuth.Message,
Signature: walletAuth.Signature,
}, ctx)
if err != nil {
returnErr = err
return
}

if !isValid {
returnErr = errors.New("invalid signature")
if !useResult.Valid {
msg := "invalid wallet challenge"
if useResult.Error != nil {
msg = useResult.Error.Message
}
returnErr = errors.New(msg)
return
}

Expand Down
25 changes: 13 additions & 12 deletions model/network_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,27 +356,28 @@ func NetworkCreate(
},
}, nil
}

networkCreate.WalletAuth.Blockchain = parsedBlockchain.String()

/**
* verify the wallet signature
* validate the wallet challenge
*/
isValid, err := VerifySignature(
networkCreate.WalletAuth.Blockchain,
networkCreate.WalletAuth.PublicKey,
networkCreate.WalletAuth.Message,
networkCreate.WalletAuth.Signature,
)

useResult, err := UseWalletAuthChallenge(&UseWalletAuthChallengeArgs{
Blockchain: networkCreate.WalletAuth.Blockchain,
PublicKey: networkCreate.WalletAuth.PublicKey,
Message: networkCreate.WalletAuth.Message,
Signature: networkCreate.WalletAuth.Signature,
}, session.Ctx)
if err != nil {
return nil, err
}

if !isValid {
if !useResult.Valid {
msg := "invalid wallet challenge"
if useResult.Error != nil {
msg = useResult.Error.Message
}
return &NetworkCreateResult{
Error: &NetworkCreateResultError{
Message: "invalid wallet signature",
Message: msg,
},
}, nil
}
Expand Down
Loading