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

HubCallback not firing "customOAuthState" event with Google federated sign in #11612

Closed josh-roboto closed 6 months ago

josh-roboto commented 1 year ago

Before opening, please confirm:

JavaScript Framework

React, Next.js

Amplify APIs

Authentication

Amplify Categories

auth

Environment information

``` # Put output below this line System: OS: macOS 13.4.1 CPU: (10) arm64 Apple M1 Max Memory: 2.25 GB / 32.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 18.16.0 - /usr/local/bin/node Yarn: 1.22.19 - ~/.yarn/bin/yarn npm: 9.4.0 - /opt/homebrew/bin/npm Browsers: Chrome: 114.0.5735.198 Edge: 114.0.1823.51 Firefox: 111.0.1 Safari: 16.5.1 npmPackages: @ampproject/toolbox-optimizer: undefined () @babel/core: undefined () @babel/runtime: 7.15.4 @cypress/angular: 0.0.0-development @cypress/mount-utils: 0.0.0-development @cypress/react: 0.0.0-development @cypress/react18: 0.0.0-development @cypress/svelte: 0.0.0-development @cypress/vue: 0.0.0-development @cypress/vue2: 0.0.0-development @edge-runtime/cookies: 3.0.6 @edge-runtime/primitives: 2.1.2 @emotion/react: 11.11.0 => 11.11.0 @emotion/styled: 11.11.0 => 11.11.0 @fontsource/roboto: 4.5.8 => 4.5.8 @hapi/accept: undefined () @mui/icons-material: 5.11.16 => 5.11.16 @mui/material: 5.13.0 => 5.13.0 @napi-rs/triples: undefined () @next/font: undefined () @next/react-dev-overlay: undefined () @opentelemetry/api: undefined () @segment/ajv-human-errors: undefined () @tanstack/react-query: 4.29.5 => 4.29.5 @testing-library/jest-dom: 5.16.5 => 5.16.5 @testing-library/react: 14.0.0 => 14.0.0 @testing-library/user-event: 14.0.0 => 14.0.0 @trivago/prettier-plugin-sort-imports: 4.1.1 => 4.1.1 @types/color-hash: 1.0.2 => 1.0.2 @types/jest: 29.5.2 => 29.5.2 @types/mixpanel-browser: 2.38.1 => 2.38.1 @types/node: 20.2.1 => 20.2.1 (14.18.47) @types/react: 18.2.6 => 18.2.6 @types/react-dom: 18.2.4 => 18.2.4 @types/react-resizable: 3.0.4 => 3.0.4 @typescript-eslint/eslint-plugin: ^5.59.7 => 5.59.7 @typescript-eslint/parser: ^5.59.7 => 5.59.7 @vercel/nft: undefined () @vercel/og: undefined () acorn: undefined () amphtml-validator: undefined () anser: undefined () arg: undefined () assert: undefined () async-retry: undefined () async-sema: undefined () aws-amplify: 5.2.5 => 5.2.5 babel-packages: undefined () browserify-zlib: undefined () browserslist: undefined () buffer: undefined () bytes: undefined () chalk: undefined () ci-info: undefined () cli-select: undefined () client-only: 0.0.1 color-hash: 2.0.2 => 2.0.2 comment-json: undefined () compression: undefined () conf: undefined () constants-browserify: undefined () content-disposition: undefined () content-type: undefined () cookie: undefined () cross-spawn: undefined () crypto-browserify: undefined () css.escape: undefined () cypress: 12.12.0 => 12.12.0 data-uri-to-buffer: undefined () debug: undefined () devalue: undefined () domain-browser: undefined () edge-runtime: undefined () eslint: ^8.40.0 => 8.40.0 eslint-config-next: 13.4.1 => 13.4.1 eslint-plugin-cypress: 2.13.3 => 2.13.3 eslint-plugin-jest: 27.2.1 => 27.2.1 eslint-plugin-react: 7.32.2 => 7.32.2 eslint-plugin-react-hooks: 4.6.0 => 4.6.0 events: undefined () find-cache-dir: undefined () find-up: undefined () fresh: undefined () get-orientation: undefined () glob: undefined () gzip-size: undefined () http-proxy: undefined () http-proxy-agent: undefined () https-browserify: undefined () https-proxy-agent: undefined () icss-utils: undefined () ignore-loader: undefined () image-size: undefined () is-animated: undefined () is-docker: undefined () is-wsl: undefined () jest: 29.5.0 => 29.5.0 jest-environment-jsdom: 29.5.0 => 29.5.0 jest-worker: undefined () json5: undefined () jsonwebtoken: undefined () jwt-decode: 3.1.2 => 3.1.2 lint-staged: 13.2.2 => 13.2.2 loader-runner: undefined () loader-utils: undefined () lodash.curry: undefined () lru-cache: undefined () micromatch: undefined () mini-css-extract-plugin: undefined () mixpanel-browser: 2.47.0 => 2.47.0 nanoid: undefined () native-url: undefined () neo-async: undefined () next: 13.4.2 => 13.4.2 node-fetch: undefined () node-html-parser: undefined () ora: undefined () os-browserify: undefined () p-limit: undefined () path-browserify: undefined () platform: undefined () postcss-flexbugs-fixes: undefined () postcss-modules-extract-imports: undefined () postcss-modules-local-by-default: undefined () postcss-modules-scope: undefined () postcss-modules-values: undefined () postcss-preset-env: undefined () postcss-safe-parser: undefined () postcss-scss: undefined () postcss-value-parser: undefined () prettier: 2.8.8 => 2.8.8 process: undefined () punycode: undefined () querystring-es3: undefined () raw-body: undefined () react: 18.2.0 => 18.2.0 react-builtin: undefined () react-dom: 18.2.0 => 18.2.0 react-dom-builtin: undefined () react-dom-experimental-builtin: undefined () react-experimental-builtin: undefined () react-is: 18.2.0 react-refresh: 0.12.0 react-resizable: 3.0.5 => 3.0.5 react-server-dom-webpack-builtin: undefined () react-server-dom-webpack-experimental-builtin: undefined () recoil: 0.7.7 => 0.7.7 regenerator-runtime: 0.13.4 sass-loader: undefined () scheduler-builtin: undefined () scheduler-experimental-builtin: undefined () schema-utils: undefined () semver: undefined () send: undefined () server-only: 0.0.1 setimmediate: undefined () shell-quote: undefined () source-map: undefined () stacktrace-parser: undefined () stream-browserify: undefined () stream-http: undefined () string-hash: undefined () string_decoder: undefined () strip-ansi: undefined () tar: undefined () terser: undefined () text-table: undefined () timers-browserify: undefined () tty-browserify: undefined () typescript: ^5.0.4 => 5.0.4 ua-parser-js: undefined () undici: undefined () unistore: undefined () util: undefined () vm-browserify: undefined () watchpack: undefined () web-vitals: undefined () webpack: undefined () webpack-sources: undefined () ws: undefined () zod: undefined () npmGlobalPackages: @aws-amplify/cli: 11.0.3 aws-cdk: 2.79.1 create-remix: 1.16.0 create-vite: 4.3.1 eslint: 8.2.0 gatsby-cli: 5.9.0 madge: 6.1.0 n: 9.1.0 npm: 9.4.0 typescript: 4.7.4 typings: 2.1.1 yarn: 1.22.17 ```

