External Service Integration Guide

# 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