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
- Your app calls
POST /api/auth/magic-linkwith the user's email and yourclient_id - Fabrixly-IDS generates a signed JWT token, saves it to the database, and sends an email with a one-time login link
- The user clicks the link → Fabrixly-IDS verifies the token → user is authenticated
- 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
- Go to Admin Console → Clients → Edit Client
- Under Authentication Settings:
- Toggle Magic Link Login to
On - Set Auth Mode to
magic-link(orflexibleto support multiple methods) - Set Magic Link Expiry (e.g.,
15minutes)
- Toggle Magic Link Login to
- 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:
- 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
1. Send Magic Link
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 |
2. Magic Link Callback (Handled by Fabrixly-IDS)
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_uriwith an OIDCcode
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." }
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_idas 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
429response 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 |