Skip to content
This repository was archived by the owner on Dec 2, 2025. It is now read-only.
This repository was archived by the owner on Dec 2, 2025. It is now read-only.

Remove Stellar Wallet Kit and Implement Passkeys Authentication #261

@JosueBrenes

Description

@JosueBrenes

Description

Remove the dependency on Stellar Wallet Kit and implement modern passkeys authentication using WebAuthn. This will provide a more secure, user-friendly authentication experience without requiring external wallet extensions like Freighter.

Reference Implementation: Stellar Smart Wallet Demo

What to Implement

  • Replace Stellar Wallet Kit with WebAuthn/Passkeys implementation
  • Remove Freighter wallet dependency
  • Implement secure passkey registration and authentication flow
  • Create backup authentication methods
  • Maintain backward compatibility during transition

Acceptance Criteria

  • Complete removal of @creit.tech/stellar-wallets-kit dependency
  • Passkeys registration flow working on all major browsers
  • Passkeys authentication for returning users
  • Fallback authentication methods for unsupported browsers
  • Secure storage of authentication credentials
  • Migration path for existing wallet-connected users

Technical Requirements

Files to Remove/Replace

  1. Wallet Kit Dependencies

    • Remove: @creit.tech/stellar-wallets-kit from package.json
    • Remove: src/config/wallet-kit.ts
    • Remove: src/components/modules/auth/helpers/stellar-wallet-kit.helper.ts
  2. Wallet Provider Replacement

    • Path: src/providers/wallet.provider.tsx
    • Replace wallet kit logic with passkeys implementation
  3. Authentication Hook Overhaul

    • Path: src/components/modules/auth/hooks/wallet.hook.ts
    • Complete rewrite for passkeys authentication

Files to Create

  1. Passkeys Service

    • Path: src/services/passkeys.service.ts
    • Purpose: WebAuthn implementation and passkey management
  2. Authentication Service

    • Path: src/services/auth.service.ts
    • Purpose: Authentication flow orchestration
  3. Credential Storage

    • Path: src/lib/credential-storage.ts
    • Purpose: Secure credential management
  4. WebAuthn Types

    • Path: src/@types/webauthn.entity.ts
    • Purpose: TypeScript definitions for WebAuthn
  5. Passkeys Components

    • Path: src/components/modules/auth/ui/passkeys/
    • Contents: Registration, authentication, and management components

Implementation Details

Passkeys Service Implementation

// src/services/passkeys.service.ts
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';

export interface PasskeyCredential {
  id: string;
  publicKey: Uint8Array;
  userId: string;
  displayName: string;
  createdAt: Date;
}

export class PasskeysService {
  private rpID = 'trustbridge.app'; // Replace with actual domain
  private rpName = 'TrustBridge';

  async isSupported(): Promise<boolean> {
    return (
      typeof window !== 'undefined' &&
      window.PublicKeyCredential &&
      typeof window.PublicKeyCredential === 'function' &&
      await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
    );
  }

  async registerPasskey(username: string, displayName: string): Promise<PasskeyCredential> {
    const challenge = crypto.getRandomValues(new Uint8Array(32));
    const userId = crypto.getRandomValues(new Uint8Array(32));

    const registrationOptions = {
      rp: {
        name: this.rpName,
        id: this.rpID,
      },
      user: {
        id: userId,
        name: username,
        displayName: displayName,
      },
      challenge: challenge,
      pubKeyCredParams: [
        { alg: -7, type: 'public-key' as const }, // ES256
        { alg: -257, type: 'public-key' as const }, // RS256
      ],
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        userVerification: 'required',
        requireResidentKey: true,
      },
      timeout: 60000,
      attestation: 'direct' as const,
    };

    try {
      const registrationResponse = await startRegistration(registrationOptions);

      // Store credential securely
      const credential: PasskeyCredential = {
        id: registrationResponse.id,
        publicKey: new Uint8Array(registrationResponse.response.publicKey),
        userId: Array.from(userId).join(','),
        displayName: displayName,
        createdAt: new Date(),
      };

      await this.storeCredential(credential);
      return credential;

    } catch (error) {
      console.error('Passkey registration failed:', error);
      throw new Error('Failed to register passkey');
    }
  }

  async authenticatePasskey(credentialId?: string): Promise<PasskeyCredential> {
    const challenge = crypto.getRandomValues(new Uint8Array(32));

    const authenticationOptions = {
      challenge: challenge,
      rpId: this.rpID,
      allowCredentials: credentialId ? [{
        id: credentialId,
        type: 'public-key' as const,
      }] : [],
      userVerification: 'required' as const,
      timeout: 60000,
    };

    try {
      const authenticationResponse = await startAuthentication(authenticationOptions);

      // Verify and retrieve stored credential
      const credential = await this.getStoredCredential(authenticationResponse.id);
      if (!credential) {
        throw new Error('Credential not found');
      }

      return credential;

    } catch (error) {
      console.error('Passkey authentication failed:', error);
      throw new Error('Failed to authenticate with passkey');
    }
  }

  async getAvailableCredentials(): Promise<PasskeyCredential[]> {
    const stored = localStorage.getItem('trustbridge_passkeys');
    if (!stored) return [];

    try {
      return JSON.parse(stored);
    } catch {
      return [];
    }
  }

  private async storeCredential(credential: PasskeyCredential): Promise<void> {
    const existing = await this.getAvailableCredentials();
    const updated = [...existing, credential];
    localStorage.setItem('trustbridge_passkeys', JSON.stringify(updated));
  }

  private async getStoredCredential(id: string): Promise<PasskeyCredential | null> {
    const credentials = await this.getAvailableCredentials();
    return credentials.find(cred => cred.id === id) || null;
  }

  async removeCredential(credentialId: string): Promise<void> {
    const credentials = await this.getAvailableCredentials();
    const filtered = credentials.filter(cred => cred.id !== credentialId);
    localStorage.setItem('trustbridge_passkeys', JSON.stringify(filtered));
  }
}

export const passkeysService = new PasskeysService();

Authentication Provider Replacement

// Updated src/providers/wallet.provider.tsx
interface AuthContextType {
  isAuthenticated: boolean;
  user: AuthUser | null;
  stellarAccount: string | null;
  login: (username: string, displayName: string) => Promise<void>;
  authenticate: (credentialId?: string) => Promise<void>;
  logout: () => Promise<void>;
  isLoading: boolean;
  error: string | null;
}

interface AuthUser {
  id: string;
  username: string;
  displayName: string;
  credentialId: string;
  stellarPublicKey: string;
  createdAt: Date;
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [user, setUser] = useState<AuthUser | null>(null);
  const [stellarAccount, setStellarAccount] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Check for existing authentication on mount
  useEffect(() => {
    checkExistingAuth();
  }, []);

  const checkExistingAuth = async () => {
    const storedUser = localStorage.getItem('trustbridge_auth_user');
    if (storedUser) {
      try {
        const userData = JSON.parse(storedUser);
        setUser(userData);
        setStellarAccount(userData.stellarPublicKey);
        setIsAuthenticated(true);
      } catch {
        localStorage.removeItem('trustbridge_auth_user');
      }
    }
  };

  const login = async (username: string, displayName: string) => {
    setIsLoading(true);
    setError(null);

    try {
      // Check if passkeys are supported
      const isSupported = await passkeysService.isSupported();
      if (!isSupported) {
        throw new Error('Passkeys are not supported on this device');
      }

      // Register new passkey
      const credential = await passkeysService.registerPasskey(username, displayName);

      // Create Stellar keypair (will be implemented in separate issue)
      const stellarKeypair = await createStellarWallet(credential.userId);

      // Create user object
      const newUser: AuthUser = {
        id: credential.userId,
        username,
        displayName,
        credentialId: credential.id,
        stellarPublicKey: stellarKeypair.publicKey(),
        createdAt: new Date(),
      };

      // Store user data
      localStorage.setItem('trustbridge_auth_user', JSON.stringify(newUser));

      setUser(newUser);
      setStellarAccount(newUser.stellarPublicKey);
      setIsAuthenticated(true);

    } catch (err) {
      setError(err instanceof Error ? err.message : 'Authentication failed');
    } finally {
      setIsLoading(false);
    }
  };

