mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-03 09:51:39 +00:00
241 lines
7.0 KiB
Go
241 lines
7.0 KiB
Go
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
|
||
|
|
}
|