Describe the bug

I am calling federatedSignIn, like this:

 Auth.federatedSignIn({
            provider: CognitoHostedUIIdentityProvider.Google,
            customState: JSON.stringify({ inviteId: inviteId }),
})

Once the user completes the GoogleOAuth process, they are successfully redirected back to my app. I see the "state" URL parameter once the user has been redirected to my app. This state URL param can be parsed by breaking it into two parts separated by a "-", and the second part contains the string above. So I know the state is getting passed back to the front end. I am manually decoding the URL param as a workaround for now.

However, the HubCallback listener is not firing the "customOAuthState" event.

Expected behavior

I would expect the "customOAuthState" event to fire and it should contain a JSON String containing the inviteId.

Reproduction steps

  1. Call Auth.federatedSignIn
  2. Complete the Google OAuth flow on Google's server
  3. Get redirected back to my app
  4. Custom state is not fired

Code Snippet

// My listener looks like this: 

 const listener: HubCallback = async (authListener) => {
      switch (authListener.payload.event) {
        case "customOAuthState": {
          LoggerService.log("custom state returned from CognitoHosted UI")
          LoggerService.log(authListener.payload.data)
          break
        }
        case "customState_failure":
          LoggerService.error("custom state failure")
          // note I never see anything here
          break
      }
    }

