Files
sonr/x/did/client/cli/cli.go
T

241 lines
7.0 KiB
Go
Raw Normal View History

2025-10-03 14:45:52 -04:00
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
}