How to Fix Magic Link Authentication Issues with Outlook SafeLinks

When implementing Magic Link authentication with Auth.js, we encountered an unexpected issue where users with Microsoft Outlook’s SafeLinks premium feature were unable to log in due to expired tokens. This post explains the root cause and presents a solution.

The Challenge

SafeLinks, a security feature in Outlook, processes authentication links before delivering them to users. While Auth.js provides a workaround for this scenario, we discovered that Outlook sends GET requests rather than the expected HEAD requests, causing the standard solution to fail.

The Solution

Our approach involves detecting requests originating from Microsoft Outlook’s SafeLinks service by identifying specific markers including IP ranges, headers, and user-agents. Here’s how to implement this in a Next.js application:

function isSafeLinksRequest(request: NextRequest): boolean {
  const userAgent = request.headers.get("user-agent") || "";
  const ip =
    request.headers.get("do-connecting-ip") ||
    request.headers.get("x-forwarded-for") ||
    "";

  const safeLinksIndicators = [
    // Microsoft Exchange Online IPs
    "13.107.6.152/31",
    "13.107.18.10/31",
    "13.107.128.0/22",
    "23.103.160.0/20",
    "40.96.0.0/13",
    "40.104.0.0/15",
    "52.96.0.0/14",
    "40.92.0.0/15",
    "40.107.0.0/16",
    "52.100.0.0/14",
    "104.47.0.0/17",
    "2a01:111:f400::/48",

    // Original domains and IPs
    "microsoft.com",
    "2a01:111:f400:",
    "40.94.",
    "52.147.",

    // Headers
    "x-ms-exchange",
    "x-forefront-antispam",
    "x-office365-filtering",

    // User agents
    "MS Email Security",
    "Office 365",
    "Microsoft Office Protocol Discovery",
  ];

  // Check all headers for any SafeLinks indicators
  const allHeaders = Object.fromEntries(request.headers);
  const hasIndicatorInHeaders = Object.entries(allHeaders).some(
    ([key, value]) =>
      safeLinksIndicators.some((indicator) =>
        (value as string)?.toLowerCase().includes(indicator.toLowerCase()),
      ),
  );

  return (
    hasIndicatorInHeaders ||
    safeLinksIndicators.some((indicator) => userAgent.includes(indicator)) ||
    safeLinksIndicators.some((indicator) => ip.includes(indicator))
  );
}

If there’s SafeLinks detected, then we send a response with 200 status so that the request will not consume our token.

export async function GET(request: NextRequest) {
  const isSafeLinks = isSafeLinksRequest(request);

  try {
    // Handle SafeLinks
    if (isSafeLinks) {
      return new Response(null, { status: 200 });
    }
    return handlers.GET(request);
  } catch (error) {
    console.error("Auth Error:", error);
    return new Response(JSON.stringify({ error: "Authentication failed" }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }
}

When implementing SafeLinks detection, keep in mind that Microsoft’s infrastructure is dynamic. The effectiveness of IP-based detection varies as users can access from different locations, and Microsoft updates their URLs and IP ranges monthly.

To maintain reliable detection, I would suggest to do the followings:

  • Regularly pull updated information from Microsoft’s endpoint service
  • Combine multiple detection methods (headers, patterns, and IPs)
  • Consider setting up automated updates for your indicators

Despite being a common issue, testing can be challenging without access to an Outlook account with SafeLinks enabled. Consider this when planning your implementation and testing strategy.

That’s it! While network-related issues can be tricky, this approach should help handle SafeLinks authentication.

NOTES:

Unlike what’s mentioned in the Auth.js documentation, SafeLinks sends GET requests rather than HEAD requests.

Testing SafeLinks integration requires specific conditions:

  • Must be tested in a production environment
  • Local development (localhost) and tunneling services (like ngrok) won’t trigger SafeLinks
  • A Microsoft 365 business account with SafeLinks enabled is required — regular Outlook accounts won’t work as SafeLinks is a premium security feature

This is my workaround at the moment, if you have any ideas or suggestion, welcome to leave a comment below.

Published by

Leave a comment