> ## Content Index
> Fetch the complete content index at: https://blog.fabrixly.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Use Magic Link Authentication
- URL: https://blog.fabrixly.com/how-to-use-magic-link-authentication/
- Published: 2026-07-07T03:08:38.000Z
- Updated: 2026-07-10T07:22:40.000Z
- Author: Fabrixly
- Tags: Documentation

Magic links let users log in by clicking a one-time link sent to their email — no passwords or OTPs required. Fabrixly-IDS handles token generation, storage, expiry, and email delivery automatically.

---

## How It Works

1. Your app calls `POST /api/auth/magic-link` with the user's email and your `client_id`
2. Fabrixly-IDS generates a signed JWT token, saves it to the database, and sends an email with a one-time login link
3. The user clicks the link → Fabrixly-IDS verifies the token → user is authenticated
4. Fabrixly-IDS redirects the user back to your app with an OIDC authorization code

> **No bearer token or API key is required** to call the magic link endpoint. It is a public-facing endpoint — but it is protected by a **rate limiter** to prevent abuse.

---

## Configuration

### Option A: Using the Admin Console

1. Go to **Admin Console → Clients → Edit Client**
2. Under **Authentication Settings**:
  - Toggle **Magic Link Login** to `On`
  - Set **Auth Mode** to `magic-link` (or `flexible` to support multiple methods)
  - Set **Magic Link Expiry** (e.g., `15` minutes)
3. Save changes.

### Option B: Using the Management API

### Authenticating API Requests

Management API endpoints (like `/api/clients`, `/api/users`, etc.) require administrative authentication. To call them:

1. **Include the Token in the Authorization Header**: Add `-H "Authorization: Bearer <your-admin-jwt-token>"` to your API calls.

**Obtain an Admin JWT Token**:

```bash
curl -X POST https://ids.fabrixly.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@example.com",
    "password": "your-admin-password"
  }'

```

*Note: This returns a JSON response containing a `token`.*

```bash
curl -X POST https://ids.fabrixly.com/api/clients \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <your-admin-jwt-token>" \
  -d '{
    "client_name": "Magic Link App",
    "redirect_uris": ["http://localhost:3006/callback"],
    "response_types": ["code"],
    "grant_types": ["authorization_code"],
    "token_endpoint_auth_method": "client_secret_basic",
    "auth_mode": "magic-link",
    "enable_magic_link": true,
    "magic_link_expiry_minutes": 15
  }'

```

---

## API Reference

### 1\. Send Magic Link

**No authentication header required.** Rate limited to prevent abuse.

```http
POST https://ids.fabrixly.com/api/auth/magic-link
Content-Type: application/json

```

**Request Body:**

| Field      | Type   | Required   | Description                                                                                                              |
| ---------- | ------ | ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| email      | string | ✅ Yes      | The user's registered email address                                                                                      |
| client\_id | string | ✅ Yes      | Your OIDC client ID (registered in Admin Console)                                                                        |
| uid        | string | ⬜ Optional | OIDC interaction UID — required only when called mid-OIDC-flow (browser login screen). Leave empty for direct API calls. |

**Example — Direct API Call:**

```json
{
  "email": "user@example.com",
  "client_id": "my-app-client-id"
}

```

**Example — Mid-OIDC-Flow (from login screen):**

```json
{
  "email": "user@example.com",
  "client_id": "my-app-client-id",
  "uid": "abc123interactionUid"
}

```

**Success Response — `200 OK`:**

```json
{
  "message": "If an account exists with this email, a magic link has been sent."
}

```

> ⚠️ **Security note:** Fabrixly-IDS always returns this identical message whether the email exists or not — to prevent user enumeration attacks. Do not try to infer user existence from the response.

**Error Responses:**

| Status                    | Reason                                                           |
| ------------------------- | ---------------------------------------------------------------- |
| 400 Bad Request           | Email field is missing                                           |
| 400 Bad Request           | Magic link login is not enabled for this client                  |
| 403 Forbidden             | Your subscription/license plan does not include magic link login |
| 429 Too Many Requests     | Rate limit exceeded — too many attempts from this IP             |
| 500 Internal Server Error | Unexpected server error                                          |

---

### 2\. Magic Link Callback (Handled by Fabrixly-IDS)

Fabrixly-IDS handles this automatically. When the user clicks the emailed link, this endpoint is called:

```http
GET https://ids.fabrixly.com/api/auth/magic-link/callback?token=<jwt_token>

```

You do **not** need to call this yourself — it is called when the user clicks the email link. Fabrixly-IDS will:

- Verify the JWT token
- Check it has not been used or expired
- Redirect the user to your registered `redirect_uri` with an OIDC `code`

