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

Multiplication of session tokens after expiration of session #13509

Closed mkolbusz closed 1 month ago

mkolbusz commented 3 months ago

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

Authentication

Amplify Version

v6

Amplify Categories

auth

Backend

None

Environment information

``` System: OS: Linux 6.5 Ubuntu 22.04.4 LTS 22.04.4 LTS (Jammy Jellyfish) CPU: (8) x64 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz Memory: 5.59 GB / 15.31 GB Container: Yes Shell: 5.8.1 - /usr/bin/zsh Binaries: Node: 20.12.2 - ~/.nvm/versions/node/v20.12.2/bin/node Yarn: 1.22.22 - ~/.nvm/versions/node/v20.12.2/bin/yarn npm: 10.5.0 - ~/.nvm/versions/node/v20.12.2/bin/npm pnpm: 8.8.0 - ~/.local/share/pnpm/pnpm Browsers: Chrome: 124.0.6367.155 npmPackages: @ampproject/toolbox-optimizer: undefined () @aws-amplify/adapter-nextjs: ^1.2.4 => 1.2.4 @aws-amplify/adapter-nextjs/api: undefined () @aws-amplify/adapter-nextjs/data: undefined () @aws-amplify/ui-react: ^6.1.12 => 6.1.12 @aws-amplify/ui-react-internal: undefined () @babel/core: undefined () @babel/runtime: 7.15.4 @edge-runtime/cookies: 4.0.2 @edge-runtime/ponyfill: 2.4.1 @edge-runtime/primitives: 4.0.2 @hapi/accept: undefined () @mswjs/interceptors: undefined () @napi-rs/triples: undefined () @next/font: undefined () @next/react-dev-overlay: undefined () @opentelemetry/api: undefined () @segment/ajv-human-errors: undefined () @tanstack/query-codemods: 4.24.3 @tanstack/react-query: ^5.45.0 => 5.45.0 @types/node: ^20 => 20.14.2 @types/react: ^18 => 18.3.3 @types/react-dom: ^18 => 18.3.0 @vercel/nft: undefined () @vercel/og: undefined () acorn: undefined () amphtml-validator: undefined () anser: undefined () arg: undefined () assert: undefined () async-retry: undefined () async-sema: undefined () autoprefixer: ^10 => 10.4.19 aws-amplify: ^6.3.6 => 6.3.6 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/data: undefined () aws-amplify/data/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 () babel-packages: undefined () browserify-zlib: undefined () browserslist: undefined () buffer: undefined () bytes: undefined () ci-info: undefined () cli-select: undefined () client-only: 0.0.1 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 () data-uri-to-buffer: undefined () debug: undefined () devalue: undefined () domain-browser: undefined () edge-runtime: undefined () eslint: ^8 => 8.57.0 eslint-config-next: 13.5.6 => 13.5.6 events: undefined () find-cache-dir: undefined () find-up: undefined () firebase: ^10.12.2 => 10.12.2 firebase/analytics: undefined () firebase/app: undefined () firebase/app-check: undefined () firebase/auth: undefined () firebase/auth/cordova: undefined () firebase/auth/web-extension: undefined () firebase/compat: undefined () firebase/compat/analytics: undefined () firebase/compat/app: undefined () firebase/compat/app-check: undefined () firebase/compat/auth: undefined () firebase/compat/database: undefined () firebase/compat/firestore: undefined () firebase/compat/functions: undefined () firebase/compat/installations: undefined () firebase/compat/messaging: undefined () firebase/compat/performance: undefined () firebase/compat/remote-config: undefined () firebase/compat/storage: undefined () firebase/database: undefined () firebase/firestore: undefined () firebase/firestore/lite: undefined () firebase/functions: undefined () firebase/installations: undefined () firebase/messaging: undefined () firebase/messaging/sw: undefined () firebase/performance: undefined () firebase/remote-config: undefined () firebase/storage: undefined () firebase/vertexai-preview: 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-worker: undefined () json5: undefined () jsonwebtoken: undefined () loader-runner: undefined () loader-utils: undefined () lodash.curry: undefined () lru-cache: undefined () micromatch: undefined () mini-css-extract-plugin: undefined () nanoid: undefined () native-url: undefined () neo-async: undefined () next: 13.5.6 => 13.5.6 node-fetch: undefined () node-html-parser: undefined () ora: undefined () os-browserify: undefined () p-limit: undefined () path-browserify: undefined () platform: undefined () postcss: ^8 => 8.4.38 (8.4.31) 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 () process: undefined () punycode: undefined () querystring-es3: undefined () raw-body: undefined () react: ^18.3.1 => 18.3.1 react-builtin: undefined () react-dom: ^18.3.1 => 18.3.1 react-dom-builtin: undefined () react-dom-experimental-builtin: undefined () react-experimental-builtin: undefined () react-is: 18.2.0 react-refresh: 0.12.0 react-server-dom-turbopack-builtin: undefined () react-server-dom-turbopack-experimental-builtin: undefined () react-server-dom-webpack-builtin: undefined () react-server-dom-webpack-experimental-builtin: undefined () 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 () superstruct: undefined () tailwindcss: ^3 => 3.4.4 tar: undefined () terser: undefined () text-table: undefined () timers-browserify: undefined () tty-browserify: undefined () typescript: ^5 => 5.4.5 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: cargo: 0.8.0 corepack: 0.25.2 husky: 9.0.11 is-ci: 3.0.1 local-ssl-proxy: 2.0.5 npm: 10.5.0 yarn: 1.22.22 ```

