next-auth icon indicating copy to clipboard operation
next-auth copied to clipboard

Instagram provider causing auth core internal lib to fail

Open emilaleksanteri opened this issue 2 years ago • 7 comments

Provider type

Instagram

Environment

System: OS: Linux 6.5 EndeavourOS CPU: (16) x64 AMD Ryzen 7 4800HS with Radeon Graphics Memory: 3.54 GB / 15.05 GB Container: Yes Shell: 5.1.16 - /bin/bash Binaries: Node: 16.20.2 - ~/.local/share/pnpm/node npm: 8.19.4 - ~/.local/share/pnpm/npm pnpm: 8.8.0 - /usr/bin/pnpm Browsers: Chromium: 117.0.5938.149

Reproduction URL

https://github.com/emilaleksanteri/igauthtest

Describe the issue

When using signin with an instagram provider, I get an error: [auth][cause]:OperationProcessingError: "response" body "token_type" property must be a non-empty string at processGenericAccessTokenResponse (file:///home/emil/work/storefront/node_modules/.pnpm/[email protected]/node_modules/oauth4webapi/build/index.js:895:15) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async Module.processAuthorizationCodeOAuth2Response (file:///home/emil/work/storefront/node_modules/.pnpm``/[email protected]/node_modules/oauth4webapi/build/index.js:1054:20) at async handleOAuth (file:///home/emil/work/storefront/node_modules/.pnpm/@[email protected]/node_modules/@auth/core/lib/oauth/callback.js:84:18) at async Module.callback (file:///home/emil/work/storefront/node_modules/.pnpm/@[email protected]/node_modules/@auth/core/lib/routes/callback.js:20:41) at async AuthInternal (file:///home/emil/work/storefront/node_modules/.pnpm/@[email protected]/node_modules/@auth/core/lib/index.js:65:38) at async Proxy.Auth (file:///home/emil/work/storefront/node_modules/.pnpm/@[email protected]/node_modules/@auth/core/index.js:100:30) at async isAuthCall (/home/emil/work/storefront/src/hooks.server.ts:123:12) at async JWTCheck (/home/emil/work/storefront/src/hooks.server.ts:116:10) at async Module.respond (/home/emil/work/storefront/node_modules/.pnpm``/@[email protected][email protected][email protected]/node_modules/@sveltejs/kit/src/runtime/server/respond.js:282:20) [auth][details]: { "provider": "instagram"}

How to reproduce

npm run dev, with valid instagram auth creds hooked up to the --host vite uri and try sign in with instagram

Expected behavior

instagram sign in to work

emilaleksanteri avatar Oct 16 '23 14:10 emilaleksanteri

https://github.com/nextauthjs/next-auth/blob/054dbe683c1dc52d4ec181eed87a2369ef64c27b/packages/core/src/lib/oauth/callback.ts#L145 error from the checks made here in the library here https://github.com/panva/oauth4webapi/blob/c17ef94bd421ba4c86d415c09109127e2dafb2d4/src/index.ts#L2127

emilaleksanteri avatar Oct 16 '23 17:10 emilaleksanteri

I've just come across the same problem, @emilaleksanteri did you manage to find a workaround?

numman-ali avatar Oct 25 '23 14:10 numman-ali

@numman-ali just didn't use instagram oauth, it looks like meta also doesn't want you to use instagram as an oauth method as seen in the docs limitations: https://developers.facebook.com/docs/instagram-basic-display-api/

emilaleksanteri avatar Oct 25 '23 14:10 emilaleksanteri

@emilaleksanteri I found a solution that will allow the instagram authentication to work with next auth. You need to factor in two points:

  1. oauth4webapi is very strict with ensuring specification are met
  2. Instagram auth misses some properties on it's respose
  3. Instagram auth gives a short access_token which must be replaced with a long lived access token

To compensate for all of the above, I debugged along the authorization flow and found where the patching for the request was needed. When oauth4webapi makes a POST request to https://api.instagram.com/oauth/access_token, the response requires patching of the body to contain token_type as "bearer" and then swapping out the short access token for a long lived access token.

Anyway, to make this easier for anyone else, all you need to do is use this fetch intercepter I created within the NextAuth GET router handler. Code for both is below. Let me know if you have any issues.

Instagram Provider set up to pass in additional scopes

export const InstagramAuthProvider: Provider = Instagram({
  clientId: process.env.AUTH_INSTAGRAM_ID,
  clientSecret: process.env.AUTH_INSTAGRAM_SECRET,
  authorization:
    "https://api.instagram.com/oauth/authorize?scope=user_profile,user_media",
  /**
   * Profile is not set for instagram as it cannot be used to authenticate a user,
   * only for fetching media and user info.
   */
});

Custom Instagram Fetch Interceptor

// instagram-fetch.interceptor.ts

/**
 * This interceptor is used to modify the response of the instagram access token request as it does not strictly follow the OAuth2 spec
 * - The token_type is missing in the response
 * @param originalFetch
 */
export const instagramFetchInterceptor =
  (originalFetch: typeof fetch) =>
  async (
    url: Parameters<typeof fetch>[0],
    options: Parameters<typeof fetch>[1] = {},
  ) => {
    /* Only intercept instagram access token request */
    if (
      url === "https://api.instagram.com/oauth/access_token" &&
      options.method === "POST"
    ) {
      const response = await originalFetch(url, options);
      /* Clone the response to be able to modify it */
      const clonedResponse = response.clone();
      const body = await clonedResponse.json();

      /* Get the long-lived access token */
      const longLivedAccessTokenResponse = await originalFetch(
        `https://graph.instagram.com/access_token?grant_type=ig_exchange_token&client_secret=${process.env.AUTH_INSTAGRAM_SECRET}&access_token=${body.access_token}`,
      );
      const longLivedAccessTokenResponseBody =
        await longLivedAccessTokenResponse.json();

      body.access_token = longLivedAccessTokenResponseBody.access_token;
      body.token_type = "bearer";
      body.expires_in = longLivedAccessTokenResponseBody.expires_in;

      // Calculate the `expires_at` Unix timestamp by adding `expires_in` to the current timestamp
      const currentTimestampInSeconds = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds
      body.expires_at =
        currentTimestampInSeconds + longLivedAccessTokenResponseBody.expires_in;

      body.scope = "user_profile user_media"; 

      /*  Create a new response with the modified body */
      const modifiedResponse = new Response(JSON.stringify(body), {
        status: response.status,
        statusText: response.statusText,
        headers: response.headers,
      });

      /* Add the original url to the response */
      return Object.defineProperty(modifiedResponse, "url", {
        value: response.url,
      });
    }

    return originalFetch(url, options);
  };

Next auth advanced configuration for interceptor and disabling login/signup via instagram:

// app/api/auth/[...nextauth]/route.ts

import { type NextRequest, NextResponse } from "next/server";

import {
  auth,
  GET as AuthGET,
  instagramFetchInterceptor,
  POST as AuthPOST,
} from "@/auth";

const originalFetch = fetch;

export async function POST(req: NextRequest) {
  return await AuthPOST(req);
}

export async function GET(req: NextRequest) {
  const url = new URL(req.url);

  if (url.pathname === "/api/auth/callback/instagram") {
    const session = await auth();
    if (!session?.user) {
      /* Prevent user creation for instagram access token */
      const signInUrl = new URL("/?modal=sign-in", req.url);
      return NextResponse.redirect(signInUrl);
    }

     /* Intercept the fetch request to patch access_token request to be oauth compliant */
    global.fetch = instagramFetchInterceptor(originalFetch);
    const response = await AuthGET(req);
    global.fetch = originalFetch;
    return response;
  }

  return await AuthGET(req);
}

Disclaimer: Meta do not allow authorization using instagram, to verify a user an individual you must use facebook auth. To mitigate this and allow for passing instagram review process, I have hard disabled login to our app via instagram by rejecting the instagram connection if it being attempted by an unauthenticated user (ie a user that has logged in via another provider).

numman-ali avatar Nov 12 '23 20:11 numman-ali

Hey @numman-ali, thanks for your contribution! What does this line const session = await auth() do? It seems like you haven't attached the code for it. I noticed you import it from @/auth.

cmd8 avatar Jun 11 '24 02:06 cmd8

same issue on the foursquare provider

sinchang avatar Aug 22 '24 09:08 sinchang

I'm having similar issue with instagram provider

ericmil87 avatar Oct 15 '24 22:10 ericmil87

Next-auth v5 beta 5.0.0-beta.25 has hidden conform callback that lets you modify the token exchange payload to match the expected OAuth specifications.

An example (Shopify does not return token_type in the payload):

..
...
        token: {
          url: `https://${shop}.myshopify.com/admin/oauth/access_token`,
          conform: async (value: Response) => {
            const data = await value.json();
            return new Response(
              JSON.stringify({
                ...data,
                token_type: "Bearer", // added missing required data
              }),
              {
                headers: value.headers,
                status: value.status,
                statusText: value.statusText,
              }
            );
          },
        },
...
..

This way we don't need the interceptor hack, neither make changes on the third party library.

There is also a [customFetch] configuration for the provider but i couldn't make it get invoked.

Hope this helps other people running into the same issue.

mtnbarreto avatar Jan 24 '25 18:01 mtnbarreto

Next-auth v5 beta 5.0.0-beta.25 has hidden conform callback that lets you modify the token exchange payload to match the expected OAuth specifications.

An example (Shopify does not return token_type in the payload):

.. ... token: { url: https://${shop}.myshopify.com/admin/oauth/access_token, conform: async (value: Response) => { const data = await value.json(); return new Response( JSON.stringify({ ...data, token_type: "Bearer", // added missing required data }), { headers: value.headers, status: value.status, statusText: value.statusText, } ); }, }, ... ..

This way we don't need the interceptor hack, neither make changes on the third party library.

There is also a [customFetch] configuration for the provider but i couldn't make it get invoked.

Hope this helps other people running into the same issue.

You are a life saver.

matscode avatar Mar 18 '25 19:03 matscode