  const authenticate = async (credentialId?: string) => {
    setIsLoading(true);
    setError(null);

    try {
      const credential = await passkeysService.authenticatePasskey(credentialId);

      // Retrieve user data
      const storedUser = localStorage.getItem('trustbridge_auth_user');
      if (!storedUser) {
        throw new Error('User data not found');
      }

      const userData = JSON.parse(storedUser);
      setUser(userData);
      setStellarAccount(userData.stellarPublicKey);
      setIsAuthenticated(true);

    } catch (err) {
      setError(err instanceof Error ? err.message : 'Authentication failed');
    } finally {
      setIsLoading(false);
    }
  };

  const logout = async () => {
    setUser(null);
    setStellarAccount(null);
    setIsAuthenticated(false);
    localStorage.removeItem('trustbridge_auth_user');
  };

  return (
    <AuthContext.Provider value={{
      isAuthenticated,
      user,
      stellarAccount,
      login,
      authenticate,
      logout,
      isLoading,
      error,
    }}>
      {children}
    </AuthContext.Provider>
  );
}

Passkey Authentication Components

// src/components/modules/auth/ui/passkeys/PasskeyLogin.tsx
export function PasskeyLogin() {
  const { login, authenticate, isLoading, error } = useAuth();
  const [isNewUser, setIsNewUser] = useState(false);
  const [username, setUsername] = useState('');
  const [displayName, setDisplayName] = useState('');
  const [availableCredentials, setAvailableCredentials] = useState<PasskeyCredential[]>([]);

  useEffect(() => {
    loadAvailableCredentials();
  }, []);

  const loadAvailableCredentials = async () => {
    const credentials = await passkeysService.getAvailableCredentials();
    setAvailableCredentials(credentials);
    setIsNewUser(credentials.length === 0);
  };

  const handleRegister = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!username.trim() || !displayName.trim()) return;

    await login(username.trim(), displayName.trim());
  };

  const handleAuthenticate = async (credentialId?: string) => {
    await authenticate(credentialId);
  };

  if (isNewUser) {
    return (
      <Card className="w-full max-w-md mx-auto">
        <CardHeader>
          <CardTitle>Create Your Account</CardTitle>
          <CardDescription>
            Set up secure passkey authentication for TrustBridge
          </CardDescription>
        </CardHeader>
        <CardContent>
          <form onSubmit={handleRegister} className="space-y-4">
            <div>
              <Label htmlFor="username">Username</Label>
              <Input
                id="username"
                value={username}
                onChange={(e) => setUsername(e.target.value)}
                placeholder="Enter your username"
                required
              />
            </div>

            <div>
              <Label htmlFor="displayName">Display Name</Label>
              <Input
                id="displayName"
                value={displayName}
                onChange={(e) => setDisplayName(e.target.value)}
                placeholder="Enter your display name"
                required
              />
            </div>

            {error && (
              <Alert variant="destructive">
                <AlertCircle className="h-4 w-4" />
                <AlertDescription>{error}</AlertDescription>
              </Alert>
            )}

            <Button type="submit" className="w-full" disabled={isLoading}>
              {isLoading ? (
                <>
                  <Loader className="mr-2 h-4 w-4 animate-spin" />
                  Creating Account...
                </>
              ) : (
                'Create Account with Passkey'
              )}
            </Button>
          </form>
        </CardContent>
      </Card>
    );
  }

  return (
    <Card className="w-full max-w-md mx-auto">
      <CardHeader>
        <CardTitle>Sign In</CardTitle>
        <CardDescription>
          Use your passkey to securely sign in to TrustBridge
        </CardDescription>
      </CardHeader>
      <CardContent className="space-y-4">
        {availableCredentials.map((credential) => (
          <Button
            key={credential.id}
            variant="outline"
            className="w-full justify-start"
            onClick={() => handleAuthenticate(credential.id)}
            disabled={isLoading}
          >
            <User className="mr-2 h-4 w-4" />
            {credential.displayName}
          </Button>
        ))}

        <Button
          variant="default"
          className="w-full"
          onClick={() => handleAuthenticate()}
          disabled={isLoading}
        >
          {isLoading ? (
            <>
              <Loader className="mr-2 h-4 w-4 animate-spin" />
              Authenticating...
            </>
          ) : (
            <>
              <Fingerprint className="mr-2 h-4 w-4" />
              Sign in with Passkey
            </>
          )}
        </Button>

        {error && (
          <Alert variant="destructive">
            <AlertCircle className="h-4 w-4" />
            <AlertDescription>{error}</AlertDescription>
          </Alert>
        )}

        <div className="text-center">
          <Button
            variant="link"
            onClick={() => setIsNewUser(true)}
            className="text-sm"
          >
            Create new account instead
          </Button>
        </div>
      </CardContent>
    </Card>
  );
}

