Account Recovery & Password Reset
The Sphere SDK provides a comprehensive recovery system for users who authenticate with externalId
and password
. This system allows users to set up recovery methods (email and/or phone), request password resets, and manage their recovery options.
Overview
The recovery system is designed to:
- Allow users to recover their accounts if they forget their password
- Support multiple recovery methods (email and phone)
- Provide secure password reset flows with OTP verification
- Enable users to manage their recovery options
📝 Note: This recovery system is only available for users who authenticate using the
externalId
+password
method. Users authenticating with phone/email + OTP have built-in recovery through their authentication method.
Table of Contents
- Prerequisites
- Recovery Methods
- Password Reset Flow
- Complete Examples
- Error Handling
- Best Practices
- API Reference
Prerequisites
Before using the recovery system, ensure:
- Your user is registered with
externalId
+password
authentication - You have initialized the Sphere SDK
- The user has set up at least one recovery method
import Sphere from "@stratosphere-network/wallet";
const sphere = new Sphere({
environment: Environment.DEVELOPMENT,
apiKey: "YOUR_PROJECT_API_KEY",
});
Recovery Methods
Create Recovery Methods
Set up initial recovery methods for a user. This should be done immediately after user registration.
async function setupRecovery() {
try {
const response = await sphere.auth.createRecoveryMethods({
externalId: "user123",
password: "currentPassword",
emailRecovery: "recovery@example.com",
phoneRecovery: "+1234567890", // Optional
});
console.log("Recovery methods created:", response);
// Output: {
// message: "Recovery methods created successfully",
// recovery: {
// id: "uuid",
// email: "recovery@example.com",
// phoneNumber: "+1234567890",
// createdAt: "2024-01-01T00:00:00.000Z"
// }
// }
} catch (error) {
console.error("Failed to create recovery methods:", error);
}
}
Get Recovery Options
Retrieve masked recovery options for a user. This is a public endpoint that doesn't require authentication.
async function checkRecoveryOptions(externalId: string) {
try {
const response = await sphere.auth.getRecoveryOptions(externalId);
console.log("Available recovery options:", response);
// Output: {
// recoveryOptions: {
// email: "re***@example.com",
// phone: "+123***890"
// }
// }
} catch (error) {
console.error("No recovery options found");
}
}
Update Recovery Methods
Replace all existing recovery methods with new ones.
async function updateAllRecoveryMethods() {
try {
const response = await sphere.auth.updateRecoveryMethods({
externalId: "user123",
password: "currentPassword",
emailRecovery: "newemail@example.com",
phoneRecovery: "+9876543210",
});
console.log("Recovery methods updated:", response);
} catch (error) {
console.error("Failed to update recovery methods:", error);
}
}
Manage Individual Methods
Add a Recovery Method
async function addPhoneRecovery() {
try {
const response = await sphere.auth.addRecoveryMethod({
externalId: "user123",
password: "currentPassword",
method: "phoneRecovery",
value: "+9876543210",
});
console.log("Phone recovery added:", response);
} catch (error) {
console.error("Failed to add recovery method:", error);
}
}
Update a Specific Method
async function updateEmailRecovery() {
try {
const response = await sphere.auth.updateRecoveryMethod({
externalId: "user123",
password: "currentPassword",
method: "emailRecovery",
value: "updated@example.com",
});
console.log("Email recovery updated:", response);
} catch (error) {
console.error("Failed to update recovery method:", error);
}
}
Remove a Recovery Method
⚠️ Important: You cannot remove the last remaining recovery method. Ensure at least one method remains active.
async function removePhoneRecovery() {
try {
const response = await sphere.auth.removeRecoveryMethod({
externalId: "user123",
password: "currentPassword",
method: "phoneRecovery",
});
console.log("Phone recovery removed:", response);
} catch (error) {
console.error("Failed to remove recovery method:", error);
}
}
Get My Recovery Methods
Retrieve all recovery methods for the authenticated user.
async function getMyRecoveryInfo() {
try {
const response = await sphere.auth.getMyRecoveryMethods({
externalId: "user123",
password: "currentPassword",
});
console.log("My recovery methods:", response);
// Output: {
// recovery: {
// id: "uuid",
// email: "recovery@example.com",
// phoneNumber: "+1234567890",
// createdAt: "2024-01-01T00:00:00.000Z",
// updatedAt: "2024-01-01T00:00:00.000Z"
// }
// }
} catch (error) {
console.error("Failed to get recovery methods:", error);
}
}
Delete All Recovery Methods
async function deleteAllRecovery() {
try {
const response = await sphere.auth.deleteAllRecoveryMethods({
externalId: "user123",
password: "currentPassword",
});
console.log("All recovery methods deleted:", response);
} catch (error) {
console.error("Failed to delete recovery methods:", error);
}
}
Password Reset Flow
Request Password Reset
Initiate a password reset by sending an OTP to the selected recovery method.
📝 Rate Limiting: Password reset requests are limited to 5 attempts per 5 minutes per IP address.
async function initiatePasswordReset(externalId: string) {
try {
// First, check available options
const options = await sphere.auth.getRecoveryOptions(externalId);
if (!options.recoveryOptions.email && !options.recoveryOptions.phone) {
throw new Error("No recovery methods available");
}
// Request reset via email or phone
const response = await sphere.auth.requestPasswordReset({
externalId: externalId,
method: options.recoveryOptions.email ? "emailRecovery" : "phoneRecovery",
});
console.log("Password reset initiated:", response);
// Output: {
// message: "Password reset OTP sent to emailRecovery",
// target: "recovery@example.com"
// }
} catch (error) {
console.error("Failed to request password reset:", error);
}
}
Reset Password with OTP
Complete the password reset using the OTP received.
async function resetPasswordWithOTP() {
try {
// Reset with email OTP
const emailReset = await sphere.auth.resetPassword({
username: "user123",
newPassword: "newSecurePassword123!",
email: "recovery@example.com",
otpCode: "123456",
});
// OR reset with phone OTP
const phoneReset = await sphere.auth.resetPassword({
username: "user123",
newPassword: "newSecurePassword123!",
phoneNumber: "+1234567890",
otpCode: "123456",
});
console.log("Password reset successful");
} catch (error) {
console.error("Failed to reset password:", error);
}
}
Complete Examples
Full Password Reset Flow
Here's a complete example of implementing a password reset flow:
class PasswordResetFlow {
constructor(private sphere: Sphere) {}
async executePasswordReset(externalId: string) {
try {
// Step 1: Check available recovery options
console.log("Checking recovery options...");
const options = await this.sphere.auth.getRecoveryOptions(externalId);
if (!options.recoveryOptions.email && !options.recoveryOptions.phone) {
throw new Error(
"No recovery methods available. Please contact support."
);
}
// Step 2: Display available methods to user
const availableMethods = [];
if (options.recoveryOptions.email) {
availableMethods.push({
method: "emailRecovery",
display: `Email: ${options.recoveryOptions.email}`,
});
}
if (options.recoveryOptions.phone) {
availableMethods.push({
method: "phoneRecovery",
display: `Phone: ${options.recoveryOptions.phone}`,
});
}
// Step 3: Let user choose recovery method (in a real app, this would be UI)
const selectedMethod = availableMethods[0].method;
// Step 4: Request password reset
console.log(`Sending OTP via ${selectedMethod}...`);
const resetRequest = await this.sphere.auth.requestPasswordReset({
externalId,
method: selectedMethod as "emailRecovery" | "phoneRecovery",
});
console.log(`OTP sent to: ${resetRequest.target}`);
// Step 5: Get OTP from user (in real app, this would be user input)
const otpCode = await this.promptUserForOTP();
// Step 6: Get new password from user
const newPassword = await this.promptUserForNewPassword();
// Step 7: Reset password
console.log("Resetting password...");
const resetData =
selectedMethod === "emailRecovery"
? {
username: externalId,
newPassword,
email: await this.promptUserForEmail(),
otpCode,
}
: {
username: externalId,
newPassword,
phoneNumber: await this.promptUserForPhone(),
otpCode,
};
await this.sphere.auth.resetPassword(resetData);
console.log("Password reset successful!");
return true;
} catch (error) {
console.error("Password reset failed:", error);
return false;
}
}
// Helper methods (implement based on your UI)
private async promptUserForOTP(): Promise<string> {
// In a real app, get OTP from user interface
return "123456";
}
private async promptUserForNewPassword(): Promise<string> {
// In a real app, get new password from user interface
return "newSecurePassword123!";
}
private async promptUserForEmail(): Promise<string> {
// In a real app, get email from user interface
return "user@example.com";
}
private async promptUserForPhone(): Promise<string> {
// In a real app, get phone from user interface
return "+1234567890";
}
}
// Usage
const passwordResetFlow = new PasswordResetFlow(sphere);
await passwordResetFlow.executePasswordReset("user123");
User Registration with Recovery Setup
Always set up recovery methods during user registration:
async function registerUserWithRecovery(userData: {
username: string;
password: string;
email: string;
phone?: string;
}) {
try {
// Step 1: Create user account
const signupResponse = await sphere.auth.signup({
externalId: userData.username,
password: userData.password,
});
console.log("User account created");
// Step 2: Login to get authentication
const loginResponse = await sphere.auth.login({
externalId: userData.username,
password: userData.password,
});
console.log("User logged in");
// Step 3: Immediately set up recovery methods
const recoveryResponse = await sphere.auth.createRecoveryMethods({
externalId: userData.username,
password: userData.password,
emailRecovery: userData.email,
phoneRecovery: userData.phone,
});
console.log("Recovery methods configured");
return {
user: loginResponse.user,
recovery: recoveryResponse.recovery,
};
} catch (error) {
console.error("Registration failed:", error);
throw error;
}
}
Recovery Management Dashboard
Example of a recovery management interface:
class RecoveryManager {
constructor(private sphere: Sphere) {}
async displayRecoveryDashboard(externalId: string, password: string) {
try {
// Get current recovery methods
const current = await this.sphere.auth.getMyRecoveryMethods({
externalId,
password,
});
console.log("Current Recovery Methods:");
console.log("Email:", current.recovery.email || "Not set");
console.log("Phone:", current.recovery.phoneNumber || "Not set");
return current.recovery;
} catch (error) {
console.error("Failed to load recovery methods:", error);
throw error;
}
}
async addOrUpdateRecoveryMethod(
externalId: string,
password: string,
method: "emailRecovery" | "phoneRecovery",
value: string
) {
try {
// Check if method exists
const current = await this.sphere.auth.getMyRecoveryMethods({
externalId,
password,
});
const methodExists =
method === "emailRecovery"
? !!current.recovery.email
: !!current.recovery.phoneNumber;
if (methodExists) {
// Update existing
return await this.sphere.auth.updateRecoveryMethod({
externalId,
password,
method,
value,
});
} else {
// Add new
return await this.sphere.auth.addRecoveryMethod({
externalId,
password,
method,
value,
});
}
} catch (error) {
console.error("Failed to update recovery method:", error);
throw error;
}
}
async removeRecoveryMethodSafely(
externalId: string,
password: string,
method: "emailRecovery" | "phoneRecovery"
) {
try {
// Check if it's the last method
const current = await this.sphere.auth.getMyRecoveryMethods({
externalId,
password,
});
const hasEmail = !!current.recovery.email;
const hasPhone = !!current.recovery.phoneNumber;
if (
(method === "emailRecovery" && !hasPhone) ||
(method === "phoneRecovery" && !hasEmail)
) {
throw new Error(
"Cannot remove the last recovery method. Add another method first."
);
}
return await this.sphere.auth.removeRecoveryMethod({
externalId,
password,
method,
});
} catch (error) {
console.error("Failed to remove recovery method:", error);
throw error;
}
}
}
Error Handling
Common Error Scenarios
async function handleRecoveryErrors() {
try {
await sphere.auth.requestPasswordReset({
externalId: "user123",
method: "emailRecovery",
});
} catch (error: any) {
if (error.response) {
switch (error.response.status) {
case 400:
console.error("Validation error:", error.response.data.message);
// Handle: Invalid email/phone format, missing required fields
break;
case 403:
console.error("Authentication failed:", error.response.data.message);
// Handle: Invalid password
break;
case 404:
console.error("Not found:", error.response.data.message);
// Handle: User not found, recovery method not found
break;
case 429:
console.error("Rate limited:", error.response.data.message);
// Handle: Too many password reset attempts
// Show user: "Please wait 5 minutes before trying again"
break;
default:
console.error("Unexpected error:", error.response.data);
}
} else {
console.error("Network error:", error.message);
}
}
}
Handling Specific Errors
// Handle attempt to remove last recovery method
async function safeRemoveRecoveryMethod(
externalId: string,
password: string,
methodToRemove: "emailRecovery" | "phoneRecovery"
) {
try {
await sphere.auth.removeRecoveryMethod({
externalId,
password,
method: methodToRemove,
});
} catch (error: any) {
if (
error.response?.data?.message?.includes(
"Cannot remove the last recovery method"
)
) {
console.error("You must maintain at least one recovery method.");
console.log(
"Please add another recovery method before removing this one."
);
} else {
throw error;
}
}
}
// Handle rate limiting gracefully
class RateLimitHandler {
private resetAttempts = new Map<string, number>();
private resetTimers = new Map<string, NodeJS.Timeout>();
async requestPasswordResetWithRateLimit(
externalId: string,
method: "emailRecovery" | "phoneRecovery"
) {
const attempts = this.resetAttempts.get(externalId) || 0;
if (attempts >= 5) {
throw new Error("Too many reset attempts. Please wait 5 minutes.");
}
try {
const response = await sphere.auth.requestPasswordReset({
externalId,
method,
});
// Increment attempts
this.resetAttempts.set(externalId, attempts + 1);
// Clear attempts after 5 minutes
if (!this.resetTimers.has(externalId)) {
const timer = setTimeout(() => {
this.resetAttempts.delete(externalId);
this.resetTimers.delete(externalId);
}, 5 * 60 * 1000);
this.resetTimers.set(externalId, timer);
}
return response;
} catch (error: any) {
if (error.response?.status === 429) {
this.resetAttempts.set(externalId, 5); // Max out attempts
throw new Error(
"Rate limit exceeded. Please wait 5 minutes before trying again."
);
}
throw error;
}
}
}
Best Practices
1. Always Set Up Recovery During Registration
// ✅ Good: Set up recovery immediately after registration
async function properUserRegistration(userData: any) {
const user = await sphere.auth.signup({
externalId: userData.username,
password: userData.password,
});
// Set up recovery methods right away
await sphere.auth.createRecoveryMethods({
externalId: userData.username,
password: userData.password,
emailRecovery: userData.email,
phoneRecovery: userData.phone,
});
}
// ❌ Bad: Delaying recovery setup
async function poorUserRegistration(userData: any) {
await sphere.auth.signup({
externalId: userData.username,
password: userData.password,
});
// User might forget to set up recovery methods later
}
2. Validate Recovery Information
function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function validatePhone(phone: string): boolean {
// E.164 format validation
const phoneRegex = /^\+[1-9]\d{1,14}$/;
return phoneRegex.test(phone);
}
async function updateRecoveryEmailSafely(
externalId: string,
password: string,
newEmail: string
) {
// Validate format
if (!validateEmail(newEmail)) {
throw new Error("Invalid email format");
}
// Update recovery email
return await sphere.auth.updateRecoveryMethod({
externalId,
password,
method: "emailRecovery",
value: newEmail,
});
}
3. Implement Password Strength Requirements
function validatePasswordStrength(password: string): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (password.length < 8) {
errors.push("Password must be at least 8 characters long");
}
if (!/[A-Z]/.test(password)) {
errors.push("Password must contain at least one uppercase letter");
}
if (!/[a-z]/.test(password)) {
errors.push("Password must contain at least one lowercase letter");
}
if (!/[0-9]/.test(password)) {
errors.push("Password must contain at least one number");
}
if (!/[!@#$%^&*]/.test(password)) {
errors.push("Password must contain at least one special character");
}
return {
isValid: errors.length === 0,
errors,
};
}
async function resetPasswordWithValidation(
username: string,
newPassword: string,
email: string,
otpCode: string
) {
// Validate password strength
const validation = validatePasswordStrength(newPassword);
if (!validation.isValid) {
throw new Error(
`Password requirements not met:\n${validation.errors.join("\n")}`
);
}
// Proceed with reset
return await sphere.auth.resetPassword({
username,
newPassword,
email,
otpCode,
});
}
4. Maintain Recovery Method Redundancy
async function ensureRecoveryRedundancy(externalId: string, password: string) {
const recovery = await sphere.auth.getMyRecoveryMethods({
externalId,
password,
});
const hasEmail = !!recovery.recovery.email;
const hasPhone = !!recovery.recovery.phoneNumber;
if (!hasEmail || !hasPhone) {
console.warn("⚠️ Recovery Warning:");
if (!hasEmail) {
console.warn(
"No email recovery set. Consider adding an email for account recovery."
);
}
if (!hasPhone) {
console.warn(
"No phone recovery set. Consider adding a phone number for account recovery."
);
}
return false;
}
return true;
}
API Reference
Types
// Recovery method types
type RecoveryMethod = "emailRecovery" | "phoneRecovery";
// Recovery data structure
interface RecoveryData {
id: string;
email?: string;
phoneNumber?: string;
createdAt: string;
updatedAt?: string;
}
// Request types
interface CreateRecoveryRequest {
externalId: string;
password: string;
emailRecovery?: string;
phoneRecovery?: string;
}
interface GetRecoveryOptionsResponse {
recoveryOptions: {
email?: string; // Masked email
phone?: string; // Masked phone
};
}
interface RequestPasswordResetRequest {
externalId: string;
method: RecoveryMethod;
}
interface RequestPasswordResetResponse {
message: string;
target: string; // The email/phone where OTP was sent
}
type ResetPasswordRequest = {
username: string;
newPassword: string;
otpCode: string;
} & (
| { email: string; phoneNumber?: never }
| { phoneNumber: string; email?: never }
);
interface UpdateRecoveryMethodRequest {
externalId: string;
password: string;
method: RecoveryMethod;
value: string;
}
Method Summary
Method | Authentication | Description |
---|---|---|
createRecoveryMethods() | User Auth | Create initial recovery methods |
getRecoveryOptions() | Public | Get masked recovery options |
requestPasswordReset() | Public | Request password reset OTP |
resetPassword() | Public | Reset password with OTP |
updateRecoveryMethods() | User Auth | Replace all recovery methods |
addRecoveryMethod() | User Auth | Add a specific recovery method |
removeRecoveryMethod() | User Auth | Remove a specific recovery method |
updateRecoveryMethod() | User Auth | Update a specific recovery method |
getMyRecoveryMethods() | User Auth | Get current recovery methods |
deleteAllRecoveryMethods() | User Auth | Delete all recovery methods |
Security Considerations
- Rate Limiting: Password reset requests are limited to 5 attempts per 5 minutes per IP
- OTP Expiration: OTP codes expire after a set time (typically 5-15 minutes)
- Masked Information: Recovery options are masked when displayed publicly
- Minimum Requirements: At least one recovery method must always be maintained
- Password Validation: Implement strong password requirements in your application
- Secure Storage: Never store passwords in plain text
- HTTPS Only: Always use HTTPS in production for all API calls
Troubleshooting
Common Issues and Solutions
Issue | Solution |
---|---|
"Cannot remove the last recovery method" | Add a new recovery method before removing the existing one |
"Rate limited: Too many requests" | Wait 5 minutes before attempting another password reset |
"Invalid credentials" | Verify the externalId and password are correct |
"OTP expired or invalid" | Request a new OTP and ensure it's entered quickly and correctly |
"User not found" | Check if the externalId exists in the system |
"Recovery method not found" | Ensure recovery methods are set up for the user |
Next Steps
- Learn about User Management for general user operations
- Explore Authentication Methods in the Getting Started guide
- Review Best Practices for secure implementation