This guide covers the complete password authentication workflow in the SSO platform. You’ll learn how to implement user registration, email/password login, password reset flows, and password management for existing users.
Prerequisites
Before implementing password authentication, ensure you have:
- Installed the SDK:
npm install @drmhse/sso-sdk - An AuthOS instance URL
- SMTP configured for sending verification and reset emails (platform-wide or organization-specific)
Overview
AuthOS supports native email/password authentication alongside OAuth providers. This gives users the flexibility to create accounts without requiring a third-party OAuth provider.
Key Features:
- Email verification required before login
- Secure password hashing (bcrypt)
- Password reset via email
- Change password (with current password verification)
- Set password for OAuth-only users
- Organization-specific SMTP support
User Registration
Registration creates a new user account with email and password. Users receive a verification email and must verify their email address before logging in.
Basic Registration
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: 'https://sso.example.com'
});
async function registerUser(email: string, password: string) {
try {
const response = await sso.auth.register({
email: email,
password: password
});
console.log(response.message);
// "Registration successful. Please check your email to verify your account."
return true;
} catch (error) {
if (error instanceof SsoApiError) {
console.error('Registration failed:', error.message);
// Handle specific errors (email already exists, weak password, etc.)
}
return false;
}
}
Registration with Organization Context
If your organization has configured custom SMTP settings, include the org_slug to ensure the verification email is sent using your organization’s email configuration:
async function registerUserWithOrgEmail(email: string, password: string) {
try {
const response = await sso.auth.register({
email: email,
password: password,
org_slug: 'acme-corp' // Use organization-specific SMTP
});
console.log(response.message);
return true;
} catch (error) {
if (error instanceof SsoApiError) {
console.error('Registration failed:', error.message);
}
return false;
}
}
React Registration Form
import { useState } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL
});
export function RegistrationForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
const validatePassword = (pass: string): string | null => {
if (pass.length < 8) {
return 'Password must be at least 8 characters';
}
if (!/[A-Z]/.test(pass)) {
return 'Password must contain at least one uppercase letter';
}
if (!/[a-z]/.test(pass)) {
return 'Password must contain at least one lowercase letter';
}
if (!/[0-9]/.test(pass)) {
return 'Password must contain at least one number';
}
return null;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
// Validate password strength
const passwordError = validatePassword(password);
if (passwordError) {
setError(passwordError);
return;
}
// Verify passwords match
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
setLoading(true);
try {
await sso.auth.register({
email,
password,
org_slug: 'acme-corp' // Optional
});
setSuccess(true);
} catch (err) {
if (err instanceof SsoApiError) {
if (err.statusCode === 409) {
setError('This email is already registered');
} else {
setError(err.message);
}
} else {
setError('Registration failed. Please try again.');
}
} finally {
setLoading(false);
}
};
if (success) {
return (
<div className="success-message">
<h2>Registration Successful!</h2>
<p>Please check your email to verify your account before logging in.</p>
<p>Check your spam folder if you don't see the email.</p>
</div>
);
}
return (
<form onSubmit={handleSubmit}>
<h2>Create Account</h2>
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="you@example.com"
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
placeholder="At least 8 characters"
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={loading}>
{loading ? 'Creating Account...' : 'Create Account'}
</button>
</form>
);
}
Password Requirements
The platform enforces strong password requirements:
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- Special characters recommended but not required
Email Verification
After registration, users receive an email with a verification link. The verification is handled automatically by the platform. Once verified, users can log in with their credentials.
Login with Email and Password
After email verification, users can authenticate with their email and password.
Basic Login
async function loginWithPassword(email: string, password: string) {
try {
// The SDK automatically handles token storage and configuration
await sso.auth.login({
email: email,
password: password
});
console.log('Login successful');
return true;
} catch (error) {
if (error instanceof SsoApiError) {
if (error.statusCode === 401) {
console.error('Invalid email or password');
} else if (error.statusCode === 403) {
console.error('Email not verified. Please check your email.');
} else {
console.error('Login failed:', error.message);
}
}
return false;
}
}
Login with MFA Support
If the user has MFA enabled, the login will return a pre-authentication token instead of a full session token. The full session is only stored after successful MFA verification:
async function loginWithPasswordAndMfa(email: string, password: string) {
try {
const tokens = await sso.auth.login({
email: email,
password: password
});
// Check if this is a pre-auth token (5 minute expiration = 300 seconds)
if (tokens.expires_in === 300) {
// User has MFA enabled - need to verify MFA code
// Session is NOT saved yet - waiting for MFA verification
return {
requiresMfa: true,
preauthToken: tokens.access_token
};
}
// Normal login without MFA - session is automatically saved
return {
requiresMfa: false,
success: true
};
} catch (error) {
if (error instanceof SsoApiError) {
throw error;
}
throw new Error('Login failed');
}
}
Complete Login Form with MFA
import { useState } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL
});
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [mfaCode, setMfaCode] = useState('');
const [preauthToken, setPreauthToken] = useState<string | null>(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handlePasswordLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const tokens = await sso.auth.login({
email,
password
});
// Check if MFA is required
if (tokens.expires_in === 300) {
setPreauthToken(tokens.access_token);
setLoading(false);
return;
}
// Normal login without MFA - session automatically saved, redirect to dashboard
window.location.href = '/dashboard';
} catch (err) {
if (err instanceof SsoApiError) {
if (err.statusCode === 401) {
setError('Invalid email or password');
} else if (err.statusCode === 403) {
setError('Please verify your email before logging in');
} else {
setError(err.message);
}
} else {
setError('Login failed. Please try again.');
}
setLoading(false);
}
};
const handleMfaVerification = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
// MFA verification automatically saves the full session
await sso.auth.verifyMfa(preauthToken!, mfaCode);
// Session is now authenticated, redirect to dashboard
window.location.href = '/dashboard';
} catch (err) {
if (err instanceof SsoApiError) {
setError('Invalid MFA code. Please try again.');
} else {
setError('Verification failed. Please try again.');
}
setLoading(false);
}
};
// Show MFA verification form
if (preauthToken) {
return (
<form onSubmit={handleMfaVerification}>
<h2>Two-Factor Authentication</h2>
<p>Enter the 6-digit code from your authenticator app</p>
<div className="form-group">
<input
type="text"
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value.replace(/\D/g, ''))}
maxLength={6}
placeholder="000000"
required
autoFocus
/>
</div>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={loading}>
{loading ? 'Verifying...' : 'Verify'}
</button>
<button
type="button"
onClick={() => {
setPreauthToken(null);
setMfaCode('');
setError('');
}}
>
Back to Login
</button>
</form>
);
}
// Show password login form
return (
<form onSubmit={handlePasswordLogin}>
<h2>Sign In</h2>
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="you@example.com"
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={loading}>
{loading ? 'Signing In...' : 'Sign In'}
</button>
<div className="links">
<a href="/forgot-password">Forgot password?</a>
<a href="/register">Create account</a>
</div>
</form>
);
}
Password Reset Workflow
The password reset flow consists of three steps:
- User requests a password reset
- User receives an email with a reset link
- User submits a new password with the reset token
Step 1: Request Password Reset
async function requestPasswordReset(email: string) {
try {
const response = await sso.auth.requestPasswordReset({
email: email,
org_slug: 'acme-corp' // Optional: use org-specific SMTP
});
console.log(response.message);
// "If the email exists, a password reset link has been sent"
return true;
} catch (error) {
console.error('Failed to send reset email:', error);
return false;
}
}
Security Note: The API always returns success, even if the email doesn’t exist. This prevents email enumeration attacks.
Step 2: User Receives Email
The user receives an email with a password reset link in this format:
https://yourdomain.com/reset-password?token=RESET_TOKEN_HERE
You need to implement the /reset-password route in your application to handle this.
Step 3: Reset Password with Token
async function resetPassword(token: string, newPassword: string) {
try {
const response = await sso.auth.resetPassword({
token: token,
new_password: newPassword
});
console.log(response.message);
// "Password reset successful"
return true;
} catch (error) {
if (error instanceof SsoApiError) {
if (error.statusCode === 400) {
console.error('Invalid or expired reset token');
} else {
console.error('Password reset failed:', error.message);
}
}
return false;
}
}
Complete Password Reset Flow
// ForgotPasswordPage.tsx
import { useState } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL
});
export function ForgotPasswordPage() {
const [email, setEmail] = useState('');
const [submitted, setSubmitted] = useState(false);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await sso.auth.requestPasswordReset({
email,
org_slug: 'acme-corp'
});
setSubmitted(true);
} catch (error) {
// Even on error, show success to prevent email enumeration
setSubmitted(true);
} finally {
setLoading(false);
}
};
if (submitted) {
return (
<div className="success-message">
<h2>Check Your Email</h2>
<p>
If an account exists with <strong>{email}</strong>, you will receive
a password reset link shortly.
</p>
<p>Check your spam folder if you don't see the email.</p>
<a href="/login">Back to Login</a>
</div>
);
}
return (
<form onSubmit={handleSubmit}>
<h2>Forgot Password</h2>
<p>Enter your email address and we'll send you a password reset link.</p>
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="you@example.com"
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send Reset Link'}
</button>
<a href="/login">Back to Login</a>
</form>
);
}
// ResetPasswordPage.tsx
import { useState, useEffect } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
import { useSearchParams } from 'react-router-dom';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL
});
export function ResetPasswordPage() {
const [searchParams] = useSearchParams();
const [token, setToken] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
const resetToken = searchParams.get('token');
if (resetToken) {
setToken(resetToken);
} else {
setError('Invalid reset link');
}
}, [searchParams]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
return;
}
setLoading(true);
try {
await sso.auth.resetPassword({
token,
new_password: password
});
setSuccess(true);
} catch (err) {
if (err instanceof SsoApiError) {
if (err.statusCode === 400) {
setError('This reset link is invalid or has expired');
} else {
setError(err.message);
}
} else {
setError('Password reset failed. Please try again.');
}
setLoading(false);
}
};
if (success) {
return (
<div className="success-message">
<h2>Password Reset Complete</h2>
<p>Your password has been successfully reset.</p>
<a href="/login">Sign In</a>
</div>
);
}
return (
<form onSubmit={handleSubmit}>
<h2>Reset Password</h2>
<div className="form-group">
<label htmlFor="password">New Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
placeholder="At least 8 characters"
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm New Password</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={loading || !token}>
{loading ? 'Resetting...' : 'Reset Password'}
</button>
</form>
);
}
Change Password (Authenticated Users)
Authenticated users can change their password by providing their current password for verification.
Basic Password Change
async function changePassword(currentPassword: string, newPassword: string) {
try {
const response = await sso.user.changePassword({
current_password: currentPassword,
new_password: newPassword
});
console.log(response.message);
// "Password changed successfully"
return true;
} catch (error) {
if (error instanceof SsoApiError) {
if (error.statusCode === 401) {
console.error('Current password is incorrect');
} else {
console.error('Password change failed:', error.message);
}
}
return false;
}
}
Change Password Form
import { useState } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
interface ChangePasswordFormProps {
sso: SsoClient;
}
export function ChangePasswordForm({ sso }: ChangePasswordFormProps) {
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess(false);
if (newPassword !== confirmPassword) {
setError('New passwords do not match');
return;
}
if (newPassword.length < 8) {
setError('New password must be at least 8 characters');
return;
}
if (newPassword === currentPassword) {
setError('New password must be different from current password');
return;
}
setLoading(true);
try {
await sso.user.changePassword({
current_password: currentPassword,
new_password: newPassword
});
setSuccess(true);
// Clear form
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
} catch (err) {
if (err instanceof SsoApiError) {
if (err.statusCode === 401) {
setError('Current password is incorrect');
} else {
setError(err.message);
}
} else {
setError('Failed to change password. Please try again.');
}
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<h2>Change Password</h2>
<div className="form-group">
<label htmlFor="currentPassword">Current Password</label>
<input
id="currentPassword"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="newPassword">New Password</label>
<input
id="newPassword"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
placeholder="At least 8 characters"
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm New Password</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
{error && <div className="error">{error}</div>}
{success && (
<div className="success">Password changed successfully!</div>
)}
<button type="submit" disabled={loading}>
{loading ? 'Changing Password...' : 'Change Password'}
</button>
</form>
);
}
Set Password for OAuth Users
Users who originally signed up via OAuth (GitHub, Google, Microsoft) don’t have a password. They can set a password to enable password-based login.
Set Password
async function setPasswordForOAuthUser(newPassword: string) {
try {
const response = await sso.user.setPassword({
new_password: newPassword
});
console.log(response.message);
// "Password set successfully"
return true;
} catch (error) {
if (error instanceof SsoApiError) {
if (error.statusCode === 409) {
console.error('Password already set. Use change password instead.');
} else {
console.error('Failed to set password:', error.message);
}
}
return false;
}
}
Set Password Form with Detection
import { useState, useEffect } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
interface SetPasswordFormProps {
sso: SsoClient;
}
export function SetPasswordForm({ sso }: SetPasswordFormProps) {
const [hasPassword, setHasPassword] = useState<boolean | null>(null);
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
checkPasswordStatus();
}, []);
const checkPasswordStatus = async () => {
try {
const profile = await sso.user.getProfile();
// Check if user has password (this would be in profile if available)
// Or attempt to set password and handle the 409 error
setHasPassword(false); // Assume no password for OAuth users
} catch (error) {
console.error('Failed to check password status:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess(false);
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
return;
}
setLoading(true);
try {
await sso.user.setPassword({
new_password: password
});
setSuccess(true);
setHasPassword(true);
setPassword('');
setConfirmPassword('');
} catch (err) {
if (err instanceof SsoApiError) {
if (err.statusCode === 409) {
setError('You already have a password set. Use the change password form instead.');
setHasPassword(true);
} else {
setError(err.message);
}
} else {
setError('Failed to set password. Please try again.');
}
} finally {
setLoading(false);
}
};
if (hasPassword === true) {
return (
<div className="info-message">
<p>You already have a password set.</p>
<p>Use the "Change Password" option if you want to update it.</p>
</div>
);
}
if (hasPassword === null) {
return <div>Loading...</div>;
}
return (
<form onSubmit={handleSubmit}>
<h2>Set Password</h2>
<p>
You signed up using a social account. Set a password to enable
email/password login.
</p>
<div className="form-group">
<label htmlFor="password">New Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
placeholder="At least 8 characters"
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
{error && <div className="error">{error}</div>}
{success && (
<div className="success">
Password set successfully! You can now log in with email and password.
</div>
)}
<button type="submit" disabled={loading}>
{loading ? 'Setting Password...' : 'Set Password'}
</button>
</form>
);
}
Best Practices
Password Security
- Client-Side Validation: Validate password strength before submission
- Never Log Passwords: Never log passwords in plain text
- HTTPS Only: Always use HTTPS in production
- Rate Limiting: The platform implements rate limiting on auth endpoints
- Secure Storage: Never store passwords in browser storage
Password Strength Requirements
Implement client-side validation to provide immediate feedback:
interface PasswordStrength {
score: number; // 0-4
feedback: string[];
isStrong: boolean;
}
function checkPasswordStrength(password: string): PasswordStrength {
const feedback: string[] = [];
let score = 0;
// Length check
if (password.length >= 8) score++;
if (password.length >= 12) score++;
// Character variety
if (/[a-z]/.test(password)) score++;
if (/[A-Z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++;
// Provide feedback
if (password.length < 8) {
feedback.push('Use at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
feedback.push('Add uppercase letters');
}
if (!/[a-z]/.test(password)) {
feedback.push('Add lowercase letters');
}
if (!/[0-9]/.test(password)) {
feedback.push('Add numbers');
}
if (!/[^A-Za-z0-9]/.test(password)) {
feedback.push('Add special characters for extra security');
}
return {
score: Math.min(score, 4),
feedback,
isStrong: score >= 3
};
}
// Usage in component
const strength = checkPasswordStrength(password);
console.log(`Password strength: ${strength.score}/4`);
console.log('Suggestions:', strength.feedback);
UX Considerations
- Clear Error Messages: Provide specific feedback for authentication failures
- Loading States: Show loading indicators during API calls
- Email Verification Reminder: Remind users to verify email if login fails
- Password Visibility Toggle: Let users toggle password visibility
- Auto-focus: Focus the appropriate input field automatically
Email Configuration
For production, configure organization-specific SMTP:
// Configure custom SMTP for your organization
await sso.organizations.setSmtp('acme-corp', {
host: 'smtp.gmail.com',
port: 587,
username: 'notifications@acme.com',
password: 'your-app-password',
from_email: 'notifications@acme.com',
from_name: 'Acme Corp'
});
This ensures:
- Emails come from your domain
- Better deliverability
- Custom branding in emails
- Compliance with email policies
Error Handling
import { SsoApiError } from '@drmhse/sso-sdk';
async function handleAuthError(error: unknown) {
if (error instanceof SsoApiError) {
switch (error.statusCode) {
case 400:
return 'Invalid request. Please check your input.';
case 401:
return 'Invalid credentials. Please try again.';
case 403:
return 'Email not verified. Please check your inbox.';
case 409:
return 'This email is already registered.';
case 429:
return 'Too many attempts. Please try again later.';
case 500:
return 'Server error. Please try again later.';
default:
return error.message;
}
}
return 'An unexpected error occurred.';
}
// Usage
try {
await sso.auth.login({ email, password });
} catch (error) {
const message = await handleAuthError(error);
showErrorToUser(message);
}
Next Steps
- MFA Management Guide - Add two-factor authentication
- Authentication Flows Guide - Learn about OAuth and device flows
- User API Reference - Detailed API documentation