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

Unable to exchange Google federated identity id token for Cognito access token #13672

Open david-sunsyte opened 1 month ago

david-sunsyte commented 1 month ago

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

Authentication

Amplify Version

v6

Amplify Categories

auth

Backend

Other

Environment information

``` # Put output below this line System: OS: Windows 11 10.0.22631 CPU: (12) x64 Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz Memory: 3.03 GB / 15.72 GB Binaries: Node: 18.20.4 - C:\Program Files\nodejs\node.EXE npm: 10.2.3 - C:\Program Files\nodejs\npm.CMD Browsers: Edge: Chromium (127.0.2651.74) Internet Explorer: 11.0.22621.3527 npmPackages: @arcgis/core: ^4.28.10 => 4.28.10 @aws-amplify/ui-react: ^6.1.4 => 6.1.4 @aws-amplify/ui-react-internal: undefined () @aws-sdk/client-cognito-identity: ^3.620.0 => 3.620.0 @headlessui/react: 2.0 => 2.0.4 @heroicons/react: ^2.1.3 => 2.1.3 @material-tailwind/react: ^2.1.9 => 2.1.9 @tailwindcss/forms: ^0.5.7 => 0.5.7 @types/react: ^18.2.55 => 18.2.56 @types/react-dom: ^18.2.19 => 18.2.19 @vitejs/plugin-react: ^4.2.1 => 4.2.1 autoprefixer: ^10.4.17 => 10.4.17 aws-amplify: ^6.0.17 => 6.0.17 aws-amplify/adapter-core: undefined () aws-amplify/analytics: undefined () aws-amplify/analytics/kinesis: undefined () aws-amplify/analytics/kinesis-firehose: undefined () aws-amplify/analytics/personalize: undefined () aws-amplify/analytics/pinpoint: undefined () aws-amplify/api: undefined () aws-amplify/api/server: undefined () aws-amplify/auth: undefined () aws-amplify/auth/cognito: undefined () aws-amplify/auth/cognito/server: undefined () aws-amplify/auth/enable-oauth-listener: undefined () aws-amplify/auth/server: undefined () aws-amplify/datastore: undefined () aws-amplify/in-app-messaging: undefined () aws-amplify/in-app-messaging/pinpoint: undefined () aws-amplify/push-notifications: undefined () aws-amplify/push-notifications/pinpoint: undefined () aws-amplify/storage: undefined () aws-amplify/storage/s3: undefined () aws-amplify/storage/s3/server: undefined () aws-amplify/storage/server: undefined () aws-amplify/utils: undefined () eslint: ^8.56.0 => 8.56.0 eslint-plugin-react: ^7.33.2 => 7.33.2 eslint-plugin-react-hooks: ^4.6.0 => 4.6.0 eslint-plugin-react-refresh: ^0.4.5 => 0.4.5 express: ^4.18.2 => 4.18.2 postcss: ^8.4.35 => 8.4.35 react: ^18.2.0 => 18.2.0 react-dom: ^18.2.0 => 18.2.0 react-router-dom: ^6.22.1 => 6.22.1 tailwindcss: ^3.4.3 => 3.4.3 vite: ^5.1.0 => 5.1.3 npmGlobalPackages: @aws-amplify/cli: 12.12.4 http-server: 14.1.1 npm: 10.2.3 pm2: 5.3.1 ```

Describe the bug

I've currently implemented a customCredentialsProvider (as shown here - https://docs.amplify.aws/react/build-a-backend/auth/advanced-workflows/) to federate an a Google identity. I successfully receive the credential from Google. I then created a customTokenProvider (as shown here - https://docs.amplify.aws/react/build-a-backend/auth/advanced-workflows/#custom-token-providers)

My issue is that I don't receive the Cognito tokens JwtBearer auth and using the Cognito user pool as the issuer.

https://cognito-idp.us-east-1.amazonaws.com/{userPoolId}

Expected behavior

After receiving the response.credential from Google, I would expect to be able to exchange the id_token that's returned for valid Cognito tokens so I can access my backend API.

Reproduction steps

Launch google sign in as shown here : https://docs.amplify.aws/react/build-a-backend/auth/advanced-workflows/#google-sign-in-react

Successfully auth with Google

Code Snippet

// Put your code below this line.

import { Amplify } from "aws-amplify";
import {
  fetchAuthSession,
  CredentialsAndIdentityIdProvider,
  CredentialsAndIdentityId,
  GetCredentialsOptions,
  AuthTokens,
} from "aws-amplify/auth";

// Note: This example requires installing `@aws-sdk/client-cognito-identity` to obtain Cognito credentials
// npm i @aws-sdk/client-cognito-identity
import { CognitoIdentity } from "@aws-sdk/client-cognito-identity";

