amplify-js: withSSRContext not updating auth information on server-side

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

Authentication

Amplify Categories

auth

Environment information


System:
    OS: macOS 10.15.7
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 496.23 MB / 16.00 GB
    Shell: 5.7.1 - /bin/zsh
  Binaries:
    Node: 12.20.0 - ~/.volta/tools/image/node/12.20.0/bin/node
    Yarn: 1.22.5 - ~/.volta/tools/image/yarn/1.22.5/bin/yarn
    npm: 6.14.8 - ~/.volta/tools/image/npm/6.14.8/bin/npm
    Watchman: 4.9.0 - /usr/local/bin/watchman
  Browsers:
    Brave Browser: 89.1.22.70
    Chrome: 89.0.4389.114
    Edge: 89.0.774.63
    Firefox Developer Edition: 88.0
    Safari: 14.0.3
  npmPackages:
    @apollo/client: ^3.3.13 => 3.3.13 
    @apollo/react-hooks: ^4.0.0 => 4.0.0 
    @apollo/react-ssr: ^4.0.0 => 4.0.0 
    @aws-amplify/ui-react: ^1.0.6 => 1.0.6 
    @material-ui/core: ^4.11.3 => 4.11.3 
    @material-ui/icons: ^4.11.2 => 4.11.2 
    @types/draft-js: ^0.11.1 => 0.11.1 
    @types/isomorphic-fetch: ^0.0.35 => 0.0.35 
    @types/jest: ^26.0.12 => 26.0.22 
    @types/next-auth: ^3.13.0 => 3.13.0 
    @types/node: ^14.0.11 => 14.14.37 
    @types/react: ^17.0.3 => 17.0.3 
    @types/react-i18next: ^8.1.0 => 8.1.0 
    @types/slate: ^0.47.7 => 0.47.8 
    @types/slate-react: ^0.22.9 => 0.22.9 
    @types/styled-components: ^5.1.0 => 5.1.9 
    @typescript-eslint/eslint-plugin: ^4.20.0 => 4.20.0 
    @typescript-eslint/parser: ^4.20.0 => 4.20.0 
    apollo-client: ^2.6.10 => 2.6.10 
    apollo-link: ^1.2.14 => 1.2.14 
    apollo-link-http: ^1.5.17 => 1.5.17 
    aws-amplify: ^3.3.26 => 3.3.26 
    aws-amplify-react: ^4.2.30 => 4.2.30 
    babel-eslint: ^10.1.0 => 10.1.0 
    babel-jest: ^26.3.0 => 26.6.3 
    babel-plugin-styled-components: ^1.12.0 => 1.12.0 
    date-fns: ^2.14.0 => 2.19.0 
    deepmerge: ^4.2.2 => 4.2.2 
    draft-js: ^0.11.6 => 0.11.7 
    eslint: ^7.23.0 => 7.23.0 
    eslint-config-prettier: ^8.1.0 => 8.1.0 
    eslint-config-react-app: ^6.0.0 => 6.0.0 
    eslint-plugin-flowtype: ^5.4.0 => 5.4.0 
    eslint-plugin-import: ^2.21.2 => 2.22.1 
    eslint-plugin-jsx-a11y: ^6.3.1 => 6.4.1 
    eslint-plugin-prettier: ^3.1.4 => 3.3.1 
    eslint-plugin-react: ^7.20.0 => 7.23.1 
    eslint-plugin-react-hooks: ^4.2.0 => 4.2.0 
    fast-check: ^2.2.1 => 2.14.0 
    graphql: ^15.1.0 => 15.5.0 
    i18next: ^20.1.0 => 20.1.0 
    jest: ^26.4.2 => 26.6.3 
    lodash: ^4.17.21 => 4.17.21 
    next: ^10.1.2 => 10.1.2 
    prettier: ^2.2.1 => 2.2.1 
    react: ^17.0.2 => 17.0.2 
    react-dom: ^17.0.2 => 17.0.2 
    react-hook-form: ^6.15.5 => 6.15.5 
    react-i18next: ^11.5.0 => 11.8.12 
    react-is: ^17.0.2 => 17.0.2 
    slate: ^0.61.3 => 0.61.3 
    slate-hyperscript: ^0.61.3 => 0.61.3 
    slate-react: ^0.61.3 => 0.61.3 
    styled-components: 5.2.1 => 5.2.1 
    styled-normalize: ^8.0.7 => 8.0.7 
    stylelint: ^13.6.1 => 13.12.0 
    stylelint-config-recommended: ^4.0.0 => 4.0.0 
    stylelint-config-styled-components: ^0.1.1 => 0.1.1 
    stylelint-processor-styled-components: ^1.10.0 => 1.10.0 
    typescript: ^4.2.3 => 4.2.3 
  npmGlobalPackages:
    npm: 6.14.8