Describe the bug

After session tokens have expired and Tanstack Query is trying to refetch the data, the server multiplies the cookies and tokens as presented below: image

It causes problems with logout sometimes and should not be multiple session tokens available.

Expected behavior

After session tokens have expired the new tokens appear and no more than one token type is stored on the client side, no duplication.

Reproduction steps

  1. Login.
  2. Go to the other tab in the browser.
  3. Wait for the session to expire.
  4. Enter the tab of the application (refetching data and refreshing the session at the same time).
  5. The auth cookies are multiplicated.

Code Snippet

Minimal reproduction repository: https://github.com/mkolbusz/nextjs-amplify-v6-issues

Log output

No response

aws-exports.js

No response

Manual configuration

{ 
  "Auth": { 
    "Cognito": { 
      "userPoolId": "eu-west-1_", 
      "userPoolClientId": "xxx" 
      }
    }
}

Additional configuration

{
    "UserPool": {
        "Id": "eu-west-1_xxx",
        "Name": "xXx",
        "Policies": {
            "PasswordPolicy": {
                "MinimumLength": 6,
                "RequireUppercase": true,
                "RequireLowercase": true,
                "RequireNumbers": true,
                "RequireSymbols": false,
                "TemporaryPasswordValidityDays": 7
            }
        },
        "DeletionProtection": "INACTIVE",
        "LambdaConfig": {
            "DefineAuthChallenge": "arn:aws:lambda:eu-west-1::xxx::function:xxx",
            "CreateAuthChallenge": "arn:aws:lambda:eu-west-1:xxx:function:xxx",
            "VerifyAuthChallengeResponse": "arn:aws:lambda:eu-west-1:xxx:function:xxx"
        },
        "LastModifiedDate": "2024-04-15T15:02:27.808000+02:00",
        "CreationDate": "2023-10-02T22:28:59.238000+02:00",
        "SchemaAttributes": [
            {
                "Name": "sub",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": false,
                "Required": true,
                "StringAttributeConstraints": {
                    "MinLength": "1",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "name",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "given_name",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "family_name",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "middle_name",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "nickname",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "preferred_username",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "profile",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "picture",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "website",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "email",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "email_verified",
                "AttributeDataType": "Boolean",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false
            },
            {
                "Name": "gender",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "birthdate",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "10",
                    "MaxLength": "10"
                }
            },
            {
                "Name": "zoneinfo",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "locale",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "phone_number",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "phone_number_verified",
                "AttributeDataType": "Boolean",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false
            },
            {
                "Name": "address",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "updated_at",
                "AttributeDataType": "Number",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "NumberAttributeConstraints": {
                    "MinValue": "0"
                }
            },
            {
                "Name": "custom:test_attribute",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {}
            },
            {
                "Name": "custom:impersonators",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "identities",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {}
            }
        ],
        "AutoVerifiedAttributes": [
            "email"
        ],
        "UsernameAttributes": [
            "email"
        ],

        "UserAttributeUpdateSettings": {
            "AttributesRequireVerificationBeforeUpdate": []
        },
        "MfaConfiguration": "OFF",
        "EstimatedNumberOfUsers": 250,
        "EmailConfiguration": {
            "EmailSendingAccount": "COGNITO_DEFAULT"
        },
        "UserPoolTags": {
            "Client": "xxx",
            "Name": "xxx",
            "Namespace": "xxx",
            "Project": "xxx",
            "Stage": "dev",
            "Terraform": "true"
        },
        "Domain": "xxx",
        "AdminCreateUserConfig": {
            "AllowAdminCreateUserOnly": false,
            "UnusedAccountValidityDays": 7
        },
        "UsernameConfiguration": {
            "CaseSensitive": false
        },
        "Arn": "arn:aws:cognito-idp:eu-west-1:xxx:userpool/eu-west-1_xxx",
        "AccountRecoverySetting": {
            "RecoveryMechanisms": [
                {
                    "Priority": 1,
                    "Name": "verified_email"
                }
            ]
        }
    }
}

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 3 months ago

@mkolbusz, thank you for opening this issue. We're investigating this as a bug and will follow up with additional questions/updates as we have them.

cwomack commented 3 months ago

@mkolbusz, thank you for also creating the sample repo. We've been able to reproduce this on our side, so can you make that sample repo private? Just want to make sure any sensitive information is kept private.

