next.jsnestjspassport.jsapple-sign-insign-in-with-apple

Apple Sign-In Fails with Mysterious 404 Error on Non-Existent /appleauth/auth/federate Endpoint


I'm implementing Apple Sign-In in my Next.js application with a NestJS backend. After the user authenticates with Apple, instead of redirecting to my configured callback URL, the browser makes a POST request to a mysterious endpoint /appleauth/auth/federate that doesn't exist in my codebase, resulting in a 404 error.

Tech Stack

Apple Developer Configuration

Service ID: (configured correctly in Apple Developer Console)

Return URL (only one configured):

https://api.example.com/api/v1/auth/apple/callback

Domains verified in Apple Developer Console:

Backend Configuration

NestJS Controller (auth.controller.ts):

typescript

@Public()
@Get('apple')
@UseGuards(AuthGuard('apple'))
async appleAuth() {
  // Initiates Apple OAuth flow
}

@Public()
@Post('apple/callback')  // Changed from @Get to @Post for form_post
@UseGuards(AuthGuard('apple'))
async appleAuthCallback(@Req() req: any, @Res() res: any) {
  const result = await this.authService.socialLogin(req.user, ipAddress, userAgent);
  // Returns HTML with tokens that uses postMessage to send to opener window
}

Environment Variables:

typescript

APPLE_CLIENT_ID=<service_id>
APPLE_TEAM_ID=<team_id>
APPLE_KEY_ID=<key_id>
APPLE_PRIVATE_KEY_PATH=./certs/AuthKey_XXX.p8
APPLE_CALLBACK_URL=https://api.example.com/api/v1/auth/apple/callback
FRONTEND_URL=https://myapp.example.com

The passport-apple strategy uses response_mode: 'form_post', so Apple POSTs the authorization response to the callback URL.

Frontend Implementation

Next.js API Route (/src/app/api/auth/apple/route.js):

javascript

export async function GET(request) {
  const backendUrl = new URL(`${API_URL}/auth/apple`);
  
  const response = await fetch(backendUrl.toString(), {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  });

  const responseText = await response.text();
  return new NextResponse(responseText, {
    status: response.status,
    headers: { "Content-Type": contentType || "text/html" },
  });
}

Frontend Auth Handler:

javascript

export const handleAppleLogin = (router, setApiError) => {
  const frontendUrl = window?.location?.origin;
  // Opens popup to /api/auth/apple
  window.open(
    `${frontendUrl}/api/auth/apple`, 
    "appleLogin", 
    "width=500,height=600"
  );
};

The Problem

Expected Flow:

  1. User clicks "Login with Apple"
  2. Frontend opens popup → https://myapp.example.com/api/auth/apple
  3. Frontend proxies to → https://api.example.com/api/v1/auth/apple
  4. Backend redirects to Apple's authentication page
  5. User authenticates with Apple ID
  6. Apple POSTs back to → https://api.example.com/api/v1/auth/apple/callback
  7. Backend processes and returns success HTML

Actual Behavior:

After step 5 (user authentication with Apple), instead of Apple redirecting to my callback URL, the browser makes this unexpected request:

POST https://myapp.example.com/appleauth/auth/federate?isRememberMeEnabled=false
Status: 404 Not Found

Request Payload:

json

{
  "accountName": "user@example.com",
  "rememberMe": false
}

Network Tab Analysis

From Chrome DevTools, the call stack shows:

send @ app.js:234
ajax @ app.js:234
(anonymous) @ app.js:10
Ee.isFederated @ app.js:666
_callAuthFederate @ app.js:666

The Ee.isFederated and _callAuthFederate functions appear to be minified library code, but I cannot identify which library.

What I've Verified

✅ The /appleauth/auth/federate endpoint does not exist anywhere in my codebase:

bash

grep -r "appleauth" src/  # No results
grep -r "federate" src/   # No results

✅ Apple Developer Console shows only ONE Return URL configured (verified multiple times)

✅ Changed callback route from @Get to @Post to handle form_post response mode

✅ Rebuilt frontend completely multiple times:

bash

rm -rf .next
npm run build

✅ Tested in:

✅ No service workers registered in the application

✅ No external <script> tags or CDN libraries loaded

package.json contains no AWS Amplify, Auth0, Cognito, or similar federated auth libraries

✅ Checked layout.js and all root-level files - no external scripts

Additional Context

Questions

  1. Where could this /appleauth/auth/federate endpoint be coming from?

  2. Why is the browser making this POST request instead of following Apple's redirect to my configured callback URL?

  3. Could this be related to the response_mode: 'form_post' in the Apple Passport strategy?

  4. Is there something in the Apple Developer Primary App ID configuration that could trigger this behavior?

  5. Could this be a Next.js build artifact or some hidden dependency?

The mysterious call stack references (Ee.isFederated, _callAuthFederate) suggest some library is intercepting the Apple authentication flow, but I cannot identify what library or where it's being loaded from. The minified function names suggest federated authentication, but I have no such libraries in my dependencies.

Has anyone encountered similar issues with Apple Sign-In where an unexpected endpoint is being called?


Solution

  • Finally fixed it. The issue had nothing to do with backend though. It was with how it was handled in frontend. Took a long time to find the actual cause!

    The Apple Sign-In was failing with an unexpected /appleauth/auth/federate error because the frontend API route (/api/auth/apple) was using fetch() to call the backend instead of redirecting the browser directly.

    What was happening:

    1. User clicks "Sign in with Apple"

    2. Frontend opens popup to https://xxx.xxx.xxx/api/auth/apple

    3. Next.js route fetched the backend URL (server-side)

    4. Backend returned a 302 redirect to Apple

    5. The server-side fetch tried to handle the OAuth flow, which broke the redirect chain

    The Fix:
    Changed /src/app/api/auth/apple/route.js to use NextResponse.redirect() instead of fetch(). This properly redirects the browser to the backend, allowing the OAuth flow to work correctly.

    Changed from:

    const response = await fetch(backendUrl.toString(), {...});
    return new NextResponse(responseText, {...});
    

    Changed to:

    return NextResponse.redirect(backendUrl, 302);
    

    And its now fixed and working fine! Hope this helps someone who faces similar issue.