This listener is placed inside useEffect, and I return a function that removes the listener on cleanup.

Log output

NA

aws-exports.js

NA, this is a front end app only

Manual configuration

Amplify.configure({
  aws_cognito_region: process.env.NEXT_PUBLIC_AWS_COGNITO_REGION,
  aws_user_pools_id: process.env.NEXT_PUBLIC_AWS_USER_POOLS_ID,
  aws_user_pools_web_client_id:
    process.env.NEXT_PUBLIC_AWS_USER_POOLS_WEB_CLIENT_ID,
  oauth: {
    redirectSignIn: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_SIGN_IN,
    redirectSignOut: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_SIGN_OUT,
    domain: process.env.NEXT_PUBLIC_OAUTH_COGNITO_DOMAIN,
    scope: ["email", "profile", "openid"],
    responseType: "code",
  },
})

Additional configuration

All of the online documentation for configuring Google OAuth has been followed. If you have specific concerns please let me know, but I don't want to post screenshots from our Auth config here.

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

nadetastic commented 1 year ago

Hi @josh-roboto thank you for opening this issue. I quickly tested this out with "aws-amplify":"5.3.3" and im getting the expected result where customOAuthState event is triggered when I federate with Google.

Im curios with one thing with your setup - specifically, not sure what LoggerService is referring to. When I simply console log the event, I see that it works. Could you clarify what LoggerService is? Additionally, another good way to see what events are being fired is to add Amplify.Logger.LOG_LEVEL = 'DEBUG' right before you configure with Amplify.configure(awsExports).

Also here is an example of what I tested with:


Amplify.Logger.LOG_LEVEL = 'DEBUG'
Amplify.configure(awsExports);

...

useEffect(() => {
    const listener = Hub.listen('auth', ({ payload }) => {
      switch (payload.event) {
        case 'customOAuthState':
          console.log('customOAuthState')
          console.log(payload) // contains the state value under "payload.data"
          break
        case 'customState_failure':
          console.log('customState_failure')
          break
      }
    })

    return () => {
      listener() // stop listening
    }
  },[]);
josh-roboto commented 1 year ago

HI @nadetastic ,

Thanks for getting back to me. LoggerService is just a wrapper around console.log - it's a service that does not log anything to the console in production. We don't allow console.log to be committed to main, so generally when I log stuff I use the LoggerService. I tried using console.log too, but that still doesn't work. I've also run the app with a debugger attached and a breakpoint set, but the breakpoint is never triggered.

I turned on the Amplify.Logger as you suggested, and I see that Hub is indeed dispatching the event to the auth channel, like this:

{event: 'customOAuthState', data: '{"inviteId": "helloWorld"}', message: 'State for user 086857a0-6fc2-4ec5-978c-d15e498f1509'}

My listener set up is the same as yours. One thing I noticed in the Amplify.Logger logs:

Unable to get the user data because the aws.cognito.signin.user.admin is not in the scopes of the access token

From line 1758 in Auth.ts. I'm not sure if this is related or not, I haven't dug too much through the Amplify source code.

nadetastic commented 1 year ago

Got it @josh-roboto thanks for the context on LoggerService. A few things:

josh-roboto commented 1 year ago

Hi @nadetastic ,

I think I just copied this from the Amplify docs here: https://docs.amplify.aws/lib/auth/auth-events/q/platform/js/

I'm using TypeScript so it looks a bit different, but I think it should still be the same thing. My code inside useEffect looks like this:

const listener: HubCallback = async (authListener) => {
      switch (authListener.payload.event) {
        case "customOAuthState": {
          LoggerService.log("custom state returned from CognitoHosted UI")
          LoggerService.log(authListener.payload.data)
          break
        }
    }
}

const removeListener = Hub.listen("auth", listener)

return () => {
      removeListener()
}

I'm listening to more events than just the customOAuth one, but other than it's exactly the same.

I tried setting up the listener as described in bullet point number 2, but still no luck. I'm not convinced about the log either, I thought it might help though.

nadetastic commented 1 year ago

