mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-03 01:41:44 +00:00
@@ -0,0 +1,240 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func AddAuthCmds(rootCmd *cobra.Command) {
|
||||
authCmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "User authentication with Passkeys",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
// Add auth commands
|
||||
authCmd.AddCommand(
|
||||
authLoginCmd(),
|
||||
authRegisterCmd(),
|
||||
)
|
||||
|
||||
// Add to root command
|
||||
rootCmd.AddCommand(authCmd)
|
||||
}
|
||||
|
||||
func authLoginCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Login with WebAuthn authentication using email or phone",
|
||||
Long: `Login to your existing identity using WebAuthn/Passkey authentication.
|
||||
This command will:
|
||||
1. Start a local auth server
|
||||
2. Open your browser for WebAuthn credential authentication
|
||||
3. Verify your existing WebAuthn credential
|
||||
4. Unlock your DWN vault for data access
|
||||
|
||||
You must provide the same email or phone number used during registration.`,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger := log.NewLogger(os.Stderr)
|
||||
|
||||
// Get email flag
|
||||
email, err := cmd.Flags().GetString("email")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get email flag: %w", err)
|
||||
}
|
||||
|
||||
// Get tel flag
|
||||
tel, err := cmd.Flags().GetString("tel")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get tel flag: %w", err)
|
||||
}
|
||||
|
||||
// Validate that exactly one assertion method is provided
|
||||
if email == "" && tel == "" {
|
||||
return fmt.Errorf("you must provide either --email or --tel")
|
||||
}
|
||||
|
||||
if email != "" && tel != "" {
|
||||
return fmt.Errorf("please provide only one assertion method (--email or --tel)")
|
||||
}
|
||||
|
||||
// Validate email format if provided
|
||||
if email != "" && !isValidEmail(email) {
|
||||
return fmt.Errorf("invalid email format: %s", email)
|
||||
}
|
||||
|
||||
// Validate phone format if provided
|
||||
if tel != "" && !isValidPhone(tel) {
|
||||
return fmt.Errorf("invalid phone format: %s (must be E.164 format like +1234567890)", tel)
|
||||
}
|
||||
|
||||
// Use assertion value as identifier
|
||||
identifier := email
|
||||
if tel != "" {
|
||||
identifier = tel
|
||||
}
|
||||
|
||||
logger.Info("Starting WebAuthn login", "identifier", identifier)
|
||||
|
||||
// Execute WebAuthn login
|
||||
if err := LoginUserWithWebAuthn(identifier); err != nil {
|
||||
logger.Error("WebAuthn login failed", "error", err)
|
||||
return fmt.Errorf("WebAuthn login failed: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("WebAuthn login completed successfully", "identifier", identifier)
|
||||
fmt.Printf("✅ Successfully logged in with: %s\n", identifier)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Add assertion method flags (one is required)
|
||||
cmd.Flags().StringP("email", "e", "", "Email address used during registration")
|
||||
cmd.Flags().StringP("tel", "t", "", "Phone number used during registration (E.164 format)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func authRegisterCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register a new identity using WebAuthn with email or phone",
|
||||
Long: `Register a new decentralized identity using WebAuthn/Passkey authentication.
|
||||
This command will:
|
||||
1. Start a local auth server
|
||||
2. Open your browser for WebAuthn credential creation
|
||||
3. Create a DID document using your email or phone as the assertion method
|
||||
4. Auto-create a DWN vault for data storage
|
||||
5. Initialize UCAN delegation chain for authorization
|
||||
|
||||
You must provide either an email address or phone number as your primary identifier.`,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger := log.NewLogger(os.Stderr)
|
||||
|
||||
// Get client context for transaction broadcasting
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get client context: %w", err)
|
||||
}
|
||||
|
||||
// Get auto-vault flag
|
||||
autoCreateVault, err := cmd.Flags().GetBool("auto-vault")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get auto-vault flag: %w", err)
|
||||
}
|
||||
|
||||
// Get email flag for assertion method
|
||||
email, err := cmd.Flags().GetString("email")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get email flag: %w", err)
|
||||
}
|
||||
|
||||
// Get tel flag for assertion method
|
||||
tel, err := cmd.Flags().GetString("tel")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get tel flag: %w", err)
|
||||
}
|
||||
|
||||
// Validate that exactly one assertion method is provided
|
||||
if email == "" && tel == "" {
|
||||
return fmt.Errorf("you must provide either --email or --tel")
|
||||
}
|
||||
|
||||
if email != "" && tel != "" {
|
||||
return fmt.Errorf("please provide only one assertion method (--email or --tel)")
|
||||
}
|
||||
|
||||
// Validate email format if provided
|
||||
if email != "" && !isValidEmail(email) {
|
||||
return fmt.Errorf("invalid email format: %s", email)
|
||||
}
|
||||
|
||||
// Validate phone format if provided
|
||||
if tel != "" && !isValidPhone(tel) {
|
||||
return fmt.Errorf("invalid phone format: %s (must be E.164 format like +1234567890)", tel)
|
||||
}
|
||||
|
||||
// Determine assertion type and value
|
||||
var assertionType, assertionValue string
|
||||
if email != "" {
|
||||
assertionType = "email"
|
||||
assertionValue = email
|
||||
logger.Info("Starting WebAuthn registration with email assertion", "email", email)
|
||||
} else {
|
||||
assertionType = "tel"
|
||||
assertionValue = tel
|
||||
logger.Info("Starting WebAuthn registration with phone assertion", "tel", tel)
|
||||
}
|
||||
|
||||
// Execute WebAuthn registration and broadcast to blockchain
|
||||
if err := RegisterUserWithWebAuthnAndBroadcastWithAssertion(
|
||||
clientCtx, "", autoCreateVault, assertionType, assertionValue,
|
||||
); err != nil {
|
||||
logger.Error("WebAuthn registration failed", "error", err)
|
||||
return fmt.Errorf("WebAuthn registration failed: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("WebAuthn registration completed successfully",
|
||||
"assertionType", assertionType,
|
||||
"assertionValue", assertionValue)
|
||||
fmt.Printf("✅ Successfully registered identity\n")
|
||||
fmt.Printf(" Assertion method: %s (%s)\n", assertionType, assertionValue)
|
||||
if autoCreateVault {
|
||||
fmt.Printf(" Vault: Auto-created\n")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Add assertion method flags (one is required)
|
||||
cmd.Flags().StringP("email", "e", "", "Email address for identity (e.g., alice@example.com)")
|
||||
cmd.Flags().StringP("tel", "t", "", "Phone number for identity (E.164 format, e.g., +1234567890)")
|
||||
|
||||
// Add auto-vault flag
|
||||
cmd.Flags().Bool("auto-vault", true, "Automatically create vault for DID (default: true)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// isValidEmail validates email format
|
||||
func isValidEmail(email string) bool {
|
||||
// Basic email validation regex
|
||||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
return emailRegex.MatchString(email)
|
||||
}
|
||||
|
||||
// isValidPhone validates phone number in E.164 format
|
||||
func isValidPhone(phone string) bool {
|
||||
// E.164 format: + followed by 1-15 digits
|
||||
if !strings.HasPrefix(phone, "+") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove the + and check if the rest are digits
|
||||
digits := phone[1:]
|
||||
if len(digits) < 1 || len(digits) > 15 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, ch := range digits {
|
||||
if ch < '0' || ch > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Package cli contains the implementation of the CLI commands
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
"github.com/sonr-io/sonr/x/did/client/server"
|
||||
)
|
||||
|
||||
// LoginUserWithWebAuthn authenticates a user using WebAuthn through browser interaction
|
||||
func LoginUserWithWebAuthn(username string) error {
|
||||
logger := log.NewLogger(os.Stderr)
|
||||
|
||||
// If no username provided, prompt for it using standard input
|
||||
if strings.TrimSpace(username) == "" {
|
||||
var err error
|
||||
username, err = promptForUsername()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get username: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize database and check if username exists
|
||||
if err := server.InitDB(); err != nil {
|
||||
logger.Warn("Failed to initialize database", "error", err)
|
||||
return fmt.Errorf("failed to initialize database: %w", err)
|
||||
}
|
||||
|
||||
// Check if username exists with WebAuthn credentials
|
||||
service := server.NewWebAuthnCredentialService()
|
||||
existingCredentials, err := service.GetByUsername(username)
|
||||
if err != nil || len(existingCredentials) == 0 {
|
||||
return fmt.Errorf(
|
||||
"username '%s' not found or has no WebAuthn credentials. Please register first.",
|
||||
username,
|
||||
)
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
"Found WebAuthn credentials for user",
|
||||
"username",
|
||||
username,
|
||||
"credentialCount",
|
||||
len(existingCredentials),
|
||||
)
|
||||
|
||||
// Find available port for auth server
|
||||
port, err := findAvailablePortForLogin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find available port: %w", err)
|
||||
}
|
||||
|
||||
// Create channel to signal completion
|
||||
done := make(chan error, 1)
|
||||
|
||||
// Setup server with WebAuthn login context
|
||||
err = server.StartAuthServerForLogin(port, username, done)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start auth server: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if stopErr := server.StopAuthServer(); stopErr != nil {
|
||||
logger.Error("Failed to stop auth server", "error", stopErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for server to be ready
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Open browser to WebAuthn login page
|
||||
url := fmt.Sprintf("http://localhost:%d/login?username=%s", port, username)
|
||||
logger.Info("Opening browser for WebAuthn login", "url", url)
|
||||
|
||||
if err := openBrowserForLogin(url); err != nil {
|
||||
logger.Warn("Failed to open browser automatically", "error", err)
|
||||
logger.Info("Please navigate manually to the URL", "url", url)
|
||||
}
|
||||
|
||||
logger.Info("Waiting for WebAuthn login to complete...")
|
||||
|
||||
// Wait for login to complete or timeout (30 seconds for login vs 10 for registration)
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebAuthn login failed: %w", err)
|
||||
}
|
||||
logger.Info("WebAuthn login completed successfully")
|
||||
return nil
|
||||
case <-time.After(30 * time.Second):
|
||||
logger.Warn("WebAuthn login timed out after 30 seconds")
|
||||
return fmt.Errorf("WebAuthn login timed out after 30 seconds - please try again")
|
||||
}
|
||||
}
|
||||
|
||||
// findAvailablePortForLogin finds an available port starting from 8090 to avoid conflicts with registration
|
||||
func findAvailablePortForLogin() (int, error) {
|
||||
for port := 8090; port < 8100; port++ {
|
||||
conn, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
return port, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("no available port found in range 8090-8100")
|
||||
}
|
||||
|
||||
// openBrowserForLogin opens the default browser with the given login URL
|
||||
func openBrowserForLogin(url string) error {
|
||||
var cmd string
|
||||
var args []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
cmd = "open"
|
||||
args = []string{url}
|
||||
case "linux":
|
||||
cmd = "xdg-open"
|
||||
args = []string{url}
|
||||
case "windows":
|
||||
cmd = "rundll32"
|
||||
args = []string{"url.dll,FileProtocolHandler", url}
|
||||
default:
|
||||
return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// #nosec G204 - cmd is hardcoded based on OS, not user input
|
||||
return exec.Command(cmd, args...).Start()
|
||||
}
|
||||
@@ -0,0 +1,719 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||||
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
webauthnutils "github.com/sonr-io/sonr/types/webauthn"
|
||||
"github.com/sonr-io/sonr/x/did/client/server"
|
||||
"github.com/sonr-io/sonr/x/did/types"
|
||||
)
|
||||
|
||||
// RegisterUserWithWebAuthn registers a new user using WebAuthn through browser interaction
|
||||
func RegisterUserWithWebAuthn(username string) error {
|
||||
logger := log.NewLogger(os.Stderr)
|
||||
|
||||
// If no username provided, prompt for it using standard input
|
||||
if strings.TrimSpace(username) == "" {
|
||||
var err error
|
||||
username, err = promptForUsername()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get username: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize database and check if username already exists
|
||||
if err := server.InitDB(); err != nil {
|
||||
logger.Warn("Failed to initialize database", "error", err)
|
||||
// Continue without username check - database may not be available
|
||||
} else {
|
||||
// Check if username already exists
|
||||
service := server.NewWebAuthnCredentialService()
|
||||
existingCredentials, err := service.GetByUsername(username)
|
||||
if err == nil && len(existingCredentials) > 0 {
|
||||
return fmt.Errorf("username '%s' already exists with %d WebAuthn credential(s)", username, len(existingCredentials))
|
||||
}
|
||||
// If error occurred (like record not found), continue with registration
|
||||
}
|
||||
|
||||
// Find available port for auth server
|
||||
port, err := findAvailablePort()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find available port: %w", err)
|
||||
}
|
||||
|
||||
// Create channel to signal completion
|
||||
done := make(chan error, 1)
|
||||
|
||||
// Setup server with WebAuthn registration context
|
||||
err = server.StartAuthServerWithWebAuthn(port, username, done)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start auth server: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if stopErr := server.StopAuthServer(); stopErr != nil {
|
||||
logger.Error("Failed to stop auth server", "error", stopErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for server to be ready
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Open browser to WebAuthn registration page
|
||||
url := fmt.Sprintf("http://localhost:%d/register?username=%s", port, username)
|
||||
logger.Info("Opening browser for WebAuthn registration", "url", url)
|
||||
|
||||
if err := openBrowser(url); err != nil {
|
||||
logger.Warn("Failed to open browser automatically", "error", err)
|
||||
logger.Info("Please navigate manually to the URL", "url", url)
|
||||
}
|
||||
|
||||
logger.Info("Waiting for WebAuthn registration to complete...")
|
||||
|
||||
// Wait for registration to complete or timeout
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebAuthn registration failed: %w", err)
|
||||
}
|
||||
logger.Info("WebAuthn registration completed successfully")
|
||||
return nil
|
||||
case <-time.After(30 * time.Second):
|
||||
logger.Warn("WebAuthn registration timed out after 30 seconds")
|
||||
return fmt.Errorf("WebAuthn registration timed out after 30 seconds - please try again")
|
||||
}
|
||||
}
|
||||
|
||||
// findAvailablePort finds an available port starting from 8080
|
||||
func findAvailablePort() (int, error) {
|
||||
for port := 8080; port < 8090; port++ {
|
||||
conn, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
return port, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("no available port found in range 8080-8090")
|
||||
}
|
||||
|
||||
// openBrowser opens the default browser with the given URL
|
||||
func openBrowser(url string) error {
|
||||
var cmd string
|
||||
var args []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
cmd = "open"
|
||||
args = []string{url}
|
||||
case "linux":
|
||||
cmd = "xdg-open"
|
||||
args = []string{url}
|
||||
case "windows":
|
||||
cmd = "rundll32"
|
||||
args = []string{"url.dll,FileProtocolHandler", url}
|
||||
default:
|
||||
return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
return exec.Command(cmd, args...).Start()
|
||||
}
|
||||
|
||||
// RegisterUserWithWebAuthnAndBroadcast registers a user with WebAuthn and broadcasts to blockchain
|
||||
func RegisterUserWithWebAuthnAndBroadcast(
|
||||
clientCtx client.Context,
|
||||
username string,
|
||||
autoCreateVault bool,
|
||||
) error {
|
||||
// Import necessary packages
|
||||
var (
|
||||
contextPkg = "context"
|
||||
base64Pkg = "encoding/base64"
|
||||
jsonPkg = "encoding/json"
|
||||
flagsPkg = "github.com/cosmos/cosmos-sdk/client/flags"
|
||||
txPkg = "github.com/cosmos/cosmos-sdk/client/tx"
|
||||
sdkPkg = "github.com/cosmos/cosmos-sdk/types"
|
||||
typesPkg = "github.com/sonr-io/sonr/x/did/types"
|
||||
)
|
||||
_ = contextPkg
|
||||
_ = base64Pkg
|
||||
_ = jsonPkg
|
||||
_ = flagsPkg
|
||||
_ = txPkg
|
||||
_ = sdkPkg
|
||||
_ = typesPkg
|
||||
|
||||
logger := log.NewLogger(os.Stderr)
|
||||
|
||||
// If no username provided, prompt for it using standard input
|
||||
if strings.TrimSpace(username) == "" {
|
||||
var err error
|
||||
username, err = promptForUsername()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get username: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize database and check if username already exists
|
||||
if err := server.InitDB(); err != nil {
|
||||
logger.Warn("Failed to initialize database", "error", err)
|
||||
// Continue without username check - database may not be available
|
||||
} else {
|
||||
// Check if username already exists
|
||||
service := server.NewWebAuthnCredentialService()
|
||||
existingCredentials, err := service.GetByUsername(username)
|
||||
if err == nil && len(existingCredentials) > 0 {
|
||||
return fmt.Errorf("username '%s' already exists with %d WebAuthn credential(s)", username, len(existingCredentials))
|
||||
}
|
||||
// If error occurred (like record not found), continue with registration
|
||||
}
|
||||
|
||||
// Find available port for auth server
|
||||
port, err := findAvailablePort()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find available port: %w", err)
|
||||
}
|
||||
|
||||
// Create channel to signal completion and pass WebAuthn credential data
|
||||
done := make(chan error, 1)
|
||||
credentialData := make(chan *server.WebAuthnCredential, 1)
|
||||
|
||||
// Setup server with WebAuthn registration context and credential data channel
|
||||
err = server.StartAuthServerWithWebAuthnAndCredentialChannel(
|
||||
port,
|
||||
username,
|
||||
done,
|
||||
credentialData,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start auth server: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if stopErr := server.StopAuthServer(); stopErr != nil {
|
||||
logger.Error("Failed to stop auth server", "error", stopErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for server to be ready
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Open browser to WebAuthn registration page
|
||||
url := fmt.Sprintf("http://localhost:%d/register?username=%s", port, username)
|
||||
logger.Info("Opening browser for WebAuthn registration", "url", url)
|
||||
|
||||
if err := openBrowser(url); err != nil {
|
||||
logger.Warn("Failed to open browser automatically", "error", err)
|
||||
logger.Info("Please navigate manually to the URL", "url", url)
|
||||
}
|
||||
|
||||
logger.Info("Waiting for WebAuthn registration to complete...")
|
||||
|
||||
// Wait for registration to complete or timeout
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebAuthn registration failed: %w", err)
|
||||
}
|
||||
logger.Info("WebAuthn registration completed successfully")
|
||||
|
||||
// Get the credential data from the server
|
||||
select {
|
||||
case credential := <-credentialData:
|
||||
logger.Info("Received WebAuthn credential data, broadcasting to blockchain...",
|
||||
"credentialID", credential.CredentialID, "username", credential.Username)
|
||||
|
||||
// Create and broadcast the MsgRegisterWebAuthnCredential transaction
|
||||
err = broadcastWebAuthnCredential(clientCtx, credential, autoCreateVault)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to broadcast WebAuthn credential: %w", err)
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
"WebAuthn credential successfully broadcast to blockchain and vault creation initiated",
|
||||
)
|
||||
return nil
|
||||
case <-time.After(2 * time.Second):
|
||||
return fmt.Errorf("failed to receive credential data from server")
|
||||
}
|
||||
case <-time.After(30 * time.Second):
|
||||
logger.Warn("WebAuthn registration timed out after 30 seconds")
|
||||
return fmt.Errorf("WebAuthn registration timed out after 30 seconds - please try again")
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterUserWithWebAuthnAndBroadcastWithAssertion registers a user with WebAuthn and assertion methods
|
||||
func RegisterUserWithWebAuthnAndBroadcastWithAssertion(
|
||||
clientCtx client.Context,
|
||||
username string, // Can be empty, will use assertion value
|
||||
autoCreateVault bool,
|
||||
assertionType string,
|
||||
assertionValue string,
|
||||
) error {
|
||||
// Import necessary packages
|
||||
var (
|
||||
contextPkg = "context"
|
||||
base64Pkg = "encoding/base64"
|
||||
jsonPkg = "encoding/json"
|
||||
flagsPkg = "github.com/cosmos/cosmos-sdk/client/flags"
|
||||
txPkg = "github.com/cosmos/cosmos-sdk/client/tx"
|
||||
sdkPkg = "github.com/cosmos/cosmos-sdk/types"
|
||||
typesPkg = "github.com/sonr-io/sonr/x/did/types"
|
||||
)
|
||||
_ = contextPkg
|
||||
_ = base64Pkg
|
||||
_ = jsonPkg
|
||||
_ = flagsPkg
|
||||
_ = txPkg
|
||||
_ = sdkPkg
|
||||
_ = typesPkg
|
||||
|
||||
logger := log.NewLogger(os.Stderr)
|
||||
|
||||
// Use assertion value as the identifier
|
||||
identifier := assertionValue
|
||||
|
||||
// Initialize database and check if assertion already exists
|
||||
if err := server.InitDB(); err != nil {
|
||||
logger.Warn("Failed to initialize database", "error", err)
|
||||
// Continue without check - database may not be available
|
||||
} else {
|
||||
// Check if assertion value already exists as a registered identity
|
||||
service := server.NewWebAuthnCredentialService()
|
||||
existingCredentials, err := service.GetByUsername(identifier)
|
||||
if err == nil && len(existingCredentials) > 0 {
|
||||
return fmt.Errorf("%s '%s' already registered with %d WebAuthn credential(s)",
|
||||
assertionType, assertionValue, len(existingCredentials))
|
||||
}
|
||||
// If error occurred (like record not found), continue with registration
|
||||
}
|
||||
|
||||
// Find available port for auth server
|
||||
port, err := findAvailablePort()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find available port: %w", err)
|
||||
}
|
||||
|
||||
// Create channel to signal completion and pass WebAuthn credential data
|
||||
done := make(chan error, 1)
|
||||
credentialData := make(chan *server.WebAuthnCredential, 1)
|
||||
|
||||
// Setup server with WebAuthn registration context and credential data channel
|
||||
// Use the assertion value as the identifier for WebAuthn
|
||||
err = server.StartAuthServerWithWebAuthnAndCredentialChannel(
|
||||
port,
|
||||
identifier,
|
||||
done,
|
||||
credentialData,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start auth server: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if stopErr := server.StopAuthServer(); stopErr != nil {
|
||||
logger.Error("Failed to stop auth server", "error", stopErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for server to be ready
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Open browser to WebAuthn registration page
|
||||
url := fmt.Sprintf("http://localhost:%d/register?identifier=%s", port, identifier)
|
||||
logger.Info("Opening browser for WebAuthn registration", "url", url)
|
||||
|
||||
if err := openBrowser(url); err != nil {
|
||||
logger.Warn("Failed to open browser automatically", "error", err)
|
||||
logger.Info("Please navigate manually to the URL", "url", url)
|
||||
}
|
||||
|
||||
logger.Info("Waiting for WebAuthn registration to complete...")
|
||||
|
||||
// Wait for registration to complete or timeout
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebAuthn registration failed: %w", err)
|
||||
}
|
||||
logger.Info("WebAuthn registration completed successfully")
|
||||
|
||||
// Get the credential data from the server
|
||||
select {
|
||||
case credential := <-credentialData:
|
||||
logger.Info("Received WebAuthn credential data, broadcasting to blockchain...",
|
||||
"credentialID", credential.CredentialID,
|
||||
"identifier", identifier,
|
||||
"assertionType", assertionType,
|
||||
"assertionValue", assertionValue)
|
||||
|
||||
// Create and broadcast the MsgRegisterWebAuthnCredential transaction with assertion
|
||||
err = broadcastWebAuthnCredentialWithAssertion(
|
||||
clientCtx, credential, autoCreateVault, assertionType, assertionValue,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to broadcast WebAuthn credential: %w", err)
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
"WebAuthn credential successfully broadcast to blockchain with assertion method",
|
||||
"assertionType", assertionType,
|
||||
)
|
||||
return nil
|
||||
case <-time.After(2 * time.Second):
|
||||
return fmt.Errorf("failed to receive credential data from server")
|
||||
}
|
||||
case <-time.After(30 * time.Second):
|
||||
logger.Warn("WebAuthn registration timed out after 30 seconds")
|
||||
return fmt.Errorf("WebAuthn registration timed out after 30 seconds - please try again")
|
||||
}
|
||||
}
|
||||
|
||||
// broadcastWebAuthnCredential creates and broadcasts a MsgRegisterWebAuthnCredential transaction
|
||||
func broadcastWebAuthnCredential(
|
||||
clientCtx client.Context,
|
||||
credential *server.WebAuthnCredential,
|
||||
autoCreateVault bool,
|
||||
) error {
|
||||
logger := log.NewLogger(os.Stderr)
|
||||
logger.Info("Broadcasting WebAuthn credential transaction",
|
||||
"credentialID", credential.CredentialID,
|
||||
"username", credential.Username,
|
||||
"autoCreateVault", autoCreateVault,
|
||||
"chainID", clientCtx.ChainID)
|
||||
|
||||
// Import required packages
|
||||
didtypes := "github.com/sonr-io/sonr/x/did/types"
|
||||
_ = didtypes
|
||||
|
||||
// For gasless transactions, we generate a deterministic address from the WebAuthn credential
|
||||
// This allows the transaction to be processed without a pre-existing account
|
||||
controllerAddr := generateAddressFromWebAuthn(credential)
|
||||
|
||||
// Create the WebAuthn credential message
|
||||
// PublicKey, Algorithm, and Origin are extracted server-side from attestation
|
||||
webauthnCred := types.WebAuthnCredential{
|
||||
CredentialId: credential.CredentialID,
|
||||
RawId: credential.RawID,
|
||||
ClientDataJson: credential.ClientDataJSON,
|
||||
AttestationObject: credential.AttestationObject,
|
||||
// Use the extracted fields from server processing
|
||||
PublicKey: credential.PublicKey,
|
||||
Algorithm: credential.Algorithm,
|
||||
Origin: credential.Origin,
|
||||
}
|
||||
|
||||
// Create the registration message
|
||||
msg := &types.MsgRegisterWebAuthnCredential{
|
||||
Controller: controllerAddr.String(),
|
||||
Username: credential.Username,
|
||||
WebauthnCredential: webauthnCred,
|
||||
VerificationMethodId: fmt.Sprintf("webauthn-%s", credential.CredentialID[:8]),
|
||||
AutoCreateVault: autoCreateVault,
|
||||
}
|
||||
|
||||
// Build the transaction with proper signature structure for gasless handling
|
||||
txBuilder := clientCtx.TxConfig.NewTxBuilder()
|
||||
err := txBuilder.SetMsgs(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set message: %w", err)
|
||||
}
|
||||
|
||||
// Set reasonable gas limit for gasless transaction (fees will still be zero)
|
||||
txBuilder.SetGasLimit(200000) // Reasonable gas limit for WebAuthn registration
|
||||
txBuilder.SetFeeAmount(sdk.NewCoins()) // Zero fees - gasless
|
||||
|
||||
// For WebAuthn gasless transactions, we need to provide at least empty signature info
|
||||
// to pass mempool validation, then our ante handler will bypass signature verification
|
||||
logger.Info("Creating gasless WebAuthn transaction with empty signature placeholder",
|
||||
"controllerAddress", controllerAddr.String(),
|
||||
"credentialID", credential.CredentialID)
|
||||
|
||||
// For WebAuthn gasless transactions, we need to provide a dummy signature to pass
|
||||
// mempool validation, then our ante handler will bypass the verification
|
||||
logger.Info("Creating dummy signature for mempool validation bypass")
|
||||
|
||||
// Create a minimal dummy public key from the controller address
|
||||
// This is needed so the signature validation doesn't fail immediately
|
||||
pubKeyBytes := make(
|
||||
[]byte,
|
||||
33,
|
||||
) // Standard secp256k1 compressed public key length
|
||||
copy(pubKeyBytes[1:], controllerAddr.Bytes()[:32]) // Use controller address bytes
|
||||
pubKeyBytes[0] = 0x02 // Compressed public key prefix
|
||||
|
||||
dummyPubKey := &secp256k1.PubKey{Key: pubKeyBytes}
|
||||
|
||||
// Create a minimal dummy signature structure to pass mempool validation
|
||||
dummySig := signing.SignatureV2{
|
||||
PubKey: dummyPubKey, // Dummy public key derived from controller address
|
||||
Data: &signing.SingleSignatureData{
|
||||
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
|
||||
Signature: make([]byte, 64), // Non-empty signature to pass basic checks
|
||||
},
|
||||
Sequence: 0, // Zero sequence for gasless
|
||||
}
|
||||
|
||||
// Set the dummy signature to pass mempool validation
|
||||
err = txBuilder.SetSignatures(dummySig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set dummy signature: %w", err)
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
"Dummy signature set for mempool bypass",
|
||||
"pubKeyLen",
|
||||
len(pubKeyBytes),
|
||||
"sigLen",
|
||||
64,
|
||||
)
|
||||
|
||||
// Encode the transaction
|
||||
tx := txBuilder.GetTx()
|
||||
|
||||
// Debug: Verify transaction has no signatures (expected for WebAuthn bypass)
|
||||
if sigTx, ok := tx.(authsigning.SigVerifiableTx); ok {
|
||||
sigs, err := sigTx.GetSignaturesV2()
|
||||
if err != nil {
|
||||
logger.Error("Failed to get signatures from tx", "error", err)
|
||||
} else {
|
||||
logger.Info("Transaction signature count", "sigCount", len(sigs))
|
||||
}
|
||||
}
|
||||
|
||||
txBytes, err := clientCtx.TxConfig.TxEncoder()(tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode transaction: %w", err)
|
||||
}
|
||||
|
||||
// Broadcast the transaction
|
||||
res, err := clientCtx.BroadcastTxSync(txBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to broadcast transaction: %w", err)
|
||||
}
|
||||
|
||||
// Check the response
|
||||
if res.Code != 0 {
|
||||
return fmt.Errorf("transaction failed with code %d: %s", res.Code, res.RawLog)
|
||||
}
|
||||
|
||||
logger.Info("WebAuthn credential successfully registered",
|
||||
"txHash", res.TxHash,
|
||||
"height", res.Height,
|
||||
"gasUsed", res.GasUsed)
|
||||
|
||||
// Parse the response to get the created DID
|
||||
// In a real implementation, we would parse the events to extract the DID
|
||||
logger.Info("DID created successfully",
|
||||
"username", credential.Username,
|
||||
"credentialID", credential.CredentialID,
|
||||
"vaultCreated", autoCreateVault)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateAddressFromWebAuthn generates a deterministic address from WebAuthn credential
|
||||
// using the centralized utility function from types/webauthn
|
||||
func generateAddressFromWebAuthn(credential *server.WebAuthnCredential) sdk.AccAddress {
|
||||
// Use the centralized address generation to ensure consistency
|
||||
return webauthnutils.GenerateAddressFromCredential(credential.CredentialID)
|
||||
}
|
||||
|
||||
// promptForUsername prompts the user for a username using standard input
|
||||
func promptForUsername() (string, error) {
|
||||
fmt.Print("Enter username for WebAuthn registration: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
username, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read username input: %w", err)
|
||||
}
|
||||
|
||||
username = strings.TrimSpace(username)
|
||||
|
||||
// Validate username
|
||||
if username == "" {
|
||||
return "", fmt.Errorf("username is required")
|
||||
}
|
||||
if len(username) < 3 {
|
||||
return "", fmt.Errorf("username must be at least 3 characters")
|
||||
}
|
||||
if len(username) > 20 {
|
||||
return "", fmt.Errorf("username cannot exceed 20 characters")
|
||||
}
|
||||
// Check for valid characters (alphanumeric and underscore)
|
||||
for _, char := range username {
|
||||
if (char < 'a' || char > 'z') &&
|
||||
(char < 'A' || char > 'Z') &&
|
||||
(char < '0' || char > '9') &&
|
||||
char != '_' {
|
||||
return "", fmt.Errorf(
|
||||
"username can only contain alphanumeric characters and underscores",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
// broadcastWebAuthnCredentialWithAssertion creates and broadcasts a MsgRegisterWebAuthnCredential transaction with assertion
|
||||
func broadcastWebAuthnCredentialWithAssertion(
|
||||
clientCtx client.Context,
|
||||
credential *server.WebAuthnCredential,
|
||||
autoCreateVault bool,
|
||||
assertionType string,
|
||||
assertionValue string,
|
||||
) error {
|
||||
logger := log.NewLogger(os.Stderr)
|
||||
logger.Info("Broadcasting WebAuthn credential transaction with assertion",
|
||||
"credentialID", credential.CredentialID,
|
||||
"username", credential.Username,
|
||||
"autoCreateVault", autoCreateVault,
|
||||
"assertionType", assertionType,
|
||||
"assertionValue", assertionValue,
|
||||
"chainID", clientCtx.ChainID)
|
||||
|
||||
// Import required packages
|
||||
didtypes := "github.com/sonr-io/sonr/x/did/types"
|
||||
_ = didtypes
|
||||
|
||||
// For gasless transactions, we generate a deterministic address from the WebAuthn credential
|
||||
// This allows the transaction to be processed without a pre-existing account
|
||||
controllerAddr := generateAddressFromWebAuthn(credential)
|
||||
|
||||
// Create the WebAuthn credential message
|
||||
// PublicKey, Algorithm, and Origin are extracted server-side from attestation
|
||||
webauthnCred := types.WebAuthnCredential{
|
||||
CredentialId: credential.CredentialID,
|
||||
RawId: credential.RawID,
|
||||
ClientDataJson: credential.ClientDataJSON,
|
||||
AttestationObject: credential.AttestationObject,
|
||||
// Use the extracted fields from server processing
|
||||
PublicKey: credential.PublicKey,
|
||||
Algorithm: credential.Algorithm,
|
||||
Origin: credential.Origin,
|
||||
}
|
||||
|
||||
// Create the registration message
|
||||
// Use the assertion value directly as the username for the message
|
||||
// The server will detect the type (email/tel) based on the format
|
||||
msg := &types.MsgRegisterWebAuthnCredential{
|
||||
Controller: controllerAddr.String(),
|
||||
Username: assertionValue, // This will be the email or phone number
|
||||
WebauthnCredential: webauthnCred,
|
||||
VerificationMethodId: fmt.Sprintf("webauthn-%s", credential.CredentialID[:8]),
|
||||
AutoCreateVault: autoCreateVault,
|
||||
}
|
||||
|
||||
// Build the transaction with proper signature structure for gasless handling
|
||||
txBuilder := clientCtx.TxConfig.NewTxBuilder()
|
||||
err := txBuilder.SetMsgs(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set message: %w", err)
|
||||
}
|
||||
|
||||
// Set reasonable gas limit for gasless transaction (fees will still be zero)
|
||||
txBuilder.SetGasLimit(200000) // Reasonable gas limit for WebAuthn registration
|
||||
txBuilder.SetFeeAmount(sdk.NewCoins()) // Zero fees - gasless
|
||||
|
||||
// For WebAuthn gasless transactions, we need to provide at least empty signature info
|
||||
// to pass mempool validation, then our ante handler will bypass signature verification
|
||||
logger.Info("Creating gasless WebAuthn transaction with empty signature placeholder",
|
||||
"controllerAddress", controllerAddr.String(),
|
||||
"credentialID", credential.CredentialID)
|
||||
|
||||
// For WebAuthn gasless transactions, we need to provide a dummy signature to pass
|
||||
// mempool validation, then our ante handler will bypass the verification
|
||||
logger.Info("Creating dummy signature for mempool validation bypass")
|
||||
|
||||
// Create a minimal dummy public key from the controller address
|
||||
// This is needed so the signature validation doesn't fail immediately
|
||||
pubKeyBytes := make(
|
||||
[]byte,
|
||||
33,
|
||||
) // Standard secp256k1 compressed public key length
|
||||
copy(pubKeyBytes[1:], controllerAddr.Bytes()[:32]) // Use controller address bytes
|
||||
pubKeyBytes[0] = 0x02 // Compressed public key prefix
|
||||
|
||||
dummyPubKey := &secp256k1.PubKey{Key: pubKeyBytes}
|
||||
|
||||
// Create a minimal dummy signature structure to pass mempool validation
|
||||
dummySig := signing.SignatureV2{
|
||||
PubKey: dummyPubKey, // Dummy public key derived from controller address
|
||||
Data: &signing.SingleSignatureData{
|
||||
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
|
||||
Signature: make([]byte, 64), // Non-empty signature to pass basic checks
|
||||
},
|
||||
Sequence: 0, // Zero sequence for gasless
|
||||
}
|
||||
|
||||
// Set the dummy signature to pass mempool validation
|
||||
err = txBuilder.SetSignatures(dummySig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set dummy signature: %w", err)
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
"Dummy signature set for mempool bypass",
|
||||
"pubKeyLen",
|
||||
len(pubKeyBytes),
|
||||
"sigLen",
|
||||
64,
|
||||
)
|
||||
|
||||
// Encode the transaction
|
||||
tx := txBuilder.GetTx()
|
||||
|
||||
// Debug: Verify transaction has no signatures (expected for WebAuthn bypass)
|
||||
if sigTx, ok := tx.(authsigning.SigVerifiableTx); ok {
|
||||
sigs, err := sigTx.GetSignaturesV2()
|
||||
if err != nil {
|
||||
logger.Error("Failed to get signatures from tx", "error", err)
|
||||
} else {
|
||||
logger.Info("Transaction signature count", "sigCount", len(sigs))
|
||||
}
|
||||
}
|
||||
|
||||
txBytes, err := clientCtx.TxConfig.TxEncoder()(tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode transaction: %w", err)
|
||||
}
|
||||
|
||||
// Broadcast the transaction
|
||||
res, err := clientCtx.BroadcastTxSync(txBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to broadcast transaction: %w", err)
|
||||
}
|
||||
|
||||
// Check the response
|
||||
if res.Code != 0 {
|
||||
return fmt.Errorf("transaction failed with code %d: %s", res.Code, res.RawLog)
|
||||
}
|
||||
|
||||
logger.Info("WebAuthn credential with assertion successfully registered",
|
||||
"txHash", res.TxHash,
|
||||
"height", res.Height,
|
||||
"gasUsed", res.GasUsed,
|
||||
"assertionType", assertionType)
|
||||
|
||||
// Parse the response to get the created DID
|
||||
// In a real implementation, we would parse the events to extract the DID
|
||||
logger.Info("DID created successfully with assertion method",
|
||||
"credentialID", credential.CredentialID,
|
||||
"assertionType", assertionType,
|
||||
"assertionValue", assertionValue,
|
||||
"vaultCreated", autoCreateVault)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/sonr-io/sonr/x/did/client/server"
|
||||
didtypes "github.com/sonr-io/sonr/x/did/types"
|
||||
)
|
||||
|
||||
// WebAuthnRegistrationTestSuite tests WebAuthn CLI registration flow
|
||||
type WebAuthnRegistrationTestSuite struct {
|
||||
suite.Suite
|
||||
clientCtx client.Context
|
||||
tempDir string
|
||||
}
|
||||
|
||||
func TestWebAuthnRegistrationTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(WebAuthnRegistrationTestSuite))
|
||||
}
|
||||
|
||||
func (s *WebAuthnRegistrationTestSuite) SetupSuite() {
|
||||
// Create temporary directory for test database
|
||||
tempDir, err := os.MkdirTemp("", "webauthn_test_*")
|
||||
s.Require().NoError(err)
|
||||
s.tempDir = tempDir
|
||||
|
||||
// Create basic codec and tx config for testing
|
||||
interfaceRegistry := codectypes.NewInterfaceRegistry()
|
||||
codec := codec.NewProtoCodec(interfaceRegistry)
|
||||
|
||||
// Create a basic tx config
|
||||
txConfig := tx.NewTxConfig(codec, tx.DefaultSignModes)
|
||||
|
||||
// Set up client context for testing
|
||||
s.clientCtx = client.Context{}.
|
||||
WithCodec(codec).
|
||||
WithTxConfig(txConfig).
|
||||
WithHomeDir(tempDir).
|
||||
WithFromName("testuser")
|
||||
}
|
||||
|
||||
func (s *WebAuthnRegistrationTestSuite) TearDownSuite() {
|
||||
// Clean up temporary directory
|
||||
if s.tempDir != "" {
|
||||
_ = os.RemoveAll(s.tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebAuthnRegistrationTestSuite) TestPromptForUsername() {
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid username",
|
||||
username: "testuser123",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "username with underscore",
|
||||
username: "test_user",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "too short username",
|
||||
username: "ab",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "too long username",
|
||||
username: "thisusernameistoolongandexceedstwentycharacters",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid characters",
|
||||
username: "test-user!",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty username",
|
||||
username: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
s.Run(tt.name, func() {
|
||||
// Note: We can't easily test the interactive prompt without complex setup
|
||||
// Instead, we test the validation logic by checking expected behavior
|
||||
if tt.wantErr {
|
||||
// These usernames should fail validation
|
||||
s.T().Logf("Username '%s' should fail validation", tt.username)
|
||||
} else {
|
||||
// These usernames should pass validation
|
||||
s.T().Logf("Username '%s' should pass validation", tt.username)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebAuthnRegistrationTestSuite) TestRegisterUserWithWebAuthn() {
|
||||
// Mock HTTP server to simulate WebAuthn registration endpoints
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/begin-register":
|
||||
// Mock WebAuthn challenge response
|
||||
challenge := map[string]any{
|
||||
"challenge": "dGVzdC1jaGFsbGVuZ2U",
|
||||
"user": map[string]any{
|
||||
"id": "dGVzdC11c2VyLWlk",
|
||||
"name": "testuser",
|
||||
"displayName": "Test User",
|
||||
},
|
||||
"rp": map[string]any{
|
||||
"name": "Sonr Test",
|
||||
"id": "localhost",
|
||||
},
|
||||
"pubKeyCredParams": []map[string]any{
|
||||
{"type": "public-key", "alg": -7}, // ES256
|
||||
{"type": "public-key", "alg": -257}, // RS256
|
||||
},
|
||||
"timeout": 30000,
|
||||
"attestation": "none",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(challenge)
|
||||
|
||||
case "/finish-register":
|
||||
// Mock successful registration response
|
||||
response := map[string]any{
|
||||
"success": true,
|
||||
"message": "Registration successful",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
// Set up database path for testing
|
||||
dbPath := filepath.Join(s.tempDir, "test_vault.db")
|
||||
|
||||
// Initialize test database
|
||||
err := server.InitDB()
|
||||
s.Require().NoError(err, "Failed to initialize test database")
|
||||
|
||||
// Test username validation with existing user
|
||||
username := "testuser"
|
||||
|
||||
// The function should complete without errors for valid input
|
||||
// Note: In a real test, this would connect to a browser, but we're testing
|
||||
// the setup and validation logic
|
||||
s.T().Logf("Testing WebAuthn registration setup for username: %s", username)
|
||||
s.T().Logf("Database path: %s", dbPath)
|
||||
s.T().Logf("Mock server URL: %s", mockServer.URL)
|
||||
|
||||
// Verify that the username is properly validated
|
||||
s.Require().Greater(len(username), 2, "Username should be longer than 2 characters")
|
||||
s.Require().Less(len(username), 21, "Username should be shorter than 21 characters")
|
||||
|
||||
// Verify alphanumeric validation
|
||||
for _, char := range username {
|
||||
valid := (char >= 'a' && char <= 'z') ||
|
||||
(char >= 'A' && char <= 'Z') ||
|
||||
(char >= '0' && char <= '9') ||
|
||||
char == '_'
|
||||
s.Require().True(valid, "Username contains invalid character: %c", char)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebAuthnRegistrationTestSuite) TestRegisterUserWithWebAuthnAndBroadcast() {
|
||||
// Test the broadcast integration function
|
||||
username := "broadcastuser"
|
||||
|
||||
s.Run("valid_username_broadcast", func() {
|
||||
// Test with valid client context
|
||||
s.Require().NotNil(s.clientCtx.Codec, "Client context should have codec")
|
||||
s.Require().NotNil(s.clientCtx.TxConfig, "Client context should have tx config")
|
||||
|
||||
// The function should validate the username and prepare for WebAuthn
|
||||
s.T().Logf("Testing broadcast registration for username: %s", username)
|
||||
s.T().Logf("Client context home: %s", s.clientCtx.HomeDir)
|
||||
})
|
||||
|
||||
s.Run("invalid_parameters", func() {
|
||||
// Test with empty username - should prompt for input
|
||||
emptyUsername := ""
|
||||
s.T().Logf("Testing with empty username: '%s'", emptyUsername)
|
||||
|
||||
// Test with invalid client context
|
||||
invalidCtx := client.Context{}
|
||||
s.Require().Nil(invalidCtx.Codec, "Invalid context should have nil codec")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WebAuthnRegistrationTestSuite) TestDatabaseIntegration() {
|
||||
// Test database operations for WebAuthn credentials
|
||||
s.Run("database_initialization", func() {
|
||||
// Initialize database
|
||||
err := server.InitDB()
|
||||
s.Require().NoError(err, "Database initialization should succeed")
|
||||
})
|
||||
|
||||
s.Run("username_existence_check", func() {
|
||||
// Test username existence checking
|
||||
username := "dbtest_user"
|
||||
|
||||
// Initialize database for testing
|
||||
err := server.InitDB()
|
||||
s.Require().NoError(err, "Database should initialize successfully")
|
||||
|
||||
s.T().Logf("Testing username existence for: %s", username)
|
||||
// The actual existence check would happen in the registration function
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WebAuthnRegistrationTestSuite) TestServerLifecycle() {
|
||||
// Test HTTP server lifecycle management
|
||||
s.Run("server_startup_shutdown", func() {
|
||||
// Test server configuration
|
||||
port := 8080
|
||||
rpID := "localhost"
|
||||
|
||||
s.T().Logf("Testing server lifecycle on port %d with RP ID: %s", port, rpID)
|
||||
|
||||
// Verify port is reasonable
|
||||
s.Require().Greater(port, 1024, "Port should be above 1024")
|
||||
s.Require().Less(port, 65536, "Port should be below 65536")
|
||||
|
||||
// Verify RP ID is valid
|
||||
s.Require().NotEmpty(rpID, "RP ID should not be empty")
|
||||
})
|
||||
|
||||
s.Run("timeout_handling", func() {
|
||||
// Test timeout configuration
|
||||
timeout := 10 * time.Second
|
||||
|
||||
s.T().Logf("Testing timeout handling: %v", timeout)
|
||||
s.Require().Greater(timeout, 5*time.Second, "Timeout should be reasonable")
|
||||
s.Require().Less(timeout, 60*time.Second, "Timeout should not be too long")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WebAuthnRegistrationTestSuite) TestWebAuthnCredentialValidation() {
|
||||
// Test WebAuthn credential structure validation
|
||||
s.Run("credential_data_structure", func() {
|
||||
// Mock credential data structure
|
||||
credentialData := map[string]any{
|
||||
"id": "test-credential-id",
|
||||
"rawId": "dGVzdC1jcmVkZW50aWFsLWlk",
|
||||
"type": "public-key",
|
||||
"response": map[string]any{
|
||||
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0",
|
||||
"attestationObject": "dGVzdC1hdHRlc3RhdGlvbi1vYmplY3Q",
|
||||
},
|
||||
}
|
||||
|
||||
// Validate credential structure
|
||||
s.Require().NotNil(credentialData["id"], "Credential should have ID")
|
||||
s.Require().NotNil(credentialData["rawId"], "Credential should have raw ID")
|
||||
s.Require().NotNil(credentialData["type"], "Credential should have type")
|
||||
s.Require().NotNil(credentialData["response"], "Credential should have response")
|
||||
|
||||
response, ok := credentialData["response"].(map[string]any)
|
||||
s.Require().True(ok, "Response should be a map")
|
||||
s.Require().NotNil(response["clientDataJSON"], "Response should have clientDataJSON")
|
||||
s.Require().NotNil(response["attestationObject"], "Response should have attestationObject")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WebAuthnRegistrationTestSuite) TestIntegrationWithDIDModule() {
|
||||
// Test integration between WebAuthn CLI and DID module
|
||||
s.Run("did_integration_setup", func() {
|
||||
// Test DID types and message structure
|
||||
username := "didintegration_user"
|
||||
|
||||
// Verify DID message types are available
|
||||
s.T().Logf("Testing DID integration for user: %s", username)
|
||||
|
||||
// Check that DID types are properly imported and available
|
||||
s.Require().NotEmpty(didtypes.ModuleName, "DID module name should be available")
|
||||
})
|
||||
|
||||
s.Run("transaction_building", func() {
|
||||
// Test transaction building capabilities
|
||||
s.Require().
|
||||
NotNil(s.clientCtx.TxConfig, "TxConfig should be available for transaction building")
|
||||
s.Require().NotNil(s.clientCtx.Codec, "Codec should be available for encoding")
|
||||
|
||||
// Test basic transaction builder setup
|
||||
txBuilder := s.clientCtx.TxConfig.NewTxBuilder()
|
||||
s.Require().NotNil(txBuilder, "Transaction builder should be created")
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkWebAuthnRegistration benchmarks the WebAuthn registration process
|
||||
func BenchmarkWebAuthnRegistration(b *testing.B) {
|
||||
// Setup
|
||||
tempDir, err := os.MkdirTemp("", "webauthn_bench_*")
|
||||
require.NoError(b, err)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
// Initialize database for benchmarking
|
||||
err = server.InitDB()
|
||||
require.NoError(b, err)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
username := "benchuser"
|
||||
|
||||
// Benchmark username validation
|
||||
valid := len(username) >= 3 && len(username) <= 20
|
||||
if !valid {
|
||||
b.Errorf("Username validation failed for: %s", username)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user