question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Having ?code= on the query string seems to always be caught by some Cognito code

See original GitHub issue

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

Authentication

Amplify Categories

auth

Environment information

# Put output below this line
  System:
    OS: Windows 10 10.0.19041
    CPU: (16) x64 Intel(R) Core(TM) i7-10875H CPU @ 2.30GHz
    Memory: 11.16 GB / 31.84 GB
  Binaries:
    Node: 14.16.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.10 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 6.14.11 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Chrome: 94.0.4606.81
    Edge: Spartan (44.19041.1266.0), Chromium (95.0.1020.44)
    Internet Explorer: 11.0.19041.1202
  npmPackages:
    @asseinfo/react-kanban: ^2.1.0 => 2.1.0
    @aws-amplify/ui-react: ^1.2.21 => 1.2.21
    @brainhubeu/react-file-input: 0.7.21 => 0.7.21
    @headlessui/react: ^1.2.0 => 1.2.0
    @ideal-postcodes/api-typings: ^2.0.1 => 2.0.1
    @ideal-postcodes/core-browser: ^1.5.2 => 1.5.2
    @sentry/react: ^6.3.0 => 6.3.0
    @sentry/tracing: ^6.2.4 => 6.2.4
    @tailwindcss/aspect-ratio: ^0.2.0 => 0.2.0
    @tailwindcss/forms: ^0.2.1 => 0.2.1
    @tailwindcss/typography: ^0.4.0 => 0.4.0 (0.2.0)
    @tailwindcss/ui: ^0.6.2 => 0.6.2
    @tailwindui/react: ^0.1.1 => 0.1.1
    @testing-library/jest-dom: ^4.2.4 => 4.2.4
    @testing-library/react: ^9.3.2 => 9.5.0
    @testing-library/user-event: ^7.1.2 => 7.2.1
    @types/javascript-time-ago: ^2.0.2 => 2.0.2
    @types/jest: ^27.0.2 => 27.0.2
    @types/node: ^14.14.33 => 14.14.33 (13.9.2, 14.11.2)
    @types/reach__router: ^1.3.7 => 1.3.7
    @types/react: ^16.14.5 => 16.14.5 (16.9.49)
    @types/react-dom: ^16.9.11 => 16.9.11 (16.9.8)
    @types/yup: ^0.29.11 => 0.29.11
    @typescript-eslint/eslint-plugin: ^4.17.0 => 4.17.0 (4.2.0)
    @typescript-eslint/parser: ^4.17.0 => 4.17.0 (4.2.0)
    apollo-boost: ^0.4.9 => 0.4.9
    autoprefixer: ^10.2.5 => 10.2.5 (7.2.6, 6.7.7, 9.7.4)
    aws: ^0.0.3-2 => 0.0.3-2
    aws-amplify: ^4.3.3 => 4.3.3
    aws-amplify-react: ^5.1.4 => 5.1.4
    aws-sdk: ^2.860.0 => 2.860.0
    axios: ^0.21.1 => 0.21.1 (0.21.4, 0.19.0)
    babel-eslint: ^10.1.0 => 10.1.0
    chart.js: ^3.2.0 => 3.2.0
    chart.js-auto:  undefined ()
    chart.js-helpers:  undefined ()
    eslint: ^7.21.0 => 7.21.0 (7.32.0)
    eslint-config-react-app: ^6.0.0 => 6.0.0
    file-saver: ^2.0.5 => 2.0.5
    formik: ^2.2.6 => 2.2.6
    google-map-react: ^2.1.10 => 2.1.10
    graphiql: ^1.4.0 => 1.4.0
    graphql: 15.5.1 => 15.5.1 (14.0.0, 14.7.0)
    graphql-auto-transformer: ^1.3.1 => 1.3.1
    graphql-tag: ^2.12.5 => 2.12.5 (2.11.0)
    history: 5 => 5.0.0 (4.10.1)
    postcode: ^5.1.0 => 5.1.0
    postcss: ^8.2.8 => 8.2.8 (6.0.23, 7.0.27, 5.2.18, 8.2.4, 8.3.11, 7.0.39, 7.0.36)
    react: ^17.0.2 => 17.0.2 (16.14.0)
    react-apollo: ^3.1.5 => 3.1.5
    react-chartjs-2: ^3.0.2 => 3.0.2
    react-datepicker: ^3.4.1 => 3.4.1
    react-dom: ^16.14.0 => 16.14.0
    react-icons: ^4.2.0 => 4.2.0
    react-pagination-hook: ^0.0.1 => 0.0.1
    react-router-dom: ^6.0.0-beta.8 => 6.0.0-beta.8 (4.3.1)
    react-scripts: 4.0.3 => 4.0.3
    react-tag-autocomplete: ^6.1.0 => 6.1.0
    react-three-state-checkbox: ^1.3.4 => 1.3.4
    react-time-ago: ^6.2.2 => 6.2.2
    react-use: ^15.3.8 => 15.3.8
    rimraf: ^3.0.2 => 3.0.2 (2.7.1)
    smoothscroll-polyfill: ^0.4.4 => 0.4.4
    tailwind: ^4.0.0 => 4.0.0
    tailwindcss: ^2.0.3 => 2.0.3
    typescript: ^4.2.3 => 4.2.3
    vest: ^2.2.3 => 2.2.3
    yup: ^0.29.3 => 0.29.3
  npmGlobalPackages:
    @aws-amplify/cli: 6.4.0
    nodemon: 2.0.7
    ts-node: 9.1.1
    yarn: 1.22.10