// You can make use of the sdk to get identityId and credentials
const cognitoidentity = new CognitoIdentity({
  region: "us-east-1",
});

// Note: The custom provider class must implement CredentialsAndIdentityIdProvider
class CustomCredentialsProvider implements CredentialsAndIdentityIdProvider {
  // Example class member that holds the login information
  federatedLogin?: {
    domain: string;
    token: string;
  };

  // Custom method to load the federated login information
  loadFederatedLogin(login?: typeof this.federatedLogin) {
    // You may also persist this by caching if needed
    this.federatedLogin = login;
  }

  async getCredentialsAndIdentityId(
    getCredentialsOptions: GetCredentialsOptions
  ): Promise<CredentialsAndIdentityId | undefined> {
    try {
      // You can add in some validation to check if the token is available before proceeding
      // You can also refresh the token if it's expired before proceeding
      const getIdResult = await cognitoidentity.getId({
        // Get the identityPoolId from config
        IdentityPoolId: "<identity-pool-id>",
        Logins: { [this.federatedLogin.domain]: this.federatedLogin.token },
      });

      const cognitoCredentialsResult =
        await cognitoidentity.getCredentialsForIdentity({
          IdentityId: getIdResult.IdentityId,
          Logins: { [this.federatedLogin.domain]: this.federatedLogin.token },
        });

      const credentials: CredentialsAndIdentityId = {
        credentials: {
          accessKeyId: cognitoCredentialsResult.Credentials?.AccessKeyId,
          secretAccessKey: cognitoCredentialsResult.Credentials?.SecretKey,
          sessionToken: cognitoCredentialsResult.Credentials?.SessionToken,
          expiration: cognitoCredentialsResult.Credentials?.Expiration,
        },
        identityId: getIdResult.IdentityId,
      };
      return credentials;
    } catch (e) {
      console.log("Error getting credentials: ", e);
    }
  }
  // Implement this to clear any cached credentials and identityId. This can be called when signing out of the federation service.
  clearCredentialsAndIdentityId(): void {}
}

const customCredentialsProvider = new CustomCredentialsProvider();
export { customCredentialsProvider };

Log output

``` // Put your logs below this line ```

aws-exports.js

export const awsExports = {
  Cognito: {
    region: "region",
    userPoolId: "",
    userPoolClientId: "",
    identityPoolId: "",
    loginWith: {
      oauth: {
        domain: "user-pool-domain",
        scopes: ["email", "profile", "openid", "aws.cognito.signin.user.admin"],
        redirectSignIn: [
          "http://localhost"
        ],
        redirectSignOut: ["http://localhost"],
        responseType: "code", // or 'token', note that REFRESH token will only be generated when the responseType is code
      },
    },
  },
};

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

cwomack commented 1 month ago

Hello, @david-sunsyte 👋. Trying to understand how to reproduce this figure out the root cause of what's blocking you here. Can you clarify where the tokens are coming from in your implementation of your customer token provider? Are you using TokenProvider somewhere else in the code that's not referenced in this issue (example from docs)?

Also, is Google the IDP for your User Pool? I'm not understanding the need for the Custom Token Provider aspect if you're looking to just use Google as a federated IDP. Any additional context/clarity on the above items would be appreciated. Thanks!

david-sunsyte commented 1 month ago

Hi @cwomack , thank you so much for your reply. The tokens are coming from the callback from the google initialize function shown below:

window.google.accounts.id.initialize({
        client_id: process.env.GOOGLE_CLIENT_ID,
        callback: (response: any) => {
          customCredentialsProvider.loadFederatedLogin({
            domain: 'accounts.google.com',
            token: response.credential,
          });
          const fetchSessionResult = await fetchAuthSession(); // will return the credentials
          console.log('fetchSessionResult: ', fetchSessionResult);
        },
      });

Granted, I did this as a long shot and in the absence of being able to identify my issue. The overall problem I'm trying to solve is for is to authenticate via Google and retrieve a token that can then be exchanged for Cognito tokens (preferrably id_token) so I can access my backend API.

I may not need to use the custom token provider to achieve this, I just don't know which way to go with this.

israx commented 1 month ago

Hello @david-sunsyte.

You might want to use a custom token provider to make authenticated calls to AppSync— assuming AppSync was configured to accept OIDC tokens before.

And you might want to use a custom credentials provider to generate AWS Credentials without using Cognito User Pools, hence avoiding creating Cognito tokens and being unable to call the cognito-idp endpoints.

If you want to create Cognito tokens, use Google as your social provider, and also create AWS Credentials. You need to configure your User Pool with Google, configure Amplify, and then call the signInWithRedirect API to authenticate to Google. Once you have this setup, you don't need to setup a custom token and credentials provider.

-docs to configure and use social providers with Amplify: https://docs.amplify.aws/react/build-a-backend/auth/concepts/external-identity-providers/