This repository was archived by the owner on Dec 2, 2025. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 52
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
Copy link
Copy link
Open
Labels
External contributorsgood first issueGood for newcomersGood for newcomersonlydust-waveContribute to awesome OSS repos during OnlyDust's open source weekContribute to awesome OSS repos during OnlyDust's open source week
Description
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-kitdependency - 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
-
Wallet Kit Dependencies
- Remove:
@creit.tech/stellar-wallets-kitfrom package.json - Remove:
src/config/wallet-kit.ts - Remove:
src/components/modules/auth/helpers/stellar-wallet-kit.helper.ts
- Remove:
-
Wallet Provider Replacement
- Path:
src/providers/wallet.provider.tsx - Replace wallet kit logic with passkeys implementation
- Path:
-
Authentication Hook Overhaul
- Path:
src/components/modules/auth/hooks/wallet.hook.ts - Complete rewrite for passkeys authentication
- Path:
Files to Create
-
Passkeys Service
- Path:
src/services/passkeys.service.ts - Purpose: WebAuthn implementation and passkey management
- Path:
-
Authentication Service
- Path:
src/services/auth.service.ts - Purpose: Authentication flow orchestration
- Path:
-
Credential Storage
- Path:
src/lib/credential-storage.ts - Purpose: Secure credential management
- Path:
-
WebAuthn Types
- Path:
src/@types/webauthn.entity.ts - Purpose: TypeScript definitions for WebAuthn
- Path:
-
Passkeys Components
- Path:
src/components/modules/auth/ui/passkeys/ - Contents: Registration, authentication, and management components
- Path:
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
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
External contributorsgood first issueGood for newcomersGood for newcomersonlydust-waveContribute to awesome OSS repos during OnlyDust's open source weekContribute to awesome OSS repos during OnlyDust's open source week