243 lines
7.3 KiB
Markdown
243 lines
7.3 KiB
Markdown
# Stytch Configuration Guide
|
|
|
|
This document explains how to configure Stytch B2B to prevent unknown users from receiving magic link emails and creating accounts.
|
|
|
|
## Overview
|
|
|
|
We've implemented a custom solution to address two critical security requirements:
|
|
|
|
1. **Prevent emails being sent to non-existent users**
|
|
2. **Block unknown email addresses from creating accounts**
|
|
|
|
## How It Works
|
|
|
|
### Custom Backend Validation
|
|
|
|
Instead of using Stytch's UI component directly (which always sends emails), we've created a custom flow:
|
|
|
|
1. **Frontend**: Custom email form in `app/auth/page.tsx`
|
|
2. **Backend API**: `/api/auth/magic-link` validates membership before sending
|
|
3. **Stytch API**: Only called if user is an existing member
|
|
|
|
### Security Features
|
|
|
|
✅ **Email validation**: Backend checks if email exists in any organization before sending magic link
|
|
✅ **No user enumeration**: Returns same message for existing and non-existing users
|
|
✅ **JIT provisioning blocked**: Organization settings prevent auto-creation of new members
|
|
✅ **Discovery flow restricted**: Users can only join organizations they're invited to
|
|
|
|
## Required Stytch Dashboard Configuration
|
|
|
|
### Step 1: Disable Self-Service Organization Creation
|
|
|
|
1. Log into your [Stytch Dashboard](https://stytch.com/dashboard)
|
|
2. Navigate to **Frontend SDK** settings
|
|
3. Find **"Create Organizations"** toggle under **Enabled methods**
|
|
4. **Disable** this toggle
|
|
|
|
**Result**: Users cannot create new organizations via the discovery flow
|
|
|
|
### Step 2: Configure Organization Settings (Per Organization)
|
|
|
|
For each organization in your Stytch project:
|
|
|
|
1. Navigate to **Organizations** in the dashboard
|
|
2. Select your organization
|
|
3. Go to **Settings** → **Authentication**
|
|
4. Configure the following:
|
|
|
|
```json
|
|
{
|
|
"email_jit_provisioning": "NOT_ALLOWED",
|
|
"email_invites": "RESTRICTED",
|
|
"email_allowed_domains": ["your-company.com"] // Optional: restrict by domain
|
|
}
|
|
```
|
|
|
|
**What each setting does:**
|
|
|
|
- `email_jit_provisioning: "NOT_ALLOWED"` - Prevents new members from being auto-created via magic link
|
|
- `email_invites: "RESTRICTED"` - Requires explicit invitation to join
|
|
- `email_allowed_domains` - (Optional) Only allows specific email domains
|
|
|
|
### Step 2a: (Optional) Configure Allowed Organization IDs
|
|
|
|
The backend validates emails by searching members across a specific list of organizations. To avoid an extra API call during login, you can provide a comma-separated allowlist:
|
|
|
|
```bash
|
|
STYTCH_ALLOWED_ORGANIZATION_IDS=org-test-123,org-test-456
|
|
```
|
|
|
|
If this variable is not set, we automatically fetch all organizations in the workspace and cache the IDs in memory.
|
|
|
|
### Step 3: Verify API Permissions
|
|
|
|
Ensure your Stytch API credentials have permission to:
|
|
- Search members (`organizations.members.search`)
|
|
- Send magic links (`magicLinks.email.discovery.send`)
|
|
|
|
## Testing the Implementation
|
|
|
|
### Test Case 1: Unknown Email
|
|
|
|
1. Enter an email that doesn't exist in any organization
|
|
2. Click "Send magic link"
|
|
3. **Expected**: Message says "If an account exists with that email, a magic link has been sent."
|
|
4. **Verify**: No email is actually sent
|
|
5. **Check backend logs**: Should see "No members found" for the email
|
|
|
|
### Test Case 2: Existing Member
|
|
|
|
1. Enter an email of an existing organization member
|
|
2. Click "Send magic link"
|
|
3. **Expected**: Same message as above
|
|
4. **Verify**: Email IS sent with magic link
|
|
5. **Check inbox**: Magic link email received
|
|
|
|
### Test Case 3: Magic Link Authentication
|
|
|
|
1. Click the magic link from Test Case 2
|
|
2. **Expected**: User is authenticated and redirected to dashboard
|
|
3. **Verify**: Session is created with correct organization
|
|
|
|
### Test Case 4: Unknown User Clicks Link (if they somehow got one)
|
|
|
|
1. If someone gets a magic link URL (e.g., from a legitimate user)
|
|
2. **Expected**: Authentication fails with error
|
|
3. **Verify**: No session is created, user cannot access dashboard
|
|
|
|
## API Endpoint Documentation
|
|
|
|
### POST `/api/auth/magic-link`
|
|
|
|
Validates email and sends magic link to existing members only.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"email": "user@company.com"
|
|
}
|
|
```
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "If an account exists with that email, a magic link has been sent."
|
|
}
|
|
```
|
|
|
|
**Response (Error):**
|
|
```json
|
|
{
|
|
"error": "Unable to process request. Please try again later."
|
|
}
|
|
```
|
|
|
|
**Note**: Response is the same whether user exists or not (prevents enumeration)
|
|
|
|
## How to Add New Members
|
|
|
|
Since self-service signup is disabled, use one of these methods:
|
|
|
|
### Method 1: Invite via Stytch Dashboard
|
|
|
|
1. Go to **Organizations** → Select org → **Members**
|
|
2. Click **Invite Member**
|
|
3. Enter email and assign roles
|
|
4. User receives invitation email
|
|
|
|
### Method 2: Programmatic Invite
|
|
|
|
```typescript
|
|
import { getStytchB2BClient } from "@/lib/auth/stytch/server";
|
|
|
|
const client = getStytchB2BClient();
|
|
|
|
await client.magicLinks.email.invite.send({
|
|
organization_id: "org-test-...",
|
|
email_address: "newuser@company.com",
|
|
invited_by_member_id: "member-test-...",
|
|
});
|
|
```
|
|
|
|
### Method 3: Create Member via API
|
|
|
|
```typescript
|
|
await client.organizations.members.create({
|
|
organization_id: "org-test-...",
|
|
email_address: "newuser@company.com",
|
|
name: "New User",
|
|
roles: ["member"],
|
|
});
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Issue: Existing users not receiving emails
|
|
|
|
**Check:**
|
|
1. Email is verified in Stytch
|
|
2. Member status is "active" (not "pending" or "invited")
|
|
3. Backend logs for member search results
|
|
4. Stytch API credentials are correct
|
|
|
|
### Issue: Unknown users still getting emails
|
|
|
|
**Check:**
|
|
1. Using `/api/auth/magic-link` endpoint (not direct Stytch SDK call)
|
|
2. Backend search is working correctly
|
|
3. No caching issues in API route
|
|
|
|
### Issue: Users can't create organizations
|
|
|
|
**This is expected!** Self-service organization creation is disabled.
|
|
|
|
**Solution:** Create organizations manually via:
|
|
- Stytch Dashboard
|
|
- Stytch API programmatically
|
|
|
|
## Environment Variables
|
|
|
|
Required in `.env.local`:
|
|
|
|
```bash
|
|
# Stytch B2B Authentication
|
|
STYTCH_PROJECT_ID=project-test-...
|
|
STYTCH_SECRET=secret-test-...
|
|
NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN=public-token-test-...
|
|
|
|
# Session configuration
|
|
NEXT_PUBLIC_STYTCH_SESSION_DURATION_MINUTES=43200 # 30 days
|
|
|
|
# App URLs
|
|
NEXT_PUBLIC_APP_BASE_URL=http://localhost:3000
|
|
NEXT_PUBLIC_STYTCH_REDIRECT_PATH=/authenticate
|
|
```
|
|
|
|
## Additional Security Recommendations
|
|
|
|
1. **Enable MFA**: Require multi-factor authentication for sensitive organizations
|
|
2. **Monitor failed attempts**: Track authentication failures in your logs
|
|
3. **Rate limiting**: Add rate limiting to `/api/auth/magic-link` endpoint
|
|
4. **Email verification**: Ensure all members have verified emails
|
|
5. **Session duration**: Keep session duration appropriate for your security requirements
|
|
|
|
## Migration from Discovery Flow
|
|
|
|
If you were previously using the Discovery flow with self-service signup:
|
|
|
|
1. **Export existing members**: Get list of all current members
|
|
2. **Notify users**: Inform them that signup is now invite-only
|
|
3. **Update documentation**: Update user docs about the new auth flow
|
|
4. **Monitor support requests**: Users may try to sign up and fail
|
|
|
|
## Questions?
|
|
|
|
For Stytch-specific configuration questions:
|
|
- [Stytch B2B Documentation](https://stytch.com/docs/b2b)
|
|
- [Stytch Support](https://stytch.com/contact)
|
|
|
|
For implementation questions related to this codebase:
|
|
- Review `app/api/auth/magic-link/route.ts` for backend logic
|
|
- Review `app/auth/page.tsx` for frontend implementation
|