NextAuth LinkedIn OAuth returns blank page after login – no API calls in LinkedIn Analytics

Hope you are doing well and staying safe!

I am trying to create a LinkedIn based authentication using typescript, but since long I am failing. My code is successfully able to give me login option using LinkedIn but when I login, it just gives me blank page. And, I have no idea why. :frowning:

Also, I see no API getting called in LinkedIn Analytics.

Below is the typescript for authentication for your reference-

// pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth";
import type { NextAuthOptions } from "next-auth";

const authOptions: NextAuthOptions = {
  providers: [
    {
      id: "linkedin",
      name: "LinkedIn",
      type: "oauth",
      version: "2.0",
      idToken: true,
      authorization: {
        url: "https://www.linkedin.com/oauth/v2/authorization",
        params: {
          scope: "openid profile email",
          response_type: "code",
        },
      },
      token: {
        url: "https://www.linkedin.com/oauth/v2/accessToken",
        async request(context) {
          const { params } = context;

          // ✅ Declare constants outside the URLSearchParams block
          const hardcodedClientId = "44ddgfdsgdsgsdfdssdfu";
          const hardcodedClientSecret = "WPL_AP1.fefefsdfsaYdada5.qyKbDw==";

          const body = new URLSearchParams({
            grant_type: "authorization_code",
            code: params.code!,
            // redirect_uri: "http://localhost:3000/api/auth/callback/linkedin",
            redirect_uri: "https://28fa-2401-4900-7085-a56e-8de6-d45c-2e3f-e535.ngrok-free.app/api/auth/callback/linkedin",
        client_id: hardcodedClientId,
            client_secret: hardcodedClientSecret,
            code_verifier: params.code_verifier!,
          });

          const res = await fetch("https://www.linkedin.com/oauth/v2/accessToken", {
            method: "POST",
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
            },
            body: body.toString(),
          });

          if (!res.ok) {
            const errorText = await res.text();
            console.error("❌ LinkedIn Token Exchange Error (Raw):", errorText);
            console.error("🔐 Sent Client ID:", hardcodedClientId);
            console.error("🔐 Sent Client Secret:", hardcodedClientSecret ? "✅ present" : "❌ missing");
            throw new Error("Token exchange failed");
          }

          return res.json();
        },
      },
      userinfo: {
        url: "https://api.linkedin.com/v2/userinfo",
      },
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name || `${profile.given_name} ${profile.family_name}`,
          email: profile.email,
          image: profile.picture,
        };
      },
      clientId: "77thyyt4uur7nu",
      clientSecret: "WPL_AP1.fefefsdfsaYdada5.qyKbDw==",
      checks: ["pkce", "state"],
    },
  ],
  secret: "fHXNb4aKPzlDDugJBzxzXZ0781Z/tdsGZtxxxsderUo=",
  debug: true,
};

export default NextAuth(authOptions);

Below is my LinkedIn developer configuration-

Generate a new Client Secret
OAuth 2.0 settings
Token time to live duration

Access token:
2 months
(5184000 seconds)

Authorized redirect URLs for your app

https://28fa-2401-4900-7085-a56e-8de6-d45c-2e3f-e535.ngrok-free.app/api/auth/callback/linkedin
OAuth 2.0 scopes
Scopes define what your app can do on a user's behalf.
The OAuth consent screen will display descriptions to end users as they are seen below. Some variation may occur if your app has a custom OAuth experience.
openid
Use your name and photo
profile
Use your name and photo
w_member_social
Create, modify, and delete posts, comments, and reactions on your behalf
email
Use the primary email address associated with your LinkedIn account

AND PRODUCTS-

Share on LinkedIn
Default Tier
Amplify your content by sharing it on LinkedIn
View docs
Endpoints

Sign In with LinkedIn using OpenID Connect
Standard Tier
Using the OpenID Connect standard
View docs
Endpoints

Your LinkedIn OAuth setup is almost correct, but the blank page and no API calls usually happen because:

  1. Incorrect userinfo.url endpoint — LinkedIn doesn’t have https://api.linkedin.com/v2/userinfo. You should use the LinkedIn API endpoints for user data:
  • For basic profile: https://api.linkedin.com/v2/me
  • For email: https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))
  1. Missing authorization header on userinfo request — You must include the access token in the header Authorization: Bearer <token> when fetching user info.
  2. Profile parsing logic doesn’t match LinkedIn’s response shape — LinkedIn’s response doesn’t have sub, name, given_name, or family_name fields like Google. You need to map from LinkedIn’s actual fields like id, localizedFirstName, localizedLastName.
  3. code_verifier may be undefined — If you’re not using PKCE properly, sending code_verifier can break the token request.

What to fix:

  • Change your userinfo config to a custom userInfo function instead of just URL:
userinfo: {
  async request({ tokens }) {
    const resProfile = await fetch("https://api.linkedin.com/v2/me", {
      headers: {
        Authorization: `Bearer ${tokens.access_token}`,
      },
    });
    const profile = await resProfile.json();

    const resEmail = await fetch("https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))", {
      headers: {
        Authorization: `Bearer ${tokens.access_token}`,
      },
    });
    const emailData = await resEmail.json();

    return {
      ...profile,
      email: emailData.elements[0]["handle~"].emailAddress,
    };
  },
},
profile(profile) {
  return {
    id: profile.id,
    name: `${profile.localizedFirstName} ${profile.localizedLastName}`,
    email: profile.email,
    image: null, // LinkedIn requires separate call for picture if needed
  };
},
  • Remove code_verifier from token request unless you implement PKCE fully.
  • Ensure your redirect_uri matches exactly the one configured in LinkedIn developer portal.

Summary:

  • LinkedIn userinfo URL is wrong, replace with API calls that include auth header.
  • Parse LinkedIn user data according to their API shape.
  • Add Authorization header on userinfo fetch.
  • Remove code_verifier if you’re not using PKCE correctly.
  • Make sure redirect URI is exact match.

Fix these and your login should work without blank page or missing API calls.