amplify-js icon indicating copy to clipboard operation
amplify-js copied to clipboard

Multiplication of session tokens after expiration of session

Open mkolbusz opened this issue 1 year ago • 4 comments

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

mkolbusz avatar Jun 18 '24 18:06 mkolbusz

@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 avatar Jun 18 '24 18:06 cwomack

@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.

cwomack avatar Jun 18 '24 19:06 cwomack

@cwomack done

mkolbusz avatar Jun 18 '24 19:06 mkolbusz

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

mkolbusz avatar Jun 28 '24 13:06 mkolbusz

@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!

cwomack avatar Jul 09 '24 20:07 cwomack

Hi @mkolbusz The fix for this issue has released with [email protected] and @aws-amplify/[email protected], please upgrade to these versions. Thanks!

HuiSF avatar Jul 23 '24 00:07 HuiSF

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

mkolbusz avatar Jul 24 '24 11:07 mkolbusz

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.

OrmEmbaar avatar Aug 02 '24 11:08 OrmEmbaar

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?

HuiSF avatar Aug 02 '24 23:08 HuiSF

@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.

OrmEmbaar avatar Aug 06 '24 20:08 OrmEmbaar

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?

HuiSF avatar Aug 07 '24 19:08 HuiSF

@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?

OrmEmbaar avatar Aug 09 '24 08:08 OrmEmbaar

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.

HuiSF avatar Aug 09 '24 17:08 HuiSF

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.

HuiSF avatar Aug 12 '24 18:08 HuiSF