Describe the bug

This is an odd one.

We had a callback that we needed for Xero (3rd party accountancy software) authentication which wants to send us a code on the querystring such as /mycallbackurl/?code=123454&secret=56789

Trouble is our App would always redirect to the root url and our code to handle the callback would never fire! After some testing we found that its just the querystring name of “code” that is causing the issue. We suspect that our Cognito Authentication is being too eager and hoovering up anything with “code” in the querystring regardless of the url structure.

To work around it and prove the point I added this to the top of my index.js in the root of the App before anything else can run:

// The xero hack, for some reason if we pass ?code= on the query string it causes a redirect to the root url /
// This breaks the xero callback url. After many hours searching we came up with this hack, we intercept it and rename it on the querystring!
const qs = window.location.search;
console.info("QS", qs);
if (window.location.pathname.indexOf("xero") > -1 && qs.indexOf("code=") > -1) {
  window.location.search = qs.replace("code", "xeroCode");
}
console.info(
  "Updated querystring to avoid the xero code quirk",
  window.location.search
);

Its not pretty, but it renames the code querystring param to something else if we know its for Xero. Then in our xero callback handling code we look for the new value. This now works! But its damn weird and something we thought would catch out others. I’ll include our UserProvider at the bottom so you can see how we are using it for the Authentication if it helps. The above code though is the only bit of “quirky” stuff we have in there.

Expected behavior

I’d have thought that the Cognito code would only kick in on whatever callback url it wants to you (yes this could even be the root url) rather than on everything. Seems a bit too much of a over-reach of the querystring for me.

Reproduction steps

This is hard to unpick, if you have an authed site already then you could wrap our UserProvider around it and see if you can get that running.

Then navigate to a sub folder (ie /myfolder/) and try adding ?code=1234 to the end. The site should redirect/reload at the root url and your querystring will have disappeared. Yet if you go back to the same folder but add ?code2=1234 to the end it won’t redirect and will instead just render and leave the querystring on the url.

Code Snippet

// Put your code below this line.
import { CognitoUser } from "@aws-amplify/auth";
import { CognitoHostedUIIdentityProvider } from "@aws-amplify/auth/lib/types";
import { CognitoUserSession } from "amazon-cognito-identity-js";
import Amplify, { Auth } from "aws-amplify";
import React, { useCallback } from "react";


export type UserContextType = {
  /** This is the user model from a successful Login */
  cognitoUser: CognitoUser;
  /** The global user ID used for BOTH cognito pool and the DynamoDB User.id field */
  globalUserID: string;
  isCognitoLoading: boolean;

  cognitoUserSession: CognitoUserSession | null;

  login: Function;
  googleLogin: Function;
  logout: Function;
  isAdmin: Function;
};

// Create a context that will hold the values that we are going to expose to our components.
export const UserContext = React.createContext<UserContextType | undefined>(
  undefined
);

