amplify-js: User object not detected in Remix.js SSR

Before opening, please confirm:

JavaScript Framework

Remix.js

Amplify APIs

Authentication, GraphQL API

Amplify Categories

auth, api

Environment information

# Put output below this line

 System:
    OS: macOS 10.15.7
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 30.52 MB / 16.00 GB
    Shell: 5.7.1 - /bin/zsh
  Binaries:
    Node: 16.3.0 - ~/.nvm/versions/node/v16.3.0/bin/node
    Yarn: 1.22.17 - /usr/local/bin/yarn
    npm: 8.5.3 - ~/.nvm/versions/node/v16.3.0/bin/npm
  Browsers:
    Chrome: 100.0.4896.75
    Firefox: 91.7.1
    Safari: 15.3
  npmPackages:
    @aws-amplify/ui-react: ^2.11.0 => 2.11.0
    @aws-amplify/ui-react-internal:  undefined ()
    @aws-amplify/ui-react-legacy:  undefined ()
    @remix-run/dev: ^1.3.3 => 1.3.3
    @remix-run/eslint-config: ^1.3.3 => 1.3.3
    @remix-run/react: ^1.3.4 => 1.3.4
    @remix-run/serve: ^1.3.4 => 1.3.4
    aws-amplify: ^4.3.18 => 4.3.18
    eslint: ^8.11.0 => 8.12.0
    react: ^17.0.2 => 17.0.2
    react-dom: ^17.0.2 => 17.0.2
    remix: ^1.3.3 => 1.3.3
  npmGlobalPackages:
    @architect/architect: 10.1.0
    @aws-amplify/cli: 7.6.26
    aws-amplify: 4.3.11-unstable.4+73587d78f
    aws-cdk: 1.131.0
    aws-sdk: 2.1090.0
    create-react-app: 5.0.0
    netlify-cli: 9.13.3
    npm: 8.5.3
    standard: 16.0.4

Describe the bug

Authorization rules for mutating data don’t pass even though I am signed in.

Expected behavior

If I’m signed in, I should be able to mutate data in SSR functions in Remix. I get errors instead. Also if I try to get the user object, it says I’m not signed in.

{
  data: { createTask: null },
  errors: [
    {
      path: [Array],
      data: null,
      errorType: 'Unauthorized',
      errorInfo: null,
      locations: [Array],
      message: 'Not Authorized to access createTask on type Mutation'
    }
  ]
}

Reproduction steps

  1. Create a Remix App
  2. Enable Amplify Auth (I went through Studio)
  3. Enable local auth (withAuthenticator)
  4. Create a GraphQL API
  5. Create a Remix action for creating a new data instance

Code Snippet

// Form
import { Form } from 'remix'
import { TextField, Button } from '@aws-amplify/ui-react'

export default function CreateTask () {
  return (
    <Form method='post' action='/tasks'>
      <TextField name='task' label='Create a task' />
      <Button marginTop='10px' type='submit' variation='default'>Create</Button>
    </Form>
  )
}
// Action
import { redirect } from 'remix'
import { withSSRContext } from 'aws-amplify'
import { createTask } from '../../src/graphql/mutations'

export async function action ({ request }) {
  const body = await request.formData()
  const SSR = withSSRContext(request)

  const user = await SSR.Auth.currentAuthenticatedUser()

  await SSR.API.graphql({
    query: createTask,
    variables: {
      input: {
        name: body._fields.task[0],
        done: false
      }
    }
  })

  return redirect('/')
}
type Task @model  @auth(rules: [{ allow: owner }]) {
  done: Boolean!
  dueDate: AWSDateTime
  name: String!
}

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": "us-east-1",
    "aws_appsync_graphqlEndpoint": "https://oulcqjwcwrf7dbrrtvdh4bgwdu.appsync-api.us-east-1.amazonaws.com/graphql",
    "aws_appsync_region": "us-east-1",
    "aws_appsync_authenticationType": "API_KEY",
    "aws_appsync_apiKey": "da2-o6lgqk6lvjaplcmupxygskjtsa",
    "aws_cognito_identity_pool_id": "us-east-1:9adfe06e-9a19-4352-ba4e-07c941a43448",
    "aws_cognito_region": "us-east-1",
    "aws_user_pools_id": "us-east-1_snzFoLBVY",
    "aws_user_pools_web_client_id": "2trpa7u054fvrksgnk8a48e4if",
    "oauth": {},
    "aws_cognito_username_attributes": [
        "EMAIL"
    ],
    "aws_cognito_social_providers": [],
    "aws_cognito_signup_attributes": [],
    "aws_cognito_mfa_configuration": "OFF",
    "aws_cognito_mfa_types": [
        "SMS"
    ],
    "aws_cognito_password_protection_settings": {
        "passwordPolicyMinLength": 8,
        "passwordPolicyCharacters": [
            "REQUIRES_LOWERCASE",
            "REQUIRES_NUMBERS",
            "REQUIRES_SYMBOLS",
            "REQUIRES_UPPERCASE"
        ]
    },
    "aws_cognito_verification_mechanisms": [
        "EMAIL"
    ]
};


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

Additional information and screenshots

https://user-images.githubusercontent.com/12969662/162237172-761e361d-86c3-45ca-a895-c4b16dbc51b5.mp4

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 17 (6 by maintainers)

Most upvoted comments

I’m able to reproduce this issue, both when a user is logged in and when no user is logged in.

Adding authMode: "AMAZON_COGNITO_USER_POOLS", to the GQL mutation doesn’t make a difference for me.

I’ll keep looking into this and keep you posted!

This issue is also in Next.js

Reading the code of withSSRContext I found the solution for remix:

index.tsx

import type { LoaderFunction } from '@remix-run/node';

import type { User } from '~/entities/auth';

import { getUserFromRequest } from '~/lib/amplify/auth/users'

type LoaderData = { user: User | null };

export const loader: LoaderFunction = async ({ request }) => {
  const user = await getUserFromRequest(request);
  
  return json<LoaderData>({ user });
}

lib/amplify/auth/users.ts

import type { Auth as AuthClass } from '@aws-amplify/auth';
import { withSSRContext } from 'aws-amplify';
import get from 'lodash/get';

import type { CognitoUserAttributes, User } from '~/entities/auth';

import logger from '../logger';

export async function getUserFromRequest(request: Request) {
  try {
    const Auth = withSSRContext({
      req: { headers: { cookie: request.headers.get('cookie') } },
    }).Auth as typeof AuthClass;

    const cognitoUser = await Auth.currentAuthenticatedUser();
    const attributes = get<CognitoUserAttributes>(
      cognitoUser,
      'attributes',
      {} as CognitoUserAttributes,
    );

    const user: User = {
      id: cognitoUser.username,
      name: attributes.name,
      email: attributes.email,
    };

    return user;
  } catch (error) {
    logger.debug(`user not authenticated ${error}`);

    return null;
  }
}

Hi all 👋🏾 - following up on this issue. The two key things needed to get this working when attempting to get the auth session on the server side are as follows:

  • You will need to login the user on the client as @visomi-dev mentioned
  • Pass the cookies to withSSRContext as shown on this comment
  • Be sure to set ssr: true in your Amplify.configure()

With the above configured, you will be able to use withSSRContext in your loader() or action() functions. I have setup a simple working example here that uses withSSRContext in a loader() function:

Thanks for all the input and discussion on this issue, let me know if there are any other questions on this.

Many thanks to @visomi-dev and @DavidBrear – I spent over a day going around and around trying to work out how to get Auth working server side and your two code snippets have been the answer I was looking for.

@DavidBrear try to call SignIn on frontend directly to aws cognito (no on the server-side).

import { useState } from 'react';
// previously configure amplify with SSR flag
import { Auth } from 'amplify' 

function SignIn() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const onSubmitHandler = async (event) => {
    event.preventDefault();
    
    try {
      await Auth.SignIn(email, password);
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <form onSubmit={onSubmitHandler}>
      <input name="email" type='email' onInput={(event) => setEmail(event.target.value)} />
      <input name="password" type='password' onInput={(event) => setPassword(event.target.value)} />
    </form>
  )
}

@visomi-dev 's solution https://github.com/aws-amplify/amplify-js/issues/9780#issuecomment-1254262202 ☝️ up there worked for me. One thing I noticed was when you do Auth.signIn in the action method it won’t set the cookies and actually doesn’t return any cookies. I did the auth sign in with:

login.tsx

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const email = formData.get("email");
  const password = formData.get("password");
  const SSR = withSSRContext({ req: request });
  const user = await SSR.Auth.signIn(email, password);
  return saveAmplifyCookies(user.pool.storage.store);
}

then in session.server.ts

export async function saveAmplifyCookies(cookies: Record<string, string>) {
  const headers = new Headers();
  Object.keys(cookies).forEach((key) => {
    headers.append(
      "Set-Cookie",
      `${key}=${cookies[key]}; Max-Age=157680000; Path=/; HttpOnly; SameSite=Lax`,
    );
  });
  return redirect("/", { headers });
}

@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