Skip to main content

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

Before using the recovery system, ensure:

  1. Your user is registered with externalId + password authentication
  2. You have initialized the Sphere SDK
  3. 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

MethodAuthenticationDescription
createRecoveryMethods()User AuthCreate initial recovery methods
getRecoveryOptions()PublicGet masked recovery options
requestPasswordReset()PublicRequest password reset OTP
resetPassword()PublicReset password with OTP
updateRecoveryMethods()User AuthReplace all recovery methods
addRecoveryMethod()User AuthAdd a specific recovery method
removeRecoveryMethod()User AuthRemove a specific recovery method
updateRecoveryMethod()User AuthUpdate a specific recovery method
getMyRecoveryMethods()User AuthGet current recovery methods
deleteAllRecoveryMethods()User AuthDelete all recovery methods

Security Considerations

  1. Rate Limiting: Password reset requests are limited to 5 attempts per 5 minutes per IP
  2. OTP Expiration: OTP codes expire after a set time (typically 5-15 minutes)
  3. Masked Information: Recovery options are masked when displayed publicly
  4. Minimum Requirements: At least one recovery method must always be maintained
  5. Password Validation: Implement strong password requirements in your application
  6. Secure Storage: Never store passwords in plain text
  7. HTTPS Only: Always use HTTPS in production for all API calls

Troubleshooting

Common Issues and Solutions

IssueSolution
"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