Browser Support and Fallbacks

WebAuthn Support Detection

// src/lib/webauthn-support.ts
export async function checkWebAuthnSupport() {
  if (typeof window === 'undefined') return false;

  const support = {
    webauthn: !!window.PublicKeyCredential,
    platform: false,
    crossPlatform: false,
  };

  if (support.webauthn) {
    try {
      support.platform = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
      support.crossPlatform = await PublicKeyCredential.isConditionalMediationAvailable?.() || false;
    } catch {
      // Fallback if methods are not available
    }
  }

  return support;
}

Fallback Authentication

For browsers that don't support passkeys:

  • Email + OTP verification
  • Traditional password authentication
  • QR code for mobile passkey setup

Migration Strategy

Phase 1: Parallel Implementation

  • Keep existing wallet kit functionality
  • Add passkeys as alternative authentication
  • Allow users to migrate voluntarily

Phase 2: Gradual Migration

  • Prompt existing users to set up passkeys
  • Provide migration wizard
  • Maintain backward compatibility

Phase 3: Complete Migration

  • Remove wallet kit dependency
  • Passkeys as primary authentication
  • Legacy support for existing sessions

Security Considerations

  • Secure credential storage with encryption
  • Proper challenge generation and validation
  • Protection against replay attacks
  • Secure backup and recovery mechanisms
  • Privacy protection for biometric data

Dependencies

New Dependencies to Add

{
  "@simplewebauthn/browser": "^9.0.0",
  "@simplewebauthn/server": "^9.0.0",
  "@simplewebauthn/types": "^9.0.0"
}

Dependencies to Remove

{
  "@creit.tech/stellar-wallets-kit": "remove"
}

Testing Strategy

Unit Tests

  • Passkey registration flow
  • Authentication flow
  • Error handling scenarios
  • Credential storage and retrieval

Integration Tests

  • End-to-end authentication flow
  • Browser compatibility testing
  • Fallback mechanism testing
  • Migration scenario testing

Browser Testing

  • Chrome (Windows, macOS, Android)
  • Safari (macOS, iOS)
  • Firefox (Windows, macOS)
  • Edge (Windows)

Definition of Done

  • Stellar Wallet Kit completely removed from codebase
  • Passkeys registration working on supported browsers
  • Passkeys authentication working for returning users
  • Fallback authentication methods implemented
  • Security measures properly implemented
  • Browser compatibility tested and documented
  • Migration path for existing users working
  • Comprehensive testing coverage
  • Documentation updated for new authentication flow

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions