# External Service Integration Guide
**⚠️ IMPORTANT: This guide covers API-based integration. For the recommended redirect-based SSO flow, see [SSO_INTEGRATION_GUIDE.md](./SSO_INTEGRATION_GUIDE.md)**
## Integration Methods
Ryan Identity supports two integration methods:
### 1. **Redirect-Based SSO** (RECOMMENDED) ⭐
Users authenticate directly on Ryan Identity's website, then are redirected back to your service with an authorization code.
**✅ Recommended for:**
- Web applications
- Services where users interact with a browser
- Best security and user experience
**See:** [SSO_INTEGRATION_GUIDE.md](./SSO_INTEGRATION_GUIDE.md)
### 2. **API-Based Integration** (This Guide)
Your service initiates password resets via API calls, but users still reset passwords on Ryan Identity.
**✅ Recommended for:**
- Programmatic integrations
- Mobile apps that manage their own auth UI
- Services that need API-driven password resets
---
This guide explains how external services (e.g., RyanInvoice, RyanCRM) can integrate with Ryan Identity using the API-based approach for password management.
## Table of Contents
1. [Overview](#overview)
2. [API Authentication](#api-authentication)
3. [Password Reset Integration](#password-reset-integration)
4. [Complete Integration Example](#complete-integration-example)
5. [API Reference](#api-reference)
6. [Security Best Practices](#security-best-practices)
7. [Testing](#testing)
## Overview
Ryan Identity is a centralized SSO and user management system for the SaaS Ryan ecosystem.
**For most use cases, you should use the [redirect-based SSO flow](./SSO_INTEGRATION_GUIDE.md).** This API-based guide is for special cases where you need programmatic password reset initiation.
### Key Benefits
- **Single Sign-On (SSO)**: Users log in once and access all SaaS Ryan services
- **Centralized User Management**: Admins manage users in one place
- **Consistent Security**: Password policies, MFA, and security features are managed centrally
- **Password Reset Handling**: Your service can delegate password resets to Ryan Identity
## API Authentication
External services authenticate with Ryan Identity using API keys.
### Getting an API Key
API keys are created by organization administrators through the Ryan Identity dashboard or via the admin API.
**API Key Format:**
```
ryanid_{keyId}_{secret}
```
Example:
```
ryanid_550e8400-e29b-41d4-a716-446655440000_a1b2c3d4e5f6...
```
### API Key Scopes
API keys have specific scopes that control what actions they can perform:
- `user:read` - Read user information
- `user:write` - Create and update users
- `user:password-reset` - Initiate password resets for users
- `*` - All permissions (use with caution)
### Using API Keys
Include your API key in the `X-API-Key` header:
```bash
curl -X POST https://identity.ryan.com/api/external/initiate-password-reset \
-H "X-API-Key: ryanid_your_key_here" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
```
## Password Reset Integration
When a user forgets their password while using your service, you should delegate the password reset process to Ryan Identity.
**Important:** The `redirectUrl` parameter should point to **your application**, not Ryan Identity. After the user successfully resets their password on Ryan Identity's reset page, they will be redirected back to your application at the URL you specify.
### Why Delegate Password Resets?
1. **Users don't know they're using Ryan Identity**: Your UI should be seamless
2. **Centralized password management**: Passwords are managed in one place
3. **Security**: You don't handle password reset tokens or temporary credentials
4. **Consistency**: All SaaS Ryan services use the same flow
### Integration Flow
```
┌─────────────────┐
│ Your Service │
│ (e.g., Invoice) │
└────────┬────────┘
│ 1. User clicks "Forgot Password"
│
▼
┌─────────────────┐
│ Your Frontend │
│ "Reset" Button │
└────────┬────────┘
│ 2. Call your backend API
│
▼
┌─────────────────┐
│ Your Backend │
│ (API Call) │
└────────┬────────┘
│ 3. POST /api/external/initiate-password-reset
│ with X-API-Key header
│
▼
┌─────────────────┐
│ Ryan Identity │
│ (API) │
└────────┬────────┘
│ 4. Generate reset token
│ 5. Send email to user
│
▼
┌─────────────────┐
│ User Email │
│ Reset Link │
└────────┬────────┘
│ 6. User clicks link
│
▼
┌─────────────────┐
│ Ryan Identity │
│ Reset Page │
└────────┬────────┘
│ 7. User enters new password
│ 8. Password updated
│
▼
┌─────────────────┐
│ Your Service │
│ (redirected) │
└─────────────────┘
```
### Implementation Steps
#### Step 1: Add "Forgot Password" Button to Your UI
```tsx
// React example
export function LoginForm() {
const handleForgotPassword = async () => {
const email = emailInput.value;
// Call your backend
const response = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
if (response.ok) {
alert('Password reset email sent! Check your inbox.');
}
};
return (
<form>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button type="submit">Sign In</button>
<button type="button" onClick={handleForgotPassword}>
Forgot Password?
</button>
</form>
);
}
```
#### Step 2: Implement Backend Endpoint
```javascript
// Node.js/Express example
const RYAN_IDENTITY_API_KEY = process.env.RYAN_IDENTITY_API_KEY;
const RYAN_IDENTITY_URL = process.env.RYAN_IDENTITY_URL || 'https://identity.ryan.com';
const APP_URL = process.env.APP_URL || 'https://your-app.com'; // YOUR application URL
app.post('/api/auth/forgot-password', async (req, res) => {
const { email } = req.body;
// Optional: Include redirect URL so user returns to your service after reset
const redirectUrl = `${APP_URL}/auth/reset-complete`;
try {
const response = await fetch(
`${RYAN_IDENTITY_URL}/api/external/initiate-password-reset`,
{
method: 'POST',
headers: {
'X-API-Key': RYAN_IDENTITY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
redirectUrl: redirectUrl // Optional
})
}
);
const data = await response.json();
if (response.ok) {
res.json({
success: true,
message: 'Password reset email sent. Please check your inbox.'
});
} else {
res.status(response.status).json({
success: false,
error: data.error
});
}
} catch (error) {
console.error('Failed to initiate password reset:', error);
res.status(500).json({
success: false,
error: 'Failed to process password reset request'
});
}
});
```
#### Step 3: Handle Redirect After Password Reset (Optional)
If you provide a `redirectUrl`, users will be redirected to your service after successfully resetting their password:
```javascript
// Handle the redirect at /auth/reset-complete
app.get('/auth/reset-complete', (req, res) => {
// User has successfully reset their password
// Show a success message and prompt them to log in
res.render('password-reset-success', {
message: 'Your password has been reset successfully. Please log in with your new password.'
});
});
```
## Complete Integration Example
Here's a complete example using Next.js (App Router):
### 1. Environment Variables (.env.local)
```bash
# Ryan Identity service configuration
RYAN_IDENTITY_API_KEY=ryanid_your_key_here
RYAN_IDENTITY_URL=https://identity.ryan.com # Production: port 443 (HTTPS)
# For development: RYAN_IDENTITY_URL=http://localhost:30000
# Your application URL (where users will be redirected after password reset)
NEXT_PUBLIC_APP_URL=https://your-app.com # Production: port 443 (HTTPS)
# For development: NEXT_PUBLIC_APP_URL=http://localhost:8080
```
### 2. Backend API Route (app/api/auth/forgot-password/route.ts)
```typescript
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const { email } = await request.json();
if (!email) {
return NextResponse.json(
{ error: 'Email is required' },
{ status: 400 }
);
}
const response = await fetch(
`${process.env.RYAN_IDENTITY_URL}/api/external/initiate-password-reset`,
{
method: 'POST',
headers: {
'X-API-Key': process.env.RYAN_IDENTITY_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/auth/reset-complete`
})
}
);
const data = await response.json();
if (response.ok) {
return NextResponse.json({
success: true,
message: data.message
});
} else {
return NextResponse.json(
{ error: data.error },
{ status: response.status }
);
}
} catch (error) {
console.error('Password reset error:', error);
return NextResponse.json(
{ error: 'Failed to process request' },
{ status: 500 }
);
}
}
```
### 3. Frontend Component (components/ForgotPasswordDialog.tsx)
```typescript
'use client';
import { useState } from 'react';
export function ForgotPasswordDialog({ onClose }: { onClose: () => void }) {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage('');
try {
const response = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const data = await response.json();
if (response.ok) {
setMessage(data.message);
setTimeout(() => onClose(), 3000);
} else {
setMessage(data.error || 'Failed to send reset email');
}
} catch (error) {
setMessage('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="dialog">
<h2>Reset Your Password</h2>
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send Reset Link'}
</button>
</form>
{message && <p className="message">{message}</p>}
</div>
);
}
```
### 4. Success Page (app/auth/reset-complete/page.tsx)
```typescript
export default function ResetCompletePage() {
return (
<div className="container">
<h1>Password Reset Successful</h1>
<p>
Your password has been reset successfully.
You can now log in with your new password.
</p>
<a href="/login" className="button">
Go to Login
</a>
</div>
);
}
```
## API Reference
### POST /api/external/initiate-password-reset
Initiates a password reset for a user. Sends an email with a reset link.
**Authentication:** Required (API Key)
**Headers:**
```
X-API-Key: ryanid_{keyId}_{secret}
Content-Type: application/json
```
**Request Body:**
```json
{
"email": "user@example.com",
"redirectUrl": "https://your-app.com/auth/reset-complete"
}
```
**Parameters:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| email | string | Yes | User's email address |
| redirectUrl | string | No | URL to redirect user after successful password reset. **This should be a URL on YOUR application domain, not Ryan Identity.** In production, use HTTPS (port 443). |
**Response (Success):**
```json
{
"success": true,
"message": "If a user with that email exists, a password reset email has been sent."
}
```
**Response (Error):**
```json
{
"success": false,
"error": "Invalid API key"
}
```
**HTTP Status Codes:**
| Code | Description |
|------|-------------|
| 200 | Success (always returned, even if user doesn't exist) |
| 400 | Invalid request (missing email, invalid email format, invalid redirectUrl) |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have required scope |
| 500 | Server error |
**Notes:**
- This endpoint always returns 200 (success) when the email is valid, even if no user exists with that email. This prevents email enumeration attacks.
- The user must belong to the same organization as the API key.
- The reset token expires in 1 hour.
- If a `redirectUrl` is provided, the user will be redirected to that URL after successfully resetting their password.
## Security Best Practices
### 1. Protect Your API Key
- **Never commit API keys to version control**
- Store API keys in environment variables
- Use different API keys for different environments (dev, staging, production)
- Rotate API keys regularly
- If a key is compromised, revoke it immediately and generate a new one
### 2. Use HTTPS
- Always use HTTPS when making API requests to Ryan Identity
- Validate SSL certificates
### 3. Validate Redirect URLs
- If you provide a `redirectUrl`, ensure it's a valid URL on your domain
- Don't accept arbitrary redirect URLs from user input (prevents phishing)
### 4. Rate Limiting
- Implement rate limiting on your "forgot password" endpoint
- Prevent abuse by limiting requests per IP address or email
### 5. User Communication
- Tell users to check their email for the reset link
- Don't reveal whether the email exists in your system
- Consider showing a message like: "If an account exists with that email, we've sent reset instructions"
### 6. Monitor API Usage
- Log all password reset requests
- Monitor for unusual patterns or high volumes
- Set up alerts for suspicious activity
## Testing
### Environment URLs
**Development:**
- Ryan Identity: `http://localhost:30000`
- Your Application: `http://localhost:YOUR_PORT` (e.g., `http://localhost:8080`)
**Production:**
- Ryan Identity: `https://identity.ryan.com` (port 443, SSL/TLS)
- Your Application: `https://your-domain.com` (port 443, SSL/TLS recommended)
**Important Notes:**
- Production deployments should use HTTPS (port 443) for security
- The `redirectUrl` parameter should point to **your application**, not Ryan Identity
- In production, ensure your domain has valid SSL/TLS certificates
### Testing in Development
Use the test API key provided for your organization:
```bash
# Test initiating a password reset
# Note: redirectUrl should point to YOUR application, not Ryan Identity
curl -X POST http://localhost:30000/api/external/initiate-password-reset \
-H "X-API-Key: ryanid_test_key_here" \
-H "Content-Type: application/json" \
-d '{
"email": "testuser@example.com",
"redirectUrl": "http://localhost:8080/auth/reset-complete"
}'
```
### Testing the Complete Flow
1. **Initiate Password Reset:**
```bash
curl -X POST http://localhost:30000/api/external/initiate-password-reset \
-H "X-API-Key: your_test_key" \
-H "Content-Type: application/json" \
-d '{"email": "testuser@example.com"}'
```
2. **Check Email:**
- In development, emails are logged to console or sent to MailHog (if configured)
- Copy the reset link from the email
3. **Visit Reset Link:**
- Open the reset link in your browser
- Enter a new password
- Submit the form
4. **Verify Redirect:**
- If you provided a `redirectUrl`, verify you're redirected to your service
- Check that the user can log in with the new password
### Example Test Script
```javascript
// test-password-reset.js
const RYAN_IDENTITY_API_KEY = 'ryanid_your_test_key';
const RYAN_IDENTITY_URL = 'http://localhost:30000'; // Ryan Identity service
const YOUR_APP_URL = 'http://localhost:8080'; // Your application
async function testPasswordReset() {
console.log('🔐 Testing password reset flow...\n');
// Step 1: Initiate password reset
console.log('1️⃣ Initiating password reset...');
const response = await fetch(
`${RYAN_IDENTITY_URL}/api/external/initiate-password-reset`,
{
method: 'POST',
headers: {
'X-API-Key': RYAN_IDENTITY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'testuser@example.com',
redirectUrl: `${YOUR_APP_URL}/auth/reset-complete` // Redirect to YOUR app
})
}
);
const data = await response.json();
console.log('Response:', JSON.stringify(data, null, 2));
if (response.ok) {
console.log('✅ Password reset initiated successfully!');
console.log('📧 Check the user\'s email for the reset link.');
} else {
console.log('❌ Password reset failed:', data.error);
}
}
testPasswordReset();
```
Run it:
```bash
node test-password-reset.js
```
## Troubleshooting
### Common Issues
**Issue:** "Invalid API key"
- **Solution:** Verify your API key is correct and hasn't expired
**Issue:** "API key does not have user:password-reset scope"
- **Solution:** Contact your Ryan Identity admin to grant the required scope
**Issue:** "Failed to send password reset email"
- **Solution:** Check Ryan Identity email configuration. In development, ensure SMTP settings are configured or MailHog is running.
**Issue:** User not receiving emails
- **Solution:**
- Check spam folder
- Verify email address is correct
- Check Ryan Identity logs for email sending errors
- Verify SMTP configuration
**Issue:** Reset link expired
- **Solution:** Reset tokens expire in 1 hour. Request a new password reset.
## Support
For questions or issues:
1. Check this documentation
2. Review Ryan Identity API logs
3. Contact the Ryan Identity team
4. Open an issue in the Ryan Identity repository
## Changelog
- **2025-11-02**: Initial documentation created