aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.39k stars 2.11k forks source link

Amplify Causing Errors With Next 12 middleware #9145

Closed bluetoken-luke closed 7 months ago

bluetoken-luke commented 2 years ago

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

Authentication

Amplify Categories

auth

Environment information

``` # Put output below this line ```

Describe the bug

We are working on updating our application up to NextJS 12. In doing so we are interested in moving the authentication check for protected pages to the new middleware feature available in next 12 (link). This code runs on the server before a request is completed. With that in mind, I went about attempting to use withSSRContext to check the users authentication status in the middleware.

import type { NextFetchEvent, NextRequest } from 'next/server';
import { Amplify, withSSRContext } from 'aws-amplify';
import { AwsConfig } from '../../../../../../src/config';

Amplify.configure({ ...AwsConfig, ssr: true });

export const middleware = async (req: NextRequest, ev: NextFetchEvent) => {
  console.log('req', req);

  const { Auth } = withSSRContext({ req: req });
  return new Response('Hello World!');
};

As you can see above, I didn't get too far before this already broke the app. It seems that something underlying is attempting to use window which of course is not present on server. This looks to be happening in Reachablility

image

Expected behavior

Authentication is checked in the same manner it would be for getServerSideProps

Reproduction steps

In a next 12 application. place a _middleware.ts or _middleware.js file in one of your page directories. In that middleware file, simply import Amplify and that should be enough to draw out the exception.

Code Snippet

import type { NextFetchEvent, NextRequest } from 'next/server';
import { Amplify, withSSRContext } from 'aws-amplify';
import { AwsConfig } from '../../../../../../src/config';

Amplify.configure({ ...AwsConfig, ssr: true });

export const middleware = async (req: NextRequest, ev: NextFetchEvent) => {
  console.log('req', req);

  const { Auth } = withSSRContext({ req: req });
  return new Response('Hello World!');
};

Log output

``` // Put your logs below this line ```

aws-exports.js

No response

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

arnaringig commented 1 year ago

@abdallahshaban557 any update on this?

abdallahshaban557 commented 1 year ago

Hi @arnaringig - not yet, we've made some progress and have a design in mind, but we believe it will have some breaking changes. We expect this to be part of our next major version. We will provide an update when we have an ETA.

phstc commented 1 year ago

@aiji42 @dyeoman2 thanks for sharing your solutions. I'm trying to implement them, but I'm getting ERR_JWT_EXPIRED after some time (60 minutes/default ID token expiration). Even if I re-login, it seems the cookie/jwt expiration does not get updated. Did you experience this too?

await jwtVerify(token, await importJWK(jwk)) this is what throws the error.

I can probably extend the ID Token expiration. But I'm concerned about why the re-sign-in does not update it.

louisthomaspro commented 11 months ago

Hi @phstc, same on my side. I also noticed that this method: Auth.currentAuthenticatedUser() is getting a new token all the time, but is not updating the Local Storage or Cookies so I am getting the same error as you ERR_JWT_EXPIRED.

Do you have updates ?

viep commented 11 months ago

Just switched to NextAuth instead of amplify due to this

jordanpurinton commented 10 months ago

bump

abdallahshaban557 commented 10 months ago

We have this on our radar as a feature that we will support as part of our next major version of the Amplify JavaScript library, which is currently in development. We will provide an update when we have an ETA.

hoquescript commented 10 months ago

I had the same problem and next-fortress didn't quite do that trick for me, but I was able to take some of their code to create the solution below.

// _middleware.js
import { NextResponse } from 'next/server';
import { decodeProtectedHeader, importJWK, jwtVerify } from 'jose';

// Middleware that prevents unauthenticated users from accessing protected resources
async function handler(req) {
  const url = req.nextUrl.clone();

  try {
    // Define paths that don't require authentication
    const unauthenticatedPaths = [
      '/',
      '/login',
      '/password-reset',
      '/pricing',
      '/signup',
      '/support',
    ];

    // Allow users to continue for pages that don't require authentication
    if (unauthenticatedPaths.includes(url.pathname)) {
      return NextResponse.next();
    } else {
      // Authenticate users for protected resources

      // Cognito data
      const region = process.env.AWS_REGION;
      const poolId = process.env.AWS_COGNITO_USER_POOL_ID;
      const clientId = process.env.AWS_USER_POOLS_WEB_CLIENT_ID;

      // Get the user's token
      const token = Object.entries(req.cookies).find(([key]) =>
        new RegExp(
          `CognitoIdentityServiceProvider\\.${clientId}\\..+\\.idToken`
        ).test(key)
      )?.[1];

      if (token) {
        // Get keys from AWS
        const { keys } = await fetch(
          `https://cognito-idp.${region}.amazonaws.com/${poolId}/.well-known/jwks.json`
        ).then((res) => res.json());

        // Decode the user's token
        const { kid } = decodeProtectedHeader(token);

        // Find the user's decoded token in the Cognito keys
        const jwk = keys.find((key) => key.kid === kid);

        if (jwk) {
          // Import JWT using the JWK
          const jwtImport = await importJWK(jwk);

          // Verify the users JWT
          const jwtVerified = await jwtVerify(token, jwtImport)
            .then((res) => res.payload.email_verified)
            .catch(() => failedToAuthenticate);

          // Allow verified users to continue
          if (jwtVerified) return NextResponse.next();
        }
      }
    }
  } catch (err) {
    console.log('err', err);
  }

  // Send 401 when an unauthenticated user trys to access API endpoints
  if (url.pathname.includes('api')) {
    return new Response('Auth required', { status: 401 });
  } else {
    // Redirect unauthenticated users to the login page when they attempt to access protected pages
    return NextResponse.redirect(`${url.origin}/login`);
  }
}

export default handler;

In nextjs 13 we should retrieve the token in the following way:

const token = req.cookies
        .getAll()
        .find((cookie) =>
          new RegExp(
            `CognitoIdentityServiceProvider\\.${clientId}\\..+\\.idToken`,
          ).test(cookie.name),
        )?.value;
Meags27 commented 10 months ago

also need this feature too, same reason as everyone else, to check if the user is authenticated in middleware.ts in a next.js app directory site. For now, I'm going to use this as a workaround https://repost.aws/knowledge-center/decode-verify-cognito-json-token

p24-max commented 9 months ago

Unresolved since nearly 2 years :(

cwomack commented 8 months ago

The developer preview for v6 of Amplify has officially been released with improvements Next.js middleware and much more! Please check out our announcement and updated documentation to see what has changed.

This issue should be resolved within the dev preview and upcoming General Availability for Amplify v6, but let us know with a comment if there are further issues.

cwomack commented 7 months ago

With the release of the latest major version of Amplify (aws-amplify@>6), this issue should now be resolved! Please refer to our release announcement, migration guide, and documentation for more information.