Align backend EIP-191 auth message with the DBIS Explorer text the frontend and legacy SPA already sign, instead of the stale SolaceScan string. Co-authored-by: Cursor <cursoragent@cursor.com>
120 lines
4.0 KiB
Go
120 lines
4.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/accounts"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestWalletAuthSignMessageMatchesFrontend(t *testing.T) {
|
|
nonce := "abc123def456"
|
|
require.Equal(
|
|
t,
|
|
"Sign this message to authenticate with DBIS Explorer.\n\nNonce: abc123def456",
|
|
walletAuthSignMessage(nonce),
|
|
)
|
|
}
|
|
|
|
func TestAuthenticateWalletRecoversSignerFromFrontendMessage(t *testing.T) {
|
|
privateKey, err := crypto.GenerateKey()
|
|
require.NoError(t, err)
|
|
address := crypto.PubkeyToAddress(privateKey.PublicKey).Hex()
|
|
nonce := "test-nonce-001"
|
|
|
|
message := walletAuthSignMessage(nonce)
|
|
messageHash := accounts.TextHash([]byte(message))
|
|
signature, err := crypto.Sign(messageHash, privateKey)
|
|
require.NoError(t, err)
|
|
signature[64] += 27
|
|
|
|
sigBytes := make([]byte, len(signature))
|
|
copy(sigBytes, signature)
|
|
if sigBytes[64] >= 27 {
|
|
sigBytes[64] -= 27
|
|
}
|
|
pubKey, err := crypto.SigToPub(messageHash, sigBytes)
|
|
require.NoError(t, err)
|
|
require.Equal(t, address, crypto.PubkeyToAddress(*pubKey).Hex())
|
|
}
|
|
|
|
func TestDecodeWalletSignatureRejectsMalformedValues(t *testing.T) {
|
|
_, err := decodeWalletSignature("deadbeef")
|
|
require.ErrorContains(t, err, "signature must start with 0x")
|
|
|
|
_, err = decodeWalletSignature("0x1234")
|
|
require.ErrorContains(t, err, "invalid signature length")
|
|
}
|
|
|
|
func TestValidateJWTReturnsClaimsWhenDBUnavailable(t *testing.T) {
|
|
secret := []byte("test-secret")
|
|
auth := NewWalletAuth(nil, secret)
|
|
|
|
token, _, err := auth.generateJWT("0x4A666F96fC8764181194447A7dFdb7d471b301C8", 4)
|
|
require.NoError(t, err)
|
|
|
|
address, track, err := auth.ValidateJWT(token)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "0x4A666F96fC8764181194447A7dFdb7d471b301C8", address)
|
|
require.Equal(t, 4, track)
|
|
}
|
|
|
|
func TestTokenTTLForTrack4IsShort(t *testing.T) {
|
|
// Track 4 (operator) must have a TTL <= 1h — that is the headline
|
|
// tightening promised by completion criterion 3 (JWT hygiene).
|
|
ttl := tokenTTLFor(4)
|
|
require.LessOrEqual(t, ttl, time.Hour, "track 4 TTL must be <= 1h")
|
|
require.Greater(t, ttl, time.Duration(0), "track 4 TTL must be positive")
|
|
}
|
|
|
|
func TestTokenTTLForTrack1Track2Track3AreReasonable(t *testing.T) {
|
|
// Non-operator tracks are allowed longer sessions, but still bounded
|
|
// at 12h so a stale laptop tab doesn't carry a week-old token.
|
|
for _, track := range []int{1, 2, 3} {
|
|
ttl := tokenTTLFor(track)
|
|
require.Greater(t, ttl, time.Duration(0), "track %d TTL must be > 0", track)
|
|
require.LessOrEqual(t, ttl, 12*time.Hour, "track %d TTL must be <= 12h", track)
|
|
}
|
|
}
|
|
|
|
func TestGeneratedJWTCarriesJTIClaim(t *testing.T) {
|
|
// Revocation keys on jti. A token issued without one is unrevokable
|
|
// and must not be produced.
|
|
a := NewWalletAuth(nil, []byte("test-secret"))
|
|
token, _, err := a.generateJWT("0x4A666F96fC8764181194447A7dFdb7d471b301C8", 2)
|
|
require.NoError(t, err)
|
|
|
|
jti, err := a.jtiFromToken(token)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, jti, "generated JWT must carry a jti claim")
|
|
require.Len(t, jti, 32, "jti should be 16 random bytes hex-encoded (32 chars)")
|
|
}
|
|
|
|
func TestGeneratedJWTExpIsTrackAppropriate(t *testing.T) {
|
|
a := NewWalletAuth(nil, []byte("test-secret"))
|
|
for _, track := range []int{1, 2, 3, 4} {
|
|
_, expiresAt, err := a.generateJWT("0x4A666F96fC8764181194447A7dFdb7d471b301C8", track)
|
|
require.NoError(t, err)
|
|
want := tokenTTLFor(track)
|
|
// allow a couple-second slack for test execution
|
|
actual := time.Until(expiresAt)
|
|
require.InDelta(t, want.Seconds(), actual.Seconds(), 5.0,
|
|
"track %d exp should be ~%s from now, got %s", track, want, actual)
|
|
}
|
|
}
|
|
|
|
func TestRevokeJWTWithoutDBReturnsError(t *testing.T) {
|
|
// With w.db == nil, revocation has nowhere to write — the call must
|
|
// fail loudly so callers don't silently assume a token was revoked.
|
|
a := NewWalletAuth(nil, []byte("test-secret"))
|
|
token, _, err := a.generateJWT("0x4A666F96fC8764181194447A7dFdb7d471b301C8", 4)
|
|
require.NoError(t, err)
|
|
|
|
err = a.RevokeJWT(context.Background(), token, "test")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "no database")
|
|
}
|