Having ?code= on the query string seems to always be caught by some Cognito code
See original GitHub issueBefore opening, please confirm:
- I have searched for duplicate or closed issues and discussions.
- I have read the guide for submitting bug reports.
- I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue.
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:
- Created 2 years ago
- Reactions:7
- Comments:26 (8 by maintainers)
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
Just sent a followup to the PR creator to see if they can make updates based on recommendations from our team.