// Create a "controller" component that will calculate all the data that we need to give to our components bellow via the `UserContext.Provider` component.
// This is where the Amplify will be mapped to a different interface, the one that we are going to expose to the rest of the app.
export const UserProvider = ({ children }) => {
  const [cognitoUser, setCognitoUser] = React.useState(null) as
    | CognitoUser
    | any;
  const [cognitoUserSession, setCognitoUserSession] =
    React.useState<CognitoUserSession | null>(null);
  const [isCognitoLoading, setIsCognitoLoading] = React.useState<boolean>(true);

  // console.count("UserProvider");

  React.useEffect(() => {
    // Configure the keys needed for the Auth module
    Auth.configure({});

    // attempt to fetch the info of the user that was already logged in
    Auth.currentAuthenticatedUser()
      .then((user) => {
        setCognitoUser(user);
        Auth.currentSession().then((session) => {
          setCognitoUserSession(session);
        });
      })
      .catch(() => {
        setCognitoUser(null);
      })
      .finally(() => setIsCognitoLoading(false));
  }, [setCognitoUser]);

  // We make sure to handle the user update here, but return the resolve value in order for our components to be  able to chain additional `.then()` logic.
  // Additionally, we `.catch` the error and "enhance it" by providing a message that our React components can use.
  const login = useCallback(
    (usernameOrEmail: string, password: string) => {
      Auth.signIn(usernameOrEmail, password)
        .then((cognitoUser) => {
          setCognitoUser(cognitoUser);
          Auth.currentSession().then((session) => {
            setCognitoUserSession(session);
          });
          return cognitoUser;
        })
        .catch((err) => {
          if (err.code === "UserNotFoundException") {
            err.message = "Invalid username or password";
          }
          throw err;
        })
        .finally(() => setIsCognitoLoading(false));
    },
    [setCognitoUser]
  );

  const googleLogin = useCallback(() => {
    Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google })
      .then((cognitoUser) => {
        setCognitoUser(cognitoUser);
        Auth.currentSession().then((session) => {
          setCognitoUserSession(session);
        });
        return cognitoUser;
      })
      .catch((err) => {
        if (err.code === "UserNotFoundException") {
          err.message = "Invalid username or password";
        }
        throw err;
      })
      .finally(() => setIsCognitoLoading(false));
  }, [setCognitoUser]);

  const logout = useCallback(() => {
    Auth.signOut()
      .then((data) => {
        setCognitoUser(null);
        setCognitoUserSession(null);
        // We should clear out the DataStore incase we have different users using the same machine
        Amplify.DataStore.clear();
        return data;
      })
      .finally(() => setIsCognitoLoading(false));
  }, [setCognitoUser, setCognitoUserSession, setIsCognitoLoading]);

  const isAdmin = useCallback(() => {
    let isAdmin = false;
    if (cognitoUserSession) {
      const groups =
        cognitoUserSession.getAccessToken().payload["cognito:groups"];
      if (groups.includes("admin")) {
        isAdmin = true;
      }
    }

    return isAdmin;
  }, [cognitoUserSession]);

  /** Helper field to wrap up the logic for getting the current users ID easier */
  const globalUserID: string = cognitoUser?.attributes?.sub ?? "";

  // Make sure to not force a re-render on the components that are reading these values, unless the `user` value has changed.
  // This is an optimisation that is mostly needed in cases where the parent of the current component re-renders and thus the current component is forced to re-render as well.
  // If it does, we want to make sure to give the `UserContext.Provider` the same value as long as the user data is the same.
  // If you have multiple other "controller" components or Providers above this component, then this will be a performance booster.
  const values = React.useMemo(
    () =>
      ({
        cognitoUser,
        globalUserID,
        isCognitoLoading,
        cognitoUserSession,
        login,
        googleLogin,
        logout,
        isAdmin,
      } as UserContextType),
    [
      cognitoUser,
      globalUserID,
      isCognitoLoading,
      cognitoUserSession,
      googleLogin,
      isAdmin,
      logout,
      login,
    ]
  );

  // Finally, return the interface that we want to expose to our other components
  return <UserContext.Provider value={values}>{children}</UserContext.Provider>;
};

// We also create a simple custom hook to read these values from.
// We want our React components to know as little as possible on how everything is handled
// so we are not only abtracting them from the fact that we are using React's context, but we also skip some imports.
export const useCognitoUser = () => {
  const context = React.useContext(UserContext);

  if (context === undefined) {
    throw new Error(
      "`useCognitoUser` hook must be used within a `UserProvider` component"
    );
  }
  return context;
};

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

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:7
  • Comments:26 (8 by maintainers)

github_iconTop GitHub Comments

3reactions
abdallahshaban557commented, Oct 11, 2022

Hi @timharsch - I am asking our team to check what needs to be done to fix this further. We will get back to you when we have more information. FYI @nadetastic @tannerabread @cwomack

3reactions
abdallahshaban557commented, Sep 2, 2022

Just sent a followup to the PR creator to see if they can make updates based on recommendations from our team.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Common Errors - Amazon Cognito Sync - AWS Documentation
The query string contains a syntax error. HTTP Status Code: 404. MissingAction. The request is missing an action or a required parameter. HTTP...
Read more >
Find Answers to AWS Questions about Amazon Cognito
Browse through Amazon Cognito questions or showcase your expertise by answering unanswered questions.
Read more >
Signing up and confirming user accounts - Amazon Cognito
When users enter the confirmation code, they automatically verify email or phone. A user account can be in any of the following states:...
Read more >
Unable to verify secret hash for client in Amazon Cognito ...
I tried all possible codes for authenticating user in cognito userpools. But I always get error saying "Error: Unable to verify secret hash...
Read more >
Implement the OAuth 2.0 Authorization Code with PKCE Flow
This tutorial shows you how to migrate from the OAuth 2.0 Implicit flow to the more secure Authorization Code with PKCE flow.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found