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.44k stars 2.13k forks source link

[v6] Intercept authentication Cognito requests with custom headers #13197

Open Abdulmalick-Dimnang opened 8 months ago

Abdulmalick-Dimnang commented 8 months ago

Is this related to a new or existing framework?

React Native

Is this related to a new or existing API?

Authentication

Is this related to another service?

No response

Describe the feature you'd like to request

Feature: Able to intercept authentication Cognito requests such as (signIn, signUp, signOut etc..) with custom headers

We know there's an option to intercept REST & GraphQL but we're looking forward to intercept Authentication requests. We believe we have searched the documentation but sadly it's not possible atm with V6

Our current use case is intercept custom headers to be able to send app check and WAF tokens to be able to validated in AWS cloudfront and we're migrating aws-amplify from V4 to V6 and we were able to intercept requests headers with v4 upon patching as follows with add "headersInterceptor" as a property to intercept with

export const initAuth = () =>
  Auth.configure({
    region: 'example',
    userPoolId: xxxx,
    userPoolWebClientId: xxxx,
    endpoint: xxxx,
    headersInterceptor: async (handler: (extraHeaders: Record<string, string>) => Promise<void>) => {
      await handler({
        'X-Firebase-AppCheck': xxx
        'X-Aws-Waf-Token': xxx,
      });
    },
  });

For example, "signOut" authentication will run as follows amplify v4

If there's a possible solution we would appreciate if someone can share it 🙏🏼

Describe the solution you'd like

It would be great to intercept authentication Cognito requests with custom headers while init Amplify as follows where "headers" would be an option to add your own custom headers

Amplify.configure({
    Auth: {
      Cognito: {
        userPoolId: xxxx,
        userPoolClientId: xxxx,
        userPoolEndpoint: xxxx,
        loginWith: {
          username: true,
          email: false,
          phone: true,
        },
        headers: async () => {
          return {
            'X-Firebase-AppCheck': xxxx,
            'X-Aws-Waf-Token': xxxx,
          };
        },
      },
    },
  });

Describe alternatives you've considered

We haven't searched for any alternative except for keep using amplify v4 since we managed to patch aws-amplify & amazon-cognito-identity-js but we're looking for a clean solution where this option can be provided out of the box

Additional context

No response

Is this something that you'd be interested in working on?

cwomack commented 8 months ago

Hello, @Abdulmalick-Dimnang 👋. I've marked this as a feature request and it appears to have some aspects of version parity from v4 to v6 as you found. Let me review this with the team internally, and I'll follow up with any questions from here.

cwomack commented 8 months ago

@Abdulmalick-Dimnang, are you able to share some more details on how you were achieving this in v4 with Amplify? I don't see where we documented this anywhere in the v4 or v5 code base.

Abdulmalick-Dimnang commented 8 months ago

hi @cwomack

we had to patch "@aws-amplify+auth+4.6.6.patch" as follows

