nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.37k stars 3.41k forks source link

Instagram provider causing auth core internal lib to fail #8868

Open emilaleksanteri opened 11 months ago

emilaleksanteri commented 11 months ago

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/oauth4webapi@2.3.0/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``/oauth4webapi@2.3.0/node_modules/oauth4webapi/build/index.js:1054:20) at async handleOAuth (file:///home/emil/work/storefront/node_modules/.pnpm/@auth+core@0.16.1/node_modules/@auth/core/lib/oauth/callback.js:84:18) at async Module.callback (file:///home/emil/work/storefront/node_modules/.pnpm/@auth+core@0.16.1/node_modules/@auth/core/lib/routes/callback.js:20:41) at async AuthInternal (file:///home/emil/work/storefront/node_modules/.pnpm/@auth+core@0.16.1/node_modules/@auth/core/lib/index.js:65:38) at async Proxy.Auth (file:///home/emil/work/storefront/node_modules/.pnpm/@auth+core@0.16.1/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``/@sveltejs+kit@1.25.2_svelte@4.2.1_vite@4.4.11/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 commented 11 months ago

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

numman-ali commented 11 months ago

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

emilaleksanteri commented 11 months ago

@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/

numman-ali commented 10 months ago

@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).

ekiwi111 commented 3 months ago

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.

sinchang commented 1 month ago

same issue on the foursquare provider