---

## Integration Examples

### Node.js — Express Backend

```javascript
const express = require('express');
const fetch = require('node-fetch'); // npm install node-fetch@2

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const IDS_URL = 'https://ids.fabrixly.com';
const CLIENT_ID = 'your-client-id'; // From Admin Console → Clients

// POST /send-magic-link
// Called when user submits their email on your login form
app.post('/send-magic-link', async (req, res) => {
  const { email } = req.body;

  if (!email) {
    return res.status(400).json({ error: 'Email is required' });
  }

  try {
    const response = await fetch(`${IDS_URL}/api/auth/magic-link`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
        // No Authorization header needed — this is a public endpoint
      },
      body: JSON.stringify({
        email,
        client_id: CLIENT_ID
        // uid: only required mid-OIDC-flow
      })
    });

    if (response.status === 429) {
      return res.status(429).json({ error: 'Too many attempts. Please wait before trying again.' });
    }

    if (response.status === 403) {
      return res.status(403).json({ error: 'Magic link login is not available on your plan.' });
    }

    if (response.status === 400) {
      const body = await response.json();
      return res.status(400).json({ error: body.error });
    }

    // Always show success to prevent user enumeration
    res.json({ message: 'If an account exists with this email, a magic link has been sent.' });

  } catch (err) {
    console.error('Magic link error:', err.message);
    res.status(500).json({ error: 'Failed to send magic link. Please try again.' });
  }
});

app.listen(3006, () => console.log('App running on http://localhost:3006'));

```

---

### Python — Requests

```python
import requests

IDS_URL = "https://ids.fabrixly.com"
CLIENT_ID = "your-client-id"  # From Admin Console → Clients

def send_magic_link(email: str) -> dict:
    """
    Send a magic link to the given email.
    No Authorization header is required.
    """
    response = requests.post(
        f"{IDS_URL}/api/auth/magic-link",
        json={
            "email": email,
            "client_id": CLIENT_ID
            # "uid": only include if called mid-OIDC-flow
        },
        headers={
            "Content-Type": "application/json"
            # No Authorization header needed
        }
    )

    if response.status_code == 429:
        raise Exception("Rate limited: too many attempts. Please wait.")
    if response.status_code == 403:
        raise Exception("Magic link not available on your subscription plan.")
    if response.status_code == 400:
        raise Exception(f"Bad request: {response.json().get('error')}")
    if response.status_code != 200:
        raise Exception(f"Unexpected error: {response.status_code}")

    return response.json()

# Usage
result = send_magic_link("user@example.com")
print(result["message"])
# → "If an account exists with this email, a magic link has been sent."

```

---

### cURL

```bash
# Send magic link — no auth header needed
curl -X POST https://ids.fabrixly.com/api/auth/magic-link \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "client_id": "your-client-id"
  }'

# Response:
# { "message": "If an account exists with this email, a magic link has been sent." }

```

---

## Magic Link Token Details

Fabrixly-IDS magic link tokens are **signed JWTs** stored in the database with the following properties:

| Property       | Value                                                            |
| -------------- | ---------------------------------------------------------------- |
| Algorithm      | HS256 (signed with server JWT secret)                            |
| Default expiry | 5 minutes (configurable per-user or per-client in Admin Console) |
| One-time use   | ✅ Token is invalidated immediately after first use               |
| Stored in DB   | ✅ Prevents replay attacks — token is deleted on verification     |
| IP tracked     | ✅ IP address recorded at creation for audit purposes             |

---

## Security Best Practices

- **Never expose your `client_id` as a secret** — it is a public identifier, not a credential
- **Always use HTTPS** — magic link tokens travel via redirect URL; plain HTTP exposes them
- **Short expiry is intentional** — 5 minutes default reduces the risk window if an email is compromised
- **Each link works only once** — Fabrixly-IDS deletes the token from the database after first use
- **Rate limiting is enforced** — your users will see a `429` response if they request too many links in a short window

---

## Troubleshooting

| Problem                       | Solution                                                                               |
| ----------------------------- | -------------------------------------------------------------------------------------- |
| Magic link email not received | Check spam/junk folder; verify SMTP is configured in Admin Console                     |
| 400 — Magic link not enabled  | Enable magic link in Admin Console → Clients → Edit → Authentication Settings          |
| 403 — Plan restriction        | Upgrade your subscription or enable the magic\_link\_enabled feature flag in your plan |
| 429 — Rate limited            | Wait a few minutes before requesting another link                                      |
| Link expired                  | Request a new magic link — expiry is 5 minutes by default                              |
| Link already used             | Each link works only once — request a new one                                          |
| Wrong client\_id              | Check your client ID in Admin Console → Clients                                        |