mkolbusz commented 3 months ago

@cwomack done

mkolbusz commented 3 months ago

@cwomack any update on this? When can I expect fix's merge?

cwomack commented 2 months ago

@mkolbusz, we are awaiting internal approvals and testing. We will update this issue as soon as we can once the fix is approved and merged. Appreciate your patience!

HuiSF commented 2 months ago

Hi @mkolbusz The fix for this issue has released with aws-amplify@6.4.2 and @aws-amplify/adapter-nextjs@1.2.9, please upgrade to these versions. Thanks!

mkolbusz commented 2 months ago

@HuiSF @cwomack thanks for the fix. It's working well.

OrmEmbaar commented 1 month ago

I have been struggling with this bug for a while. I note the fix clears cookies that have been set on a non-root path when the access token has expired, but the fix does NOT seem to work when the refresh token has expired. In that case, the non-root path cookies continue to be persisted to cookie storage.

HuiSF commented 1 month ago

Hi @OrmEmbaar thanks for following up.

When the access token and refresh token are both expired and the library attempts to refresh the tokens from the server side, it will fail. In this case, the underlying logic will clear all outdated tokens, so it should set Set-Cookie headers in the response object you passed into runWithAmplifyServerSideContext() to remove token cookies from the client. To make this work, the'response` should be sent back to the client-side.

Amplify API calls would fail due to non-refreshable tokens; how are you handling the API errors? Does the error handling prevent the response object that contains the Set-Cookie headers from being sent back to the client?

OrmEmbaar commented 1 month ago

@HuiSF

The invalid cookies are being set inside getServerSideProps, so I'm not sure how the response object is being handled. I assume Next is dealing with it.

Also, how does this work for routes with slugs? I am seeing cookies set on /path, but we don't use any Amplify SSR methods on /path. However, we do use Amplify SSR methods on /path/[slug].

I think the issue may be that users are issued a corrupted cookie for /path when they land on /path/[slug], then when they visit /path there is no set-cookie header to remove the corrupted cookies. In our case, that results in a tokenRefresh_failure loop.

HuiSF commented 1 month ago

Hi @OrmEmbaar I think I understood. In this case the Amplify library couldn't predict the paths you may have, so the cookies couldn't be removed. I think it may require a custom solution in your page implementation to remove invalid cookies. You can do this via the response object in getServerSideProps. e.g.

// This is pseudo code
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
  // get the requested url, and use this to determine it's under a specific path
  const url = req.url; 
  const isUnderAPath = ...; // e.g. /sub-path/dymaic-route is under `sub-path`
  if (isUnderAPath) {
    // collecting Amplify cookie names
    const clearCookieNames = Object.keys(req.cookies)
      .filter(cookieName => cookieName.startsWith('CognitoIdentityServiceProvider.'));
    // insert Set-Cookie headers to remove these cookies into the response object
    for (let cookieName of clearCookieNames) {
      res.appendHeader('Set-Cookie', `${clearCookieNames}=;Expires=${new Date('1970').toUTCString()}`)
    }
  }

  return { props: {} };
};

Could you give it a try?

OrmEmbaar commented 1 month ago

@HuiSF I think our plan is to just ride it out. We're not getting many reports from users anymore and when we do we simply ask them to clear their cookies. We have upgraded to the latest version, so the number of users effected will diminish over time.

I have noticed that sometimes our auth cookies expire in a week whereas other times they expire in a year. A week is the length of our refresh token. If they all expired in a week the problem would solve itself rapidly. Do you know under what conditions they expire in a week vs a year?

HuiSF commented 1 month ago

Thanks for the follow up again @OrmEmbaar

Do you know under what conditions they expire in a week vs a year?

The expiration period of the access token, ID token and refresh token are configurable. This can be done with the following in the scope of Amplify:

  1. Directly modify the TTLs from the Cognito console. (Go to your user pool, the user pool client you are using, look the section "App client information" that contains "Refresh token expiration" etc)
  2. If you are using Amplify CLI (the "Gen1" experience), you should be able to update refresh token lifetime via amplify update auth
  3. If you are using Amplify Gen2 experience, you can modify the TTLs via the backend resource. E.g.
    
    import { defineBackend } from '@aws-amplify/backend';
    import { auth } from './auth/resource';
    import { data } from './data/resource';

const backend = defineBackend({ auth, data, });

const { cfnUserPoolClient } = backend.auth.resources.cfnResources;

// Set token validities to a minimum to reduce canary test running time. cfnUserPoolClient.accessTokenValidity = 5; cfnUserPoolClient.idTokenValidity = 5; cfnUserPoolClient.refreshTokenValidity = 60; cfnUserPoolClient.tokenValidityUnits = { accessToken: 'minutes', idToken: 'minutes', refreshToken: 'minutes', };



More details about the token can be found [here](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html).
HuiSF commented 1 month ago

I'm closing this issue as the originally reported bug has been fixed. Please feel free to reach out if anything we can help with.