diff --git a/node_modules/@aws-amplify/auth/lib-esm/Auth.js b/node_modules/@aws-amplify/auth/lib-esm/Auth.js
index 8fffcfa..57054b4 100644
--- a/node_modules/@aws-amplify/auth/lib-esm/Auth.js
+++ b/node_modules/@aws-amplify/auth/lib-esm/Auth.js
@@ -152,7 +152,7 @@ var AuthClass = /** @class */ (function () {
         logger.debug('configure Auth');
         var conf = Object.assign({}, this._config, Parser.parseMobilehubConfig(config).Auth, config);
         this._config = conf;
-        var _a = this._config, userPoolId = _a.userPoolId, userPoolWebClientId = _a.userPoolWebClientId, cookieStorage = _a.cookieStorage, oauth = _a.oauth, region = _a.region, identityPoolId = _a.identityPoolId, mandatorySignIn = _a.mandatorySignIn, refreshHandlers = _a.refreshHandlers, identityPoolRegion = _a.identityPoolRegion, clientMetadata = _a.clientMetadata, endpoint = _a.endpoint;
+        var _a = this._config, userPoolId = _a.userPoolId, userPoolWebClientId = _a.userPoolWebClientId, cookieStorage = _a.cookieStorage, oauth = _a.oauth, region = _a.region, identityPoolId = _a.identityPoolId, mandatorySignIn = _a.mandatorySignIn, refreshHandlers = _a.refreshHandlers, identityPoolRegion = _a.identityPoolRegion, clientMetadata = _a.clientMetadata, endpoint = _a.endpoint, headersInterceptor = _a.headersInterceptor;
         if (!this._config.storage) {
             // backward compatability
             if (cookieStorage)
@@ -181,7 +181,7 @@ var AuthClass = /** @class */ (function () {
                 endpoint: endpoint,
             };
             userPoolData.Storage = this._storage;
-            this.userPool = new CognitoUserPool(userPoolData, this.wrapRefreshSessionCallback);
+            this.userPool = new CognitoUserPool(userPoolData, this.wrapRefreshSessionCallback, headersInterceptor);
         }
         this.Credentials.configure({
             mandatorySignIn: mandatorySignIn,

and "amazon-cognito-identity-js+5.2.10.patch"

diff --git a/node_modules/amazon-cognito-identity-js/src/Client.js b/node_modules/amazon-cognito-identity-js/src/Client.js
index ebdbb64..42db2f4 100644
--- a/node_modules/amazon-cognito-identity-js/src/Client.js
+++ b/node_modules/amazon-cognito-identity-js/src/Client.js
@@ -19,10 +19,11 @@ export default class Client {
     * @param {string} endpoint endpoint
     * @param {object} fetchOptions options for fetch API (only credentials is supported)
     */
-   constructor(region, endpoint, fetchOptions) {
+   constructor(region, endpoint, fetchOptions, headersInterceptor) {
        this.endpoint = endpoint || `https://cognito-idp.${region}.amazonaws.com/`;
        const { credentials } = fetchOptions || {};
        this.fetchOptions = credentials ? { credentials } : {};
+       this.headersInterceptor = headersInterceptor;
    }

    /**
@@ -71,75 +72,77 @@ export default class Client {
     * @returns {void}
     */
    request(operation, params, callback) {
-       const headers = {
-           'Content-Type': 'application/x-amz-json-1.1',
-           'X-Amz-Target': `AWSCognitoIdentityProviderService.${operation}`,
-           'X-Amz-User-Agent': UserAgent.prototype.userAgent,
-       };
-
-       const options = Object.assign({}, this.fetchOptions, {
-           headers,
-           method: 'POST',
-           mode: 'cors',
-           cache: 'no-cache',
-           body: JSON.stringify(params),
-       });
+       this.headersInterceptor(extraHeaders => {
+           const headers = {
+               'Content-Type': 'application/x-amz-json-1.1',
+               'X-Amz-Target': `AWSCognitoIdentityProviderService.${operation}`,
+               'X-Amz-User-Agent': UserAgent.prototype.userAgent,
+               ...extraHeaders
+           };
+           const options = Object.assign({}, this.fetchOptions, {
+               headers,
+               method: 'POST',
+               mode: 'cors',
+               cache: 'no-cache',
+               body: JSON.stringify(params),
+           });

-       let response;
-       let responseJsonData;
-
-       fetch(this.endpoint, options)
-           .then(
-               resp => {
-                   response = resp;
-                   return resp;
-               },
-               err => {
-                   // If error happens here, the request failed
-                   // if it is TypeError throw network error
-                   if (err instanceof TypeError) {
-                       throw new Error('Network error');
+           let response;
+           let responseJsonData;
+
+           fetch(this.endpoint, options)
+               .then(
+                   resp => {
+                       response = resp;
+                       return resp;
+                   },
+                   err => {
+                       // If error happens here, the request failed
+                       // if it is TypeError throw network error
+                       if (err instanceof TypeError) {
+                           throw new Error('Network error');
+                       }
+                       throw err;
                    }
-                   throw err;
-               }
-           )
-           .then(resp => resp.json().catch(() => ({})))
-           .then(data => {
-               // return parsed body stream
-               if (response.ok) return callback(null, data);
-               responseJsonData = data;
-
-               // Taken from aws-sdk-js/lib/protocol/json.js
-               // eslint-disable-next-line no-underscore-dangle
-               const code = (data.__type || data.code).split('#').pop();
-               const error = new Error(data.message || data.Message || null)
-               error.name = code
-               error.code = code
-               return callback(error);
-           })
-           .catch(err => {
-               // first check if we have a service error
-               if (
-                   response &&
-                   response.headers &&
-                   response.headers.get('x-amzn-errortype')
-               ) {
-                   try {
-                       const code = response.headers.get('x-amzn-errortype').split(':')[0];
-                       const error = new Error(response.status ? response.status.toString() : null)
-                       error.code = code
-                       error.name = code
-                       error.statusCode = response.status
-                       return callback(error);
-                   } catch (ex) {
-                       return callback(err);
+               )
+               .then(resp => resp.json().catch(() => ({})))
+               .then(data => {
+                   // return parsed body stream
+                   if (response.ok) return callback(null, data);
+                   responseJsonData = data;
+
+                   // Taken from aws-sdk-js/lib/protocol/json.js
+                   // eslint-disable-next-line no-underscore-dangle
+                   const code = (data.__type || data.code).split('#').pop();
+                   const error = new Error(data.message || data.Message || null)
+                   error.name = code
+                   error.code = code
+                   return callback(error);
+               })
+               .catch(err => {
+                   // first check if we have a service error
+                   if (
+                       response &&
+                       response.headers &&
+                       response.headers.get('x-amzn-errortype')
+                   ) {
+                       try {
+                           const code = response.headers.get('x-amzn-errortype').split(':')[0];
+                           const error = new Error(response.status ? response.status.toString() : null)
+                           error.code = code
+                           error.name = code
+                           error.statusCode = response.status
+                           return callback(error);
+                       } catch (ex) {
+                           return callback(err);
+                       }
+                       // otherwise check if error is Network error
+                   } else if (err instanceof Error && err.message === 'Network error') {
+                       err.code = 'NetworkError'
                    }
-                   // otherwise check if error is Network error
-               } else if (err instanceof Error && err.message === 'Network error') {
-                   err.code = 'NetworkError'
-               }
-               return callback(err);
-           });
+                   return callback(err);
+               });
+       });
    }
 }

diff --git a/node_modules/amazon-cognito-identity-js/src/CognitoUserPool.js b/node_modules/amazon-cognito-identity-js/src/CognitoUserPool.js
index 87a134c..b1246e5 100644
--- a/node_modules/amazon-cognito-identity-js/src/CognitoUserPool.js
+++ b/node_modules/amazon-cognito-identity-js/src/CognitoUserPool.js
@@ -25,7 +25,7 @@ export default class CognitoUserPool {
     *        to support cognito advanced security features. By default, this
     *        flag is set to true.
     */
-   constructor(data, wrapRefreshSessionCallback) {
+   constructor(data, wrapRefreshSessionCallback, headersInterceptor) {
        const {
            UserPoolId,
            ClientId,
@@ -44,7 +44,7 @@ export default class CognitoUserPool {
        this.userPoolId = UserPoolId;
        this.clientId = ClientId;

-       this.client = new Client(region, endpoint, fetchOptions);
+       this.client = new Client(region, endpoint, fetchOptions, headersInterceptor);

        /**
         * By default, AdvancedSecurityDataCollectionFlag is set to true,
Abdulmalick-Dimnang commented 8 months ago

apologies for now clearing it up, even with v4 it's not possible unless we have to patch it 🙏

cwomack commented 7 months ago

Related to #12308

Abdulmalick-Dimnang commented 6 months ago

@cwomack just to follow up, this update might not be in the near road map?

yonatanganot commented 1 month ago

We also have this issue

cwomack commented 1 month ago

@Abdulmalick-Dimnang and @yonatanganot (as well as anyone else following this issue), wanted to provide a quick response here and let you know we haven't forgotten about this.

While there are no updates for this feature request at this point, it's still on our radar. If there's any progress to report, I'll follow up with a comment as soon as possible! Appreciate your patience.

ThisisBada commented 1 month ago

Hello @cwomack

May I please know if there is any workarounds for this since it is supported in aws-sdk but not in amplify?

Thanks

cwomack commented 1 month ago

@ThisisBada, the only way to implement a workaround is to patch the node_modules (similar to what @Abdulmalick-Dimnang did in the comment above). However, that comment/patch is only viable for v5 since the amazon-cognito-identity-js package is not used in v6. If you're on v6, you'd have to implement a similar patch/workaround.

Right now we don't have an ETA on a fix, but we'll update this issue once we have progress to communicate.