@josh-roboto Thanks for the clarification - however I'm still not able to reproduce this. Testing with the latest version of aws-amplify and next (both app and pages directories) im able to get the customOAuthState event along with the payload.

Could you confirm what versions of aws-amplify as well as nextjs you are using? Also if possible, could you share a minimal copy of you app that's having this issue as an alternative?

josh-roboto commented 1 year ago

@nadetastic I am using aws-amplify 5.2.5, and NextJS 13.4.2 .

I will get to work on creating the minimal copy. I've got some other Jira tickets here that are higher priority, but hopefully should have something for you by late tonight or end of day tomorrow.

josh-roboto commented 1 year ago

@nadetastic I've got a minimal example here:

https://github.com/QO-Development/CustomStateRepro

You're going to need a .env file to make this work. While technically these values aren't secrets, I'd rather not post them here. Any chance I can shoot you an email with the required env variables?

You'll need to navigate to localhost:3000/signin to see the button I've created. The app doesn't do much besides call Auth.federatedSignIn(). You should be able to see me logging the custom state via the parsed URL to the console. But I can't see any Hub event for customOAuthState being triggered.

josh-roboto commented 1 year ago

Hi @nadetastic , any update here? Thanks very much.

nadetastic commented 1 year ago

Hi @josh-roboto apologies for the delay on this. Were you able to resolve this? If not, could you re-share the minimal repo so I can take a look at it?

josh-roboto commented 1 year ago

Hi @nadetastic , I made it public again. Take a look.

I was not able to resolve it. I'm still using the workaround of parsing the URL myself, which seems to be working quite reliably.

One more thought I've had in the interim: Our backend team is setting up and configuring the Cognito instance through CDK. We're really only using Amplify as a frontend hosting service and using the Amplify SDK to help with auth. Is there something we might need to configure in Cognito to make it play nice with the Amplify library? All of the other Auth functionality I've leveraged in the SDK works great, e.g. signUp, signIn, forgotPasswordSubmit, etc.

nadetastic commented 1 year ago

Hi @josh-roboto I've taken a look at this and Im able to get the custom state without any issues when using my own backend that I deployed with the Amplify CLI. I did need to "disable" some of the logic where you were using the api service as I didn't have much context as to how that looked like, but didn't seem necessary.

Logged response logged response Hub logic hub logic

Using CDK to deploy your backend shouldn't be an issue, at least im not aware of any problem that may be stemming from that. Since I used an existing cognito instance that I deployed with amplify, I'm also testing to see if there's a difference in how CDK and Amplify may be creating cognito resources.

One thing i would like to confirm however, is if you are also creating a Federated Identity pool with your CDK stack and connecting it to your Userpool? That could be a potential issue in case you are not.

josh-roboto commented 1 year ago

Hi @nadetastic , we do not have a federated identity pool within our CDK stack. If we need access to any AWS resources we issue some pretty tightly scoped creds via STS. Could you please tell me what the issue might be if we don't have an identity pool? Thanks!

nadetastic commented 7 months ago

Hi @josh-roboto following up here - we recently released a new version of aws-amplify and im curious to see if you have since encountered this issue with this latest version (or you were able to get unblocked). I haven't been able to identify any known issues related to this in a case where you don't have an identity pool.

josh-roboto commented 7 months ago

Hi @nadetastic , I've upgraded to Amplify v6. We have a workaround in place with no plans to look at this any time soon. Our sprint calendars are pretty full. Basically what I'm doing for now is parsing the state param to get the encoded stuff I need, like this:

        // Google + Amplify pass back a hex encoded state param, so we need to manually parse it
        const manualParams = new URLSearchParams(
          new URL(`${pathname}${search}${hash}`, "http://doesntmatter").search,
        );
        const state = manualParams.get("state");

        if (state) {
          const encodedInvite = state.split("-")[1];
          if (encodedInvite) {
            const decodedInvite = Buffer.from(encodedInvite, "hex").toString(
              "utf8",
            );
            inviteId = (JSON.parse(decodedInvite) as { inviteId: string })
              .inviteId;
          }
        }
nadetastic commented 6 months ago

Sounds good, I'll go ahead and close this out for now, when you have a chance to circle back feel free to comment or open a new issue so I can gather additional details about your environment.