How to Use Magic Link Authentication

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:

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.

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

No authentication header required. Rate limited to prevent abuse.

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:

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

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

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

Success Response — 200 OK:

{
  "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

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

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

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

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

# 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." }

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

Subscribe to The Fabrixly Blog

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe