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.42k stars 2.12k forks source link

401 error authenticating through cognito #11799

Closed ArjunSohur closed 1 year ago

ArjunSohur commented 1 year ago

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

GraphQL API

Amplify Categories

auth, api

Environment information

``` (base) arjunsohur@tack-119 np_website2 % npx envinfo --system --binaries --browsers --npmPackages --duplicates --npmGlobalPackages System: OS: macOS 12.6 CPU: (8) arm64 Apple M1 Memory: 91.19 MB / 8.00 GB Shell: 5.8.1 - /bin/zsh Binaries: Node: 18.17.0 - /usr/local/bin/node npm: 9.8.1 - /usr/local/bin/npm Browsers: Chrome: 115.0.5790.170 Edge: 114.0.1823.67 Safari: 15.6.1 npmPackages: @aws-amplify/ui-react: ^5.1.0 => 5.1.0 @aws-amplify/ui-react-internal: undefined () @testing-library/jest-dom: ^5.17.0 => 5.17.0 @testing-library/react: ^13.4.0 => 13.4.0 @testing-library/user-event: ^13.5.0 => 13.5.0 aws-amplify: ^5.3.7 => 5.3.7 graphql: ^16.7.1 => 16.7.1 (15.8.0) ra-data-graphql: ^4.12.2 => 4.12.2 react: ^18.2.0 => 18.2.0 react-dom: ^18.2.0 => 18.2.0 react-router-dom: ^6.14.2 => 6.14.2 react-scripts: 5.0.1 => 5.0.1 web-vitals: ^2.1.4 => 2.1.4 npmGlobalPackages: @aws-amplify/cli: 12.2.5 corepack: 0.18.0 npm: 9.8.1 ```

Describe the bug

m trying to set up a web app using react.js hosted by amplify on AWS, which I will need to store user preferences of certain categories in a dynamoDB table.

Users must authenticate with Cognito before being able to view/interact with their preferences. I have been having a hard time trying to connect to DynamoDB table that came from the graphQL api which was created with the amplify. In particular, I have been receiving the following error:

Error: Request failed with status code 401 at createError Currently, I try to authenticate the api through "AMAZON_COGNITO_USER_POOLS", however, I have also tried authenticating through IAM, as the answer to this question suggested (Appsync return 401 errors when connecting with cognito).

I have also tried to add more permissions (graphQL and appsync authorization, along with the expected dynampDB permissions) to the identity pool which is the identity provider for the user group which defines the Cognito user pool.

This web app is my first, so it is entirely possible that I'm missing something obvious. Also, my please correct me if my jargon is incorrect or confusing.

I'm more than happy to provide more code if necessary.

Thanks for your help!

Expected behavior

I'd want the information that I send to dynamoDB to be stored.

Reproduction steps

  1. Create react app
  2. Amplify add Auth - set up authentication with email-based log in
  3. Create home and preferences page
  4. Create page routing between preferences and home
  5. Amplify add api - Cognito sign in for api authorization
  6. Send data through api

(src looks like:

Screen Shot 2023-08-14 at 4 40 26 PM

)

Code Snippet

preferences.js:


import React, { useState, useEffect } from 'react';
import logoImage from '../Logo.jpg';

import { API, graphqlOperation, Auth} from 'aws-amplify';
import { getUser } from '../graphql/queries';
import { createUser, updateUser } from '../graphql/mutations';

function Prefs() {

  // INITIAL PREFS DEFAULT ---------------------------------------------------------------------------------------------------------
  const initialPreferences = *array of preferences*

  const initialCategoryRatings = *array of category ratings*

  const initialUnusedTopics = []
  // END INITIAL PREFS DEFAULT ------------------------------------------------------------------------------------------------------

  // START OF STATE STUFF -----------------------------------------------------------------------------------------------------------
  const [preferences, setPreferences] = useState(initialPreferences);
  const [categoryRatings, setCategoryRatings] = useState(initialCategoryRatings);
  const [unusedTopics, setUnusedTopics] = useState(initialUnusedTopics);

  const [editingMode, setEditingMode] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  // END OF STATE STUFF ---------------------------------------------------------------------------------------------------------------

  // HANDELS STRING I/O ---------------------------------------------------------------------------------------------------------------
  const convert3DArrayToString = (array3D) => {
    return JSON.stringify(array3D);
  };

  const convertStringTo3DArray = (string) => {
    return JSON.parse(string);
  };
  // END HANDELS STRING I/O -----------------------------------------------------------------------------------------------------------

  // START OF API STUFF -------------------------------------------------------------------------------------------------------------
  const fetchUser = async () => {
    try {

        // getting user ID
        const user = await Auth.currentAuthenticatedUser();
        const userID = user.attributes.sub; // check if this works
        console.log("userID", userID);

        // geting dat abased on userID
        const userData = await API.graphql(graphqlOperation(getUser, { userID }));

        const temp_Preferences = userData.data.getUser.user_preferences;
        console.log("fetched prefs", temp_Preferences);
        setPreferences(convertStringTo3DArray(temp_Preferences));

        const temp_CategoryRatings = userData.data.getUser.category_ratings;
        console.log("fetched cat ratings", temp_CategoryRatings);
        setCategoryRatings(convertStringTo3DArray(temp_CategoryRatings));

        const temp_UnusedTopics = userData.data.getUser.unused_topics;
        console.log("fetched inital topics", temp_UnusedTopics);
        setUnusedTopics(convertStringTo3DArray(temp_UnusedTopics));

    } catch (error) {
        console.error("Error in fetching data", error)
      try {

        const user = await Auth.currentAuthenticatedUser(); // this call is broken because of improper data formatting - I'll fix later
        const userID = user.attributes.sub; // check if this works
        const email = user.attributes.email

        const input = {
          id: userID,
          email: email,
          category_ratings: convert3DArrayToString(categoryRatings),
          user_preferences: convert3DArrayToString(preferences),
          unused_topics: convert3DArrayToString(unusedTopics)
          // are the time and updated at done automatically?
        };

        console.log("input", input)

       // **LINE THAT SENDS API CALL**
        const create_user_response = await API.graphql({
          query: createUser, 
          variables: { input },
          authMode: "AMAZON_COGNITO_USER_POOLS"  
        });

        console.log('User created:', create_user_response.data.createUser);

      } catch (error){
        console.error('Error creating user:', error);
      }
    }
  }

  const updateUserLocal = async () => {
    try {
      const user = await Auth.currentAuthenticatedUser();
      const userID = user.attributes.sub; // check if this works
      console.log("userID", userID);

      const newinput = {
        id: userID,
        email: user.attributes.email,
        category_ratings: convert3DArrayToString(categoryRatings),
        user_preferences: convert3DArrayToString(preferences),
        unused_topics: convert3DArrayToString(unusedTopics)
        // are the time and updated at done automatically?
      };

      const update_user_response = await API.graphql(graphqlOperation(updateUser, { newinput }));
      console.log('User Updated: ', update_user_response)
    } catch (error){
      console.error('Couldnt update user: ', error)
    }
  }
  // END OF API STUFF -------------------------------------------------------------------------------------------------------------

  // LOADING AND UNLOADING PAGE ---------------------------------------------------------------------------------------------------

  // Fetch user data when the component mounts and when they unload
  useEffect(() => {

    // solves missing depedencies error
    const fetchData = async () => {
      await fetchUser(); // Call fetchUser here
    };

    const updateUserData = async () => {
      await updateUserLocal();
    }

    fetchData();

    // Attach event listener to call updateUserLocal when user leaves the page
    const beforeUnloadHandler = (event) => {
      event.preventDefault();
      updateUserData();
    };

    window.addEventListener('beforeunload', beforeUnloadHandler);

    return () => {
      window.removeEventListener('beforeunload', beforeUnloadHandler);
    };
  }, []);
  // END LOADING AND UNLOADING PAGE -----------------------------------------------------------------------------------------------

  // WEBSITE STUFF ----------------------------------------------------------------------------------------------------------------
  const handlePreferenceChange = (categoryIndex, subCategoryIndex, newValue) => {
    const updatedPreferences = [...preferences];
    updatedPreferences[categoryIndex][subCategoryIndex][1] = newValue;
    setPreferences(updatedPreferences);
  };

  const handleCategoryRatingChange = (categoryIndex, newValue) => {
    const updatedCategoryRatings = [...categoryRatings];
    updatedCategoryRatings[categoryIndex][1] = newValue;
    setCategoryRatings(updatedCategoryRatings);
  };

  const removePreference = (categoryIndex, subCategoryIndex) => {
    const removedPreference = preferences[categoryIndex][subCategoryIndex];
    const updatedPreferences = [...preferences];
    updatedPreferences[categoryIndex].splice(subCategoryIndex, 1);
    setPreferences(updatedPreferences);
    setUnusedTopics([...unusedTopics, [removedPreference, categoryIndex, subCategoryIndex]]);
  };

  const restorePreference = (removedPreference) => {
    const [preference, categoryIndex, subCategoryIndex] = removedPreference;
    const updatedPreferences = [...preferences];
    updatedPreferences[categoryIndex].splice(subCategoryIndex, 0, preference);
    setPreferences(updatedPreferences);
    const updatedUnusedTopics = unusedTopics.filter(pref => pref !== removedPreference);
    setUnusedTopics(updatedUnusedTopics);
  };

  const handleSearch = (query) => {
    setSearchQuery(query);
  };

  const filteredUnusedTopics = unusedTopics.filter(pref =>
    pref[0][0].toLowerCase().includes(searchQuery.toLowerCase())
  );

  // END OF WEBSITE STUFF -------------------------------------------------------------------------------------------------------------

  // RETURN ----------------------------------------------------------------------------------------------------------------------------
  return (
    <header className="Page-header">
      <img src={logoImage} alt="News Pigeon Logo" style={{ width: '100px', height: 'auto' }} />
      <h2> News Pigeon Prefs </h2>
      {preferences.map((category, categoryIndex) => (
        <div key={categoryIndex}>
          <h3>{categoryRatings[categoryIndex][0]}</h3>
          <div>
            Category Rating:
            {editingMode ? (
              <input
                type="number"
                value={categoryRatings[categoryIndex][1]}
                min={1}
                max={10}
                onChange={(e) => handleCategoryRatingChange(categoryIndex, parseInt(e.target.value))}
              />
            ) : (
              <span>{categoryRatings[categoryIndex][1]}</span>
            )}
          </div>
          <ul>
            {category.map((subCategory, subCategoryIndex) => (
              <li key={subCategoryIndex}>
                {subCategory[0]}:
                {editingMode ? (
                  <input
                    type="number"
                    value={subCategory[1]}
                    min={1}
                    max={10}
                    onChange={(e) =>
                      handlePreferenceChange(categoryIndex, subCategoryIndex, parseInt(e.target.value))
                    }
                  />
                ) : (
                  <span>{subCategory[1]}</span>
                )}
                {editingMode && (
                  <button onClick={() => removePreference(categoryIndex, subCategoryIndex)}>Remove</button>
                )}
              </li>
            ))}
          </ul>
        </div>
      ))}
      <button onClick={() => setEditingMode(!editingMode)}>
        {editingMode ? 'Save Preferences' : 'Change Preferences'}
      </button>
      <div>
        <h3>Unused Topics</h3>
        <input
          type="text"
          value={searchQuery}
          placeholder="Search unused topics"
          onChange={(e) => handleSearch(e.target.value)}
        />
        <ul>
          {filteredUnusedTopics.map((unusedTopic, index) => (
            <li key={index}>
              {unusedTopic[0][0]} ({unusedTopic[0][1]})
              <button onClick={() => restorePreference(unusedTopic)}>Restore</button>
            </li>
          ))}
        </ul>
      </div>
    </header>
  );

  // END OF RETURN -------------------------------------------------------------------------------------------------------------------
}

export default Prefs;

// END OF FILE -----------------------------------------------------------------------------------------------------------------------

App.js:

import React from 'react';
import './App.css';
import awsconfig from './aws-exports';
import { Amplify } from 'aws-amplify';
import { Authenticator } from '@aws-amplify/ui-react';

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import Prefs from './pages/preferences';
import NavBar from './components/NavBar';

Amplify.configure(awsconfig);
function App() {
  return (
    <div className="App">

      <div>
        <BrowserRouter>

          <div className="Authenticator-container">
          <Authenticator>
            {({ signOut, user }) => (
              <div>
                <button onClick={signOut}>Sign out</button>
                <NavBar />

                <Routes>
                  <Route path="/" element={<Home />} />
                  <Route path="/home" element={<Home />} />
                  <Route path="/preferences" element={<Prefs />} />
                </Routes>

              </div>
            )}
          </Authenticator>
          </div>

        </BrowserRouter>
      </div>
  </div>
  );
}

export default App;

Log output

console log: ``` errors : Array(1) 0 : data : null errorInfo : null errorType : "Unauthorized" locations : Array(1) 0 : {line: 2, column: 3, sourceName: null} length : 1 [[Prototype]] : Array(0) message : "Not Authorized to access createUser on type Mutation" path : Array(1) 0 : "createUser" ```

aws-exports.js

const awsmobile = { "aws_project_region": "us-east-2", "aws_cognito_identity_pool_id": "us-east-2:", "aws_cognito_region": "us-east-2", "aws_user_poolsid": "us-east-2", "aws_user_pools_web_client_id": "", "oauth": {}, "aws_cognito_username_attributes": [ "EMAIL" ], "aws_cognito_social_providers": [], "aws_cognito_signup_attributes": [ "EMAIL" ], "aws_cognito_mfa_configuration": "OFF", "aws_cognito_mfa_types": [ "SMS" ], "aws_cognito_password_protection_settings": { "passwordPolicyMinLength": 8, "passwordPolicyCharacters": [] }, "aws_cognito_verification_mechanisms": [ "EMAIL" ], "aws_appsync_graphqlEndpoint": "https://jappsync-api.us-east-2.amazonaws.com/graphql", "aws_appsync_region": "us-east-2", "aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS" };



export default awsmobile;

### Manual configuration

_No response_

### Additional configuration

I can't get the above command line to work, but I gave the user group associated with cognito the permissions to read/write to dynamoDB, appsync, and amplify permissions. 

### Mobile Device

_No response_

### Mobile Operating System

_No response_

### Mobile Browser

_No response_

### Mobile Browser Version

_No response_

### Additional information and screenshots

_No response_
cwomack commented 1 year ago

Hello, @ArjunSohur 👋. Thanks for creating this issue and hoping we can get you unblocked! From what I see in the details you provided, the only error you're seeing is the, "Not Authorized to access createUser on type Mutation". Is this correct?

Can you check the network activity and verify if you see the auth token? If you see it there, we'll likely need to see the schema that you're using if you can share that.