Describe the bug

I’m using Cognito Authentication with Amplify in a next.js environment. I’ve configured amplify for SSR, but it seems like the withSSRContext is not updating properly. Despite a successful login, I’m not authorized on the serverside.

My flow: index.tsx

export const withAuth = async (context) => {
  try {
    const SSR = withSSRContext(context);
    const user = await SSR.Auth.currentAuthenticatedUser();
    return user;
  } catch (error) {
    context.res.writeHead(301, { Location: '/sign-in' });
    context.res.end();
    return null;
  }
};

export async function getServerSideProps(context) {
  const auth = await withAuth(context);
  return {
    props: {
      authenticated: auth != null,
    },
  };
}

Because you are not logged in, you will get redirected to /sign-in. On the /sign-in page you can log in via Auth.federatedSignIn({ provider: 'Google' })

The sign-in redirect URL is configured to http://localhost:3000. After you log in successfully you will land on the index.tsx again, but the Auth within getServerSideProps is not updated. It constantly returns an error from await SSR.Auth.currentAuthenticatedUser(); so you will land at the /sign-in page again.

Expected behavior

I expect that the Auth information is available on the server-side after a successful login. For me, it seems like the amplify-js is updating the auth on the client-side and this is too late for handling stuff in a server-environment.

Reproduction steps

  1. create-next-app amplify-auth-ssr-test
  2. amplify init
  3. amplify add auth
  4. Add an auth with a social provider and a redirect sign-in URL to http://localhost:3000
  5. Try to do some auth stuff mentioned within the bug details

Code Snippet

// Put your code below this line.

Log output

// Put your logs below this line


aws-exports.js

/* eslint-disable */
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
    "aws_project_region": "eu-central-1",
    "aws_cognito_identity_pool_id": "XXX",
    "aws_cognito_region": "eu-central-1",
    "aws_user_pools_id": "XXX",
    "aws_user_pools_web_client_id": "XXX",
    "oauth": {
        "domain": "XXX",
        "scope": [
            "phone",
            "email",
            "openid",
            "profile",
            "aws.cognito.signin.user.admin"
        ],
        "redirectSignIn": "http://localhost:3000/",
        "redirectSignOut": "http://localhost:3000/",
        "responseType": "code"
    },
    "federationTarget": "COGNITO_USER_POOLS",
    "aws_appsync_graphqlEndpoint": "XXX",
    "aws_appsync_region": "XXX",
    "aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS"
};


export default awsmobile;

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

I will try to create an example repo today

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 9
  • Comments: 25 (5 by maintainers)

Most upvoted comments

For those who are using Next.js with SSR to authenticate if the user has access to a speciffic page. The solution is similar as @lifedup but without setting the credentials in each method.

In your _app.ts (if you are using typescript) configure the following aws credentials:

import Amplify, { withSSRContext } from 'aws-amplify';
// Imported from amplify pull
import awsconfig from '../aws-exports';

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

// Auth from SSR and not directly from aws-amplify
const { Auth } = withSSRContext();

Auth.configure({
  region: process.env.AUTH_REGION,
  userPoolId: process.env.AUTH_POOL,
  userPoolWebClientId: process.env.AUTH_POOL_CLIENT,
  mandatorySignIn: false,
  // Set this only if you wish to use cookies to storage otherwise ignore it
  cookieStorage: {
    domain: 'localhost',
    // Set true if is a domain with https. For localhost set it to false
    secure: false,
    path: '/',
    expires: 2,
  },
})

In your index.ts or any other page that you would like to verify authentication:


import { withSSRContext } from "aws-amplify";

// Function that handles redirect for the user. You can isolate it in a /utils folder to re-use it in all your pages.
export async function authenticatedUsers(context) {
  const { Auth } = withSSRContext(context);
  try {
    await Auth.currentAuthenticatedUser();
  } catch (error) {
    console.log(error)
    return true
  }
  return false
}

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  let shouldRedirect = await authenticatedUsers(ctx);
  if (shouldRedirect) {
    return {
      redirect: {
        destination: '/login',
        permanent: false
      }
    }
  }
  return {
    props: {}
  }
}

I was able to reproduce this today. As of now redirecting client side should be the best work around for this. This is caused by the way Amplify loads cookies inside the browser, and how it can detect if a user is logged in or not.

Amplify must load at least once client side after being redirected from a federated login before getServerSideProps can detect if the user is logged in or not.

To make this faster, make sure the route is completely empty, and all it does is redirect.

@chrisbonifacio

The bug occurs because the username contains @ but in cookies the @ becomes %40, on the following line the username contains @ instead of %40 https://github.com/aws-amplify/amplify-js/blob/main/packages/amazon-cognito-identity-js/src/CognitoUser.js#L1388

I guess the encodeURIComponent solve the problem

@tobiastimm Are you or anyone else still blocked on this issue? The cause of the issue can be different for each person but there have been some comments noting proper usage of withSSRContext, some gotchas like when it comes to oauth that require Amplify to be able to finish the oauth flow which can be interrupted by server side redirects (workaround being client side redirect after signin is confirmed).

I believe I am also experiencing this issue, or a similar one. Our Cognito is configured to use two styles of login, a federated login through my company’s SSO, and Cognito defined users I can log in to directly. -With the users internal to Cognito using withSSRContext() works as expected -With federated users authenticated by our SSO withSSRContext() never recognizes the logged in user. currentAuthenticatedUser() always throws the unauthenticated exception.

Yup exactly, @bmkennedy-hcg you hit the nail on the head. This is the common scenario we are also encountering. For us, on initial sing-in redirect there is no authenticated user. So what we do is we fallback/redirect to a callback page route, which loads amplify client side, which then performs another client side redirect, and then and only then is the user authenticated.

It’s a lame workaround but it works. Since the initial sign-in redirect from cognito contains the code and state, I figured I could use the oAuthClient exported by the server, however the client the server uses, actually rely’s on window object. So go figure…

Thanks, @voigtito. Like you mentioned, I was able to get withSSRContext to work once I configured Auth inside of getServerSideProps. My code now looks like this:

export const getServerSideProps: GetServerSideProps<Props> = async ({
  params,
  req,
}) => {
  const { Auth } = withSSRContext({ req });
  Auth.configure({...config});

  try {
    const user = await Auth.currentAuthenticatedUser();
    ....
  } catch (error) {
    ....
  }
}

None of the docs mentioned that it is necessary to configure the Auth object returned by withSSRContext. Here is an example: https://aws.amazon.com/blogs/mobile/ssr-support-for-aws-amplify-javascript-libraries/

Hi 👋 Closing this as we have not heard back from you. If you are still experiencing this issue and in need of assistance, please feel free to open a new issue and fill out as much info in the bug report form as you can provide.

Thank you!

@bezreyhan if you configure the Amplify or Auth just once in your _app.tsx it should work! If you configure it only in your index.tsx it wont work properly.

In a next.js application if you create the _app.tsx file inside /pages folder your next.js will always look at first. So this is the file where you need to configure your Amplify.

Here is an example of the _app.tsx file:

import dotenv from 'dotenv';
import { AppProps } from 'next/app';
import Amplify from 'aws-amplify';
import awsconfig from '../aws-exports';
import '../styles/global.css';

dotenv.config();

Amplify.configure({...awsconfig,ssr: true});
// Auth from SSR and not directly from aws-amplify
const { Auth } = withSSRContext();

Auth.configure({
  region: process.env.AUTH_REGION,
  userPoolId: process.env.AUTH_POOL,
  userPoolWebClientId: process.env.AUTH_POOL_CLIENT,
  mandatorySignIn: false,
  // Set this only if you wish to use cookies to storage otherwise ignore it
  cookieStorage: {
    domain: 'localhost',
    // Set true if is a domain with https. For localhost set it to false
    secure: false,
    path: '/',
    expires: 2,
  },
})

const MyApp = ({ Component, pageProps }: AppProps) => {
  return (
     <Component {...pageProps} />
  )
}

export default MyApp

After setting this you can use Auth inside your getServerSideProps properly.