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
Technical Requirements
Files to Remove/Replace
-
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
-
Wallet Provider Replacement
- Path:
src/providers/wallet.provider.tsx
- Replace wallet kit logic with passkeys implementation
-
Authentication Hook Overhaul
- Path:
src/components/modules/auth/hooks/wallet.hook.ts
- Complete rewrite for passkeys authentication
Files to Create
-
Passkeys Service
- Path:
src/services/passkeys.service.ts
- Purpose: WebAuthn implementation and passkey management
-
Authentication Service
- Path:
src/services/auth.service.ts
- Purpose: Authentication flow orchestration
-
Credential Storage
- Path:
src/lib/credential-storage.ts
- Purpose: Secure credential management
-
WebAuthn Types
- Path:
src/@types/webauthn.entity.ts
- Purpose: TypeScript definitions for WebAuthn
-
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
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
Acceptance Criteria
@creit.tech/stellar-wallets-kitdependencyTechnical Requirements
Files to Remove/Replace
Wallet Kit Dependencies
@creit.tech/stellar-wallets-kitfrom package.jsonsrc/config/wallet-kit.tssrc/components/modules/auth/helpers/stellar-wallet-kit.helper.tsWallet Provider Replacement
src/providers/wallet.provider.tsxAuthentication Hook Overhaul
src/components/modules/auth/hooks/wallet.hook.tsFiles to Create
Passkeys Service
src/services/passkeys.service.tsAuthentication Service
src/services/auth.service.tsCredential Storage
src/lib/credential-storage.tsWebAuthn Types
src/@types/webauthn.entity.tsPasskeys Components
src/components/modules/auth/ui/passkeys/Implementation Details
Passkeys Service Implementation
Authentication Provider Replacement
Passkey Authentication Components
Browser Support and Fallbacks
WebAuthn Support Detection
Fallback Authentication
For browsers that don't support passkeys:
Migration Strategy
Phase 1: Parallel Implementation
Phase 2: Gradual Migration
Phase 3: Complete Migration
Security Considerations
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
Integration Tests
Browser Testing
Definition of Done