diff --git a/.github/release-drafter/entraid-config.yml b/.github/release-drafter/entraid-config.yml
new file mode 100644
index 00000000000..d0ddd00773a
--- /dev/null
+++ b/.github/release-drafter/entraid-config.yml
@@ -0,0 +1,50 @@
+name-template: 'entraid@$NEXT_PATCH_VERSION'
+tag-template: 'entraid@$NEXT_PATCH_VERSION'
+autolabeler:
+  - label: 'chore'
+    files:
+      - '*.md'
+      - '.github/*'
+  - label: 'bug'
+    branch:
+      - '/bug-.+'
+  - label: 'chore'
+    branch:
+      - '/chore-.+'
+  - label: 'feature'
+    branch:
+      - '/feature-.+'
+categories:
+  - title: 'Breaking Changes'
+    labels:
+      - 'breakingchange'
+  - title: '🚀 New Features'
+    labels:
+      - 'feature'
+      - 'enhancement'
+  - title: '🐛 Bug Fixes'
+    labels:
+      - 'fix'
+      - 'bugfix'
+      - 'bug'
+  - title: '🧰 Maintenance'
+    label:
+      - 'chore'
+      - 'maintenance'
+      - 'documentation'
+      - 'docs'
+
+change-template: '- $TITLE (#$NUMBER)'
+include-paths:
+  - 'packages/entraid'
+exclude-labels:
+  - 'skip-changelog'
+template: |
+  ## Changes
+
+  $CHANGES
+
+  ## Contributors
+  We'd like to thank all the contributors who worked on this release!
+
+  $CONTRIBUTORS
diff --git a/.github/workflows/release-drafter-entraid.yml b/.github/workflows/release-drafter-entraid.yml
new file mode 100644
index 00000000000..d522c6cef6f
--- /dev/null
+++ b/.github/workflows/release-drafter-entraid.yml
@@ -0,0 +1,24 @@
+name: Release Drafter
+
+on:
+  push:
+    # branches to consider in the event; optional, defaults to all
+    branches:
+      - master
+
+jobs:
+
+  update_release_draft:
+
+    permissions:
+      contents: write
+      pull-requests: write
+    runs-on: ubuntu-latest
+    steps:
+      # Drafts your next Release notes as Pull Requests are merged into "master"
+      - uses: release-drafter/release-drafter@v5
+        with:
+          # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
+           config-name: release-drafter/entraid-config.yml
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/package-lock.json b/package-lock.json
index ba18a98b6a5..8fdd049a5b2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,6 +33,29 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/@azure/msal-common": {
+      "version": "14.16.0",
+      "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz",
+      "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/@azure/msal-node": {
+      "version": "2.16.2",
+      "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz",
+      "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/msal-common": "14.16.0",
+        "jsonwebtoken": "^9.0.0",
+        "uuid": "^8.3.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
     "node_modules/@babel/code-frame": {
       "version": "7.23.5",
       "dev": true,
@@ -824,6 +847,10 @@
         "node": ">=12"
       }
     },
+    "node_modules/@redis/authx": {
+      "resolved": "packages/authx",
+      "link": true
+    },
     "node_modules/@redis/bloom": {
       "resolved": "packages/bloom",
       "link": true
@@ -832,6 +859,10 @@
       "resolved": "packages/client",
       "link": true
     },
+    "node_modules/@redis/entraid": {
+      "resolved": "packages/entraid",
+      "link": true
+    },
     "node_modules/@redis/graph": {
       "resolved": "packages/graph",
       "link": true
@@ -929,11 +960,82 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/body-parser": {
+      "version": "1.19.5",
+      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
+      "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/connect": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/connect": {
+      "version": "3.4.38",
+      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+      "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/express": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
+      "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/body-parser": "*",
+        "@types/express-serve-static-core": "^4.17.33",
+        "@types/qs": "*",
+        "@types/serve-static": "*"
+      }
+    },
+    "node_modules/@types/express-serve-static-core": {
+      "version": "4.19.6",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
+      "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "@types/qs": "*",
+        "@types/range-parser": "*",
+        "@types/send": "*"
+      }
+    },
+    "node_modules/@types/express-session": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz",
+      "integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/express": "*"
+      }
+    },
     "node_modules/@types/http-cache-semantics": {
       "version": "4.0.4",
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/http-errors": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
+      "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/mime": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+      "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/mocha": {
       "version": "10.0.6",
       "dev": true,
@@ -947,6 +1049,43 @@
         "undici-types": "~5.26.4"
       }
     },
+    "node_modules/@types/qs": {
+      "version": "6.9.17",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
+      "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/range-parser": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/send": {
+      "version": "0.17.4",
+      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
+      "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/mime": "^1",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/serve-static": {
+      "version": "1.15.7",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
+      "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/http-errors": "*",
+        "@types/node": "*",
+        "@types/send": "*"
+      }
+    },
     "node_modules/@types/sinon": {
       "version": "17.0.3",
       "dev": true,
@@ -973,6 +1112,20 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/agent-base": {
       "version": "7.1.0",
       "dev": true,
@@ -1112,6 +1265,13 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/array-union": {
       "version": "1.0.2",
       "dev": true,
@@ -1260,6 +1420,48 @@
         "readable-stream": "^3.4.0"
       }
     },
+    "node_modules/body-parser": {
+      "version": "1.20.3",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+      "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "content-type": "~1.0.5",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "on-finished": "2.4.1",
+        "qs": "6.13.0",
+        "raw-body": "2.5.2",
+        "type-is": "~1.6.18",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/body-parser/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/body-parser/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/boxen": {
       "version": "7.1.1",
       "dev": true,
@@ -1466,6 +1668,12 @@
         "ieee754": "^1.1.13"
       }
     },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
     "node_modules/bundle-name": {
       "version": "4.1.0",
       "dev": true,
@@ -1480,6 +1688,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/cacheable-lookup": {
       "version": "7.0.0",
       "dev": true,
@@ -1531,18 +1749,38 @@
       }
     },
     "node_modules/call-bind": {
-      "version": "1.0.5",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.1",
-        "set-function-length": "^1.1.1"
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
+      "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/callsites": {
       "version": "3.1.0",
       "dev": true,
@@ -1805,11 +2043,51 @@
         "url": "https://github.com/yeoman/configstore?sponsor=1"
       }
     },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/convert-source-map": {
       "version": "1.9.0",
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/cookie": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+      "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/cosmiconfig": {
       "version": "9.0.0",
       "dev": true,
@@ -2003,16 +2281,21 @@
       }
     },
     "node_modules/define-data-property": {
-      "version": "1.1.1",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "get-intrinsic": "^1.2.1",
-        "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
       },
       "engines": {
         "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/define-lazy-prop": {
@@ -2055,11 +2338,32 @@
         "node": ">= 14"
       }
     },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/deprecation": {
       "version": "2.3.1",
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
     "node_modules/diff": {
       "version": "5.0.0",
       "dev": true,
@@ -2082,11 +2386,55 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/dotenv": {
+      "version": "16.4.7",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
+      "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz",
+      "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/electron-to-chromium": {
       "version": "1.4.656",
       "dev": true,
@@ -2102,6 +2450,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/env-paths": {
       "version": "2.2.1",
       "dev": true,
@@ -2175,6 +2533,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/es-errors": {
       "version": "1.3.0",
       "dev": true,
@@ -2292,6 +2660,13 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/escape-string-regexp": {
       "version": "4.0.0",
       "dev": true,
@@ -2351,6 +2726,16 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/execa": {
       "version": "8.0.1",
       "dev": true,
@@ -2395,6 +2780,131 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/express": {
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+      "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.20.3",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.7.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.3.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.13.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.19.0",
+        "serve-static": "1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/express-session": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
+      "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "0.7.2",
+        "cookie-signature": "1.0.7",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-headers": "~1.0.2",
+        "parseurl": "~1.3.3",
+        "safe-buffer": "5.2.1",
+        "uid-safe": "~2.1.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/express-session/node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express-session/node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/express-session/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/express-session/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/express/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/express/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/express/node_modules/path-to-regexp": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+      "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/external-editor": {
       "version": "3.1.0",
       "dev": true,
@@ -2525,6 +3035,42 @@
         "node": ">=8"
       }
     },
+    "node_modules/finalhandler": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+      "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "2.0.1",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/finalhandler/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/finalhandler/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/find-cache-dir": {
       "version": "3.3.2",
       "dev": true,
@@ -2603,6 +3149,26 @@
         "node": ">=12.20.0"
       }
     },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/fromentries": {
       "version": "1.3.2",
       "dev": true,
@@ -2701,15 +3267,20 @@
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.2.3",
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz",
+      "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "es-errors": "^1.0.0",
+        "call-bind-apply-helpers": "^1.0.0",
+        "dunder-proto": "^1.0.0",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "has-proto": "^1.0.1",
-        "has-symbols": "^1.0.3",
-        "hasown": "^2.0.0"
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -2934,11 +3505,13 @@
       }
     },
     "node_modules/gopd": {
-      "version": "1.0.1",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
       "dev": true,
       "license": "MIT",
-      "dependencies": {
-        "get-intrinsic": "^1.1.3"
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -3001,11 +3574,13 @@
       }
     },
     "node_modules/has-property-descriptors": {
-      "version": "1.0.1",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "get-intrinsic": "^1.2.2"
+        "es-define-property": "^1.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -3023,7 +3598,9 @@
       }
     },
     "node_modules/has-symbols": {
-      "version": "1.0.3",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -3063,7 +3640,9 @@
       }
     },
     "node_modules/hasown": {
-      "version": "2.0.0",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -3091,6 +3670,23 @@
       "dev": true,
       "license": "BSD-2-Clause"
     },
+    "node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/http-proxy-agent": {
       "version": "7.0.0",
       "dev": true,
@@ -3360,6 +3956,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
     "node_modules/is-arguments": {
       "version": "1.1.1",
       "dev": true,
@@ -4032,26 +4638,81 @@
         "node": ">=6"
       }
     },
-    "node_modules/jsonc-parser": {
-      "version": "3.2.1",
+    "node_modules/jsonc-parser": {
+      "version": "3.2.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jsonfile": {
+      "version": "6.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/jsonwebtoken": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+      "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+      "license": "MIT",
+      "dependencies": {
+        "jws": "^3.2.2",
+        "lodash.includes": "^4.3.0",
+        "lodash.isboolean": "^3.0.3",
+        "lodash.isinteger": "^4.0.4",
+        "lodash.isnumber": "^3.0.3",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.isstring": "^4.0.1",
+        "lodash.once": "^4.0.0",
+        "ms": "^2.1.1",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/jsonwebtoken/node_modules/semver": {
+      "version": "7.6.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/just-extend": {
+      "version": "6.2.0",
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/jsonfile": {
-      "version": "6.1.0",
-      "dev": true,
+    "node_modules/jwa": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+      "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
       "license": "MIT",
       "dependencies": {
-        "universalify": "^2.0.0"
-      },
-      "optionalDependencies": {
-        "graceful-fs": "^4.1.6"
+        "buffer-equal-constant-time": "1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
       }
     },
-    "node_modules/just-extend": {
-      "version": "6.2.0",
-      "dev": true,
-      "license": "MIT"
+    "node_modules/jws": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^1.4.1",
+        "safe-buffer": "^5.0.1"
+      }
     },
     "node_modules/keyv": {
       "version": "4.5.4",
@@ -4119,14 +4780,42 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lodash.includes": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+      "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isboolean": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+      "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isinteger": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+      "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isnumber": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+      "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+      "license": "MIT"
+    },
     "node_modules/lodash.isplainobject": {
       "version": "4.0.6",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/lodash.isstring": {
       "version": "4.0.1",
-      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
       "license": "MIT"
     },
     "node_modules/lodash.uniqby": {
@@ -4209,6 +4898,26 @@
         "node": ">= 12"
       }
     },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/merge-stream": {
       "version": "2.0.0",
       "dev": true,
@@ -4222,6 +4931,16 @@
         "node": ">= 8"
       }
     },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/micromatch": {
       "version": "4.0.5",
       "dev": true,
@@ -4234,6 +4953,19 @@
         "node": ">=8.6"
       }
     },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/mime-db": {
       "version": "1.52.0",
       "dev": true,
@@ -4384,7 +5116,6 @@
     },
     "node_modules/ms": {
       "version": "2.1.3",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/mute-stream": {
@@ -4406,6 +5137,16 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/netmask": {
       "version": "2.0.2",
       "dev": true,
@@ -4723,6 +5464,29 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "dev": true,
@@ -5178,6 +5942,16 @@
         "parse-path": "^7.0.0"
       }
     },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "dev": true,
@@ -5365,6 +6139,20 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
     "node_modules/proxy-agent": {
       "version": "6.3.1",
       "dev": true,
@@ -5410,6 +6198,22 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/qs": {
+      "version": "6.13.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.0.6"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/queue-microtask": {
       "version": "1.2.3",
       "dev": true,
@@ -5440,6 +6244,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/random-bytes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+      "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/randombytes": {
       "version": "2.1.0",
       "dev": true,
@@ -5448,6 +6262,32 @@
         "safe-buffer": "^5.1.0"
       }
     },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/rc": {
       "version": "1.2.8",
       "dev": true,
@@ -5880,7 +6720,6 @@
     },
     "node_modules/safe-buffer": {
       "version": "5.2.1",
-      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -5970,6 +6809,58 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/send": {
+      "version": "0.19.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+      "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/send/node_modules/debug/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/send/node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/serialize-javascript": {
       "version": "6.0.0",
       "dev": true,
@@ -5978,21 +6869,40 @@
         "randombytes": "^2.1.0"
       }
     },
+    "node_modules/serve-static": {
+      "version": "1.16.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+      "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.19.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
     "node_modules/set-blocking": {
       "version": "2.0.0",
       "dev": true,
       "license": "ISC"
     },
     "node_modules/set-function-length": {
-      "version": "1.2.0",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "define-data-property": "^1.1.1",
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.2",
+        "get-intrinsic": "^1.2.4",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.1"
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -6011,6 +6921,13 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "dev": true,
@@ -6058,13 +6975,19 @@
       }
     },
     "node_modules/side-channel": {
-      "version": "1.0.4",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+      "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "call-bind": "^1.0.0",
-        "get-intrinsic": "^1.0.2",
-        "object-inspect": "^1.9.0"
+        "call-bind": "^1.0.7",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4",
+        "object-inspect": "^1.13.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -6191,6 +7114,16 @@
       "dev": true,
       "license": "BSD-3-Clause"
     },
+    "node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/stdin-discarder": {
       "version": "0.2.2",
       "dev": true,
@@ -6404,6 +7337,16 @@
         "node": ">=8.0"
       }
     },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
     "node_modules/trim-repeated": {
       "version": "1.0.0",
       "dev": true,
@@ -6462,6 +7405,20 @@
         "node": ">=8"
       }
     },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/typed-array-buffer": {
       "version": "1.0.0",
       "dev": true,
@@ -6585,6 +7542,19 @@
         "node": ">=14.17"
       }
     },
+    "node_modules/uid-safe": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+      "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "random-bytes": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/unbox-primitive": {
       "version": "1.0.2",
       "dev": true,
@@ -6642,6 +7612,16 @@
         "node": ">= 10.0.0"
       }
     },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/update-browserslist-db": {
       "version": "1.0.13",
       "dev": true,
@@ -6750,14 +7730,33 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
     "node_modules/uuid": {
       "version": "8.3.2",
-      "dev": true,
       "license": "MIT",
       "bin": {
         "uuid": "dist/bin/uuid"
       }
     },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/vscode-oniguruma": {
       "version": "1.7.0",
       "dev": true,
@@ -7127,6 +8126,21 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "packages/authx": {
+      "name": "@redis/authx",
+      "version": "5.0.0-next.5",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/msal-node": "^2.16.1"
+      },
+      "devDependencies": {},
+      "engines": {
+        "node": ">= 18"
+      },
+      "peerDependencies": {
+        "@redis/client": "^5.0.0-next.5"
+      }
+    },
     "packages/bloom": {
       "name": "@redis/bloom",
       "version": "5.0.0-next.5",
@@ -7155,8 +8169,52 @@
       },
       "engines": {
         "node": ">= 18"
+      },
+      "peerDependencies": {
+        "@redis/authx": "^5.0.0-next.5"
+      }
+    },
+    "packages/entraid": {
+      "name": "@redis/entraid",
+      "version": "5.0.0-next.5",
+      "license": "MIT",
+      "dependencies": {
+        "@azure/msal-node": "^2.16.1"
+      },
+      "devDependencies": {
+        "@redis/test-utils": "*",
+        "@types/express": "^4.17.21",
+        "@types/express-session": "^1.18.0",
+        "@types/node": "^22.9.0",
+        "dotenv": "^16.3.1",
+        "express": "^4.21.1",
+        "express-session": "^1.18.1"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "peerDependencies": {
+        "@redis/authx": "^5.0.0-next.5",
+        "@redis/client": "^5.0.0-next.5"
       }
     },
+    "packages/entraid/node_modules/@types/node": {
+      "version": "22.10.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
+      "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.20.0"
+      }
+    },
+    "packages/entraid/node_modules/undici-types": {
+      "version": "6.20.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+      "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "packages/graph": {
       "name": "@redis/graph",
       "version": "5.0.0-next.5",
diff --git a/packages/client/lib/authx/credentials-provider.ts b/packages/client/lib/authx/credentials-provider.ts
new file mode 100644
index 00000000000..667795be9b3
--- /dev/null
+++ b/packages/client/lib/authx/credentials-provider.ts
@@ -0,0 +1,102 @@
+import { Disposable } from './disposable';
+/**
+ * Provides credentials asynchronously.
+ */
+export interface AsyncCredentialsProvider {
+  readonly type: 'async-credentials-provider';
+  credentials: () => Promise<BasicAuth>
+}
+
+/**
+ * Provides credentials asynchronously with support for continuous updates via a subscription model.
+ * This is useful for environments where credentials are frequently rotated or updated or can be revoked.
+ */
+export interface StreamingCredentialsProvider {
+  readonly type: 'streaming-credentials-provider';
+
+  /**
+   * Provides initial credentials and subscribes to subsequent updates. This is used internally by the node-redis client
+   * to handle credential rotation and re-authentication.
+   *
+   * Note: The node-redis client manages the subscription lifecycle automatically. Users only need to implement
+   * onReAuthenticationError if they want to be notified about authentication failures.
+   *
+   * Error handling:
+   * - Errors received via onError indicate a fatal issue with the credentials stream
+   * - The stream is automatically closed(disposed) when onError occurs
+   * - onError typically mean the provider failed to fetch new credentials after retrying
+   *
+   * @example
+   * ```ts
+   * const provider = getStreamingProvider();
+   * const [initialCredentials, disposable] = await provider.subscribe({
+   *   onNext: (newCredentials) => {
+   *     // Handle credential update
+   *   },
+   *   onError: (error) => {
+   *     // Handle fatal stream error
+   *   }
+   * });
+   *
+   * @param listener - Callbacks to handle credential updates and errors
+   * @returns A Promise resolving to [initial credentials, cleanup function]
+   */
+  subscribe: (listener: StreamingCredentialsListener<BasicAuth>) => Promise<[BasicAuth, Disposable]>
+
+  /**
+   * Called when authentication fails or credentials cannot be renewed in time.
+   * Implement this to handle authentication errors in your application.
+   *
+   * @param error - Either a CredentialsError (invalid/expired credentials) or
+   *                UnableToObtainNewCredentialsError (failed to fetch new credentials on time)
+   */
+  onReAuthenticationError: (error: ReAuthenticationError) => void;
+
+}
+
+/**
+ * Type representing basic authentication credentials.
+ */
+export type BasicAuth = { username?: string, password?: string }
+
+/**
+ * Callback to handle credential updates and errors.
+ */
+export type StreamingCredentialsListener<T> = {
+  onNext: (credentials: T) => void;
+  onError: (e: Error) => void;
+}
+
+
+/**
+ * Providers that can supply authentication credentials
+ */
+export type CredentialsProvider = AsyncCredentialsProvider | StreamingCredentialsProvider
+
+/**
+ * Errors that can occur during re-authentication.
+ */
+export type ReAuthenticationError = CredentialsError | UnableToObtainNewCredentialsError
+
+/**
+ * Thrown when re-authentication fails with provided credentials .
+ * e.g. when the credentials are invalid, expired or revoked.
+ *
+ */
+export class CredentialsError extends Error {
+  constructor(message: string) {
+    super(`Re-authentication with latest credentials failed: ${message}`);
+    this.name = 'CredentialsError';
+  }
+
+}
+
+/**
+ * Thrown when new credentials cannot be obtained before current ones expire
+ */
+export class UnableToObtainNewCredentialsError extends Error {
+  constructor(message: string) {
+    super(`Unable to obtain new credentials : ${message}`);
+    this.name = 'UnableToObtainNewCredentialsError';
+  }
+}
\ No newline at end of file
diff --git a/packages/client/lib/authx/disposable.ts b/packages/client/lib/authx/disposable.ts
new file mode 100644
index 00000000000..ee4526a37bd
--- /dev/null
+++ b/packages/client/lib/authx/disposable.ts
@@ -0,0 +1,6 @@
+/**
+ * Represents a resource that can be disposed.
+ */
+export interface Disposable {
+  dispose(): void;
+}
\ No newline at end of file
diff --git a/packages/client/lib/authx/identity-provider.ts b/packages/client/lib/authx/identity-provider.ts
new file mode 100644
index 00000000000..a2d25c8f9db
--- /dev/null
+++ b/packages/client/lib/authx/identity-provider.ts
@@ -0,0 +1,22 @@
+/**
+ * An identity provider is responsible for providing a token that can be used to authenticate with a service.
+ */
+
+/**
+ * The response from an identity provider when requesting a token.
+ *
+ * note: "native" refers to the type of the token that the actual identity provider library is using.
+ *
+ * @type T The type of the native idp token.
+ * @property token The token.
+ * @property ttlMs The time-to-live of the token in epoch milliseconds extracted from the native token in local time.
+ */
+export type TokenResponse<T> = { token: T, ttlMs: number };
+
+export interface IdentityProvider<T> {
+  /**
+   * Request a token from the identity provider.
+   * @returns A promise that resolves to an object containing the token and the time-to-live in epoch milliseconds.
+   */
+  requestToken(): Promise<TokenResponse<T>>;
+}
\ No newline at end of file
diff --git a/packages/client/lib/authx/index.ts b/packages/client/lib/authx/index.ts
new file mode 100644
index 00000000000..ce611e1497f
--- /dev/null
+++ b/packages/client/lib/authx/index.ts
@@ -0,0 +1,15 @@
+export { TokenManager, TokenManagerConfig, TokenStreamListener, RetryPolicy, IDPError } from './token-manager';
+export {
+  CredentialsProvider,
+  StreamingCredentialsProvider,
+  UnableToObtainNewCredentialsError,
+  CredentialsError,
+  StreamingCredentialsListener,
+  AsyncCredentialsProvider,
+  ReAuthenticationError,
+  BasicAuth
+} from './credentials-provider';
+export { Token } from './token';
+export { IdentityProvider, TokenResponse } from './identity-provider';
+
+export { Disposable } from './disposable'
\ No newline at end of file
diff --git a/packages/client/lib/authx/token-manager.spec.ts b/packages/client/lib/authx/token-manager.spec.ts
new file mode 100644
index 00000000000..1cc2a207edc
--- /dev/null
+++ b/packages/client/lib/authx/token-manager.spec.ts
@@ -0,0 +1,588 @@
+import { strict as assert } from 'node:assert';
+import { Token } from './token';
+import { IDPError, RetryPolicy, TokenManager, TokenManagerConfig, TokenStreamListener } from './token-manager';
+import { IdentityProvider, TokenResponse } from './identity-provider';
+import { setTimeout } from 'timers/promises';
+
+describe('TokenManager', () => {
+
+  /**
+   * Helper function to delay execution for a given number of milliseconds.
+   * @param ms
+   */
+  const delay = (ms: number) => {
+    return setTimeout(ms);
+  }
+
+  /**
+   * IdentityProvider that returns a fixed test token for testing and doesn't handle TTL.
+   */
+  class TestIdentityProvider implements IdentityProvider<string> {
+    requestToken(): Promise<TokenResponse<string>> {
+      return Promise.resolve({ token: 'test-token 1', ttlMs: 1000 });
+    }
+  }
+
+  /**
+   * Helper function to create a test token with a given TTL .
+   * @param ttlMs Time-to-live in milliseconds
+   */
+  const createToken = (ttlMs: number): Token<string> => {
+    return new Token('test-token', ttlMs, 0);
+  };
+
+  /**
+   * Listener that records received tokens and errors for testing.
+   */
+  class TestListener implements TokenStreamListener<string> {
+
+    public readonly receivedTokens: Token<string>[] = [];
+    public readonly errors: IDPError[] = [];
+
+    onNext(token: Token<string>): void {
+      this.receivedTokens.push(token);
+    }
+
+    onError(error: IDPError): void {
+      this.errors.push(error);
+    }
+  }
+
+  /**
+   * IdentityProvider that returns a sequence of tokens with a fixed delay simulating network latency.
+   * Used for testing token refresh scenarios.
+   */
+  class ControlledIdentityProvider implements IdentityProvider<string> {
+    private tokenIndex = 0;
+    private readonly delayMs: number;
+    private readonly ttlMs: number;
+
+    constructor(
+      private readonly tokens: string[],
+      delayMs: number = 0,
+      tokenTTlMs: number = 100
+    ) {
+      this.delayMs = delayMs;
+      this.ttlMs = tokenTTlMs;
+    }
+
+    async requestToken(): Promise<TokenResponse<string>> {
+
+      if (this.tokenIndex >= this.tokens.length) {
+        throw new Error('No more test tokens available');
+      }
+
+      if (this.delayMs > 0) {
+        await setTimeout(this.delayMs);
+      }
+
+      return { token: this.tokens[this.tokenIndex++], ttlMs: this.ttlMs };
+    }
+
+  }
+
+  /**
+   * IdentityProvider that simulates various error scenarios with configurable behavior
+   */
+  class ErrorSimulatingProvider implements IdentityProvider<string> {
+    private requestCount = 0;
+
+    constructor(
+      private readonly errorSequence: Array<Error | string>,
+      private readonly delayMs: number = 0,
+      private readonly ttlMs: number = 100
+    ) {}
+
+    async requestToken(): Promise<TokenResponse<string>> {
+
+      if (this.delayMs > 0) {
+        await delay(this.delayMs);
+      }
+
+      const result = this.errorSequence[this.requestCount];
+      this.requestCount++;
+
+      if (result instanceof Error) {
+        throw result;
+      } else if (typeof result === 'string') {
+        return { token: result, ttlMs: this.ttlMs };
+      } else {
+        throw new Error('No more responses configured');
+      }
+    }
+
+    getRequestCount(): number {
+      return this.requestCount;
+    }
+  }
+
+  describe('constructor validation', () => {
+    it('should throw error if ratio is greater than 1', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 1.1
+      };
+
+      assert.throws(
+        () => new TokenManager(new TestIdentityProvider(), config),
+        /expirationRefreshRatio must be less than or equal to 1/
+      );
+    });
+
+    it('should throw error if ratio is negative', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: -0.1
+      };
+
+      assert.throws(
+        () => new TokenManager(new TestIdentityProvider(), config),
+        /expirationRefreshRatio must be greater or equal to 0/
+      );
+    });
+
+    it('should accept ratio of 1', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 1
+      };
+
+      assert.doesNotThrow(
+        () => new TokenManager(new TestIdentityProvider(), config)
+      );
+    });
+
+    it('should accept ratio of 0', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0
+      };
+
+      assert.doesNotThrow(
+        () => new TokenManager(new TestIdentityProvider(), config)
+      );
+    });
+  });
+
+  describe('calculateRefreshTime', () => {
+    it('should calculate correct refresh time with 0.8 ratio', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0.8
+      };
+
+      const manager = new TokenManager(new TestIdentityProvider(), config);
+      const token = createToken(1000);
+      const refreshTime = manager.calculateRefreshTime(token, 0);
+
+      // With 1000s TTL and 0.8 ratio, should refresh at 800s
+      assert.equal(refreshTime, 800);
+    });
+
+    it('should return 0 for ratio of 0', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0
+      };
+
+      const manager = new TokenManager(new TestIdentityProvider(), config);
+      const token = createToken(1000);
+      const refreshTime = manager.calculateRefreshTime(token, 0);
+
+      assert.equal(refreshTime, 0);
+    });
+
+    it('should refresh at expiration time with ratio of 1', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 1
+      };
+
+      const manager = new TokenManager(new TestIdentityProvider(), config);
+      const token = createToken(1000);
+      const refreshTime = manager.calculateRefreshTime(token, 0);
+
+      assert.equal(refreshTime, 1000);
+    });
+
+    it('should handle short TTL tokens', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0.8
+      };
+
+      const manager = new TokenManager(new TestIdentityProvider(), config);
+      const token = createToken(5);
+      const refreshTime = manager.calculateRefreshTime(token, 0);
+
+      assert.equal(refreshTime, 4);
+    });
+
+    it('should handle expired tokens', () => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0.8
+      };
+
+      const manager = new TokenManager(new TestIdentityProvider(), config);
+      // Create token that expired 100s ago
+      const token = createToken(-100);
+      const refreshTime = manager.calculateRefreshTime(token, 0);
+
+      // Should return refresh time of 0 for expired tokens
+      assert.equal(refreshTime, 0);
+    });
+    describe('token refresh scenarios', () => {
+
+      describe('token refresh', () => {
+        it('should handle token refresh', async () => {
+          const networkDelay = 20;
+          const tokenTtl = 100;
+
+          const config: TokenManagerConfig = {
+            expirationRefreshRatio: 0.8
+          };
+
+          const identityProvider = new ControlledIdentityProvider(['token1', 'token2', 'token3'], networkDelay, tokenTtl);
+          const manager = new TokenManager(identityProvider, config);
+          const listener = new TestListener();
+          const disposable = manager.start(listener);
+
+          assert.equal(manager.getCurrentToken(), null, 'Should not have token yet');
+          // Wait for the first token request to complete ( it should be immediate, and we should wait only for the network delay)
+          await delay(networkDelay)
+
+          assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token');
+          assert.equal(listener.receivedTokens[0].value, 'token1', 'Should have correct token value');
+          assert.equal(listener.receivedTokens[0].expiresAtMs - listener.receivedTokens[0].receivedAtMs,
+            tokenTtl, 'Should have correct TTL');
+          assert.equal(listener.errors.length, 0, 'Should not have any errors: ' + listener.errors);
+          assert.equal(manager.getCurrentToken().value, 'token1', 'Should have current token');
+
+          await delay(80);
+
+          assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet');
+          assert.equal(listener.errors.length, 0, 'Should not have any errors');
+
+          await delay(networkDelay);
+
+          assert.equal(listener.receivedTokens.length, 2, 'Should receive second token');
+          assert.equal(listener.receivedTokens[1].value, 'token2', 'Should have correct token value');
+          assert.equal(listener.receivedTokens[1].expiresAtMs - listener.receivedTokens[1].receivedAtMs,
+            tokenTtl, 'Should have correct TTL');
+          assert.equal(listener.errors.length, 0, 'Should not have any errors');
+          assert.equal(manager.getCurrentToken().value, 'token2', 'Should have current token');
+
+          await delay(80);
+
+          assert.equal(listener.receivedTokens.length, 2, 'Should not receive new token yet');
+          assert.equal(listener.errors.length, 0, 'Should not have any errors');
+
+          await delay(networkDelay);
+
+          assert.equal(listener.receivedTokens.length, 3, 'Should receive third token');
+          assert.equal(listener.receivedTokens[2].value, 'token3', 'Should have correct token value');
+          assert.equal(listener.receivedTokens[2].expiresAtMs - listener.receivedTokens[2].receivedAtMs,
+            tokenTtl, 'Should have correct TTL');
+          assert.equal(listener.errors.length, 0, 'Should not have any errors');
+          assert.equal(manager.getCurrentToken().value, 'token3', 'Should have current token');
+
+          disposable?.dispose();
+        });
+      });
+    });
+  });
+
+  describe('TokenManager error handling', () => {
+
+    describe('error scenarios', () => {
+      it('should not recover if retries are not enabled', async () => {
+
+        const networkDelay = 20;
+        const tokenTtl = 100;
+
+        const config: TokenManagerConfig = {
+          expirationRefreshRatio: 0.8
+        };
+
+        const identityProvider = new ErrorSimulatingProvider(
+          [
+            'token1',
+            new Error('Fatal error'),
+            'token3'
+          ],
+          networkDelay,
+          tokenTtl
+        );
+
+        const manager = new TokenManager(identityProvider, config);
+        const listener = new TestListener();
+        const disposable = manager.start(listener);
+
+        await delay(networkDelay);
+
+        assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token');
+        assert.equal(listener.receivedTokens[0].value, 'token1', 'Should have correct initial token');
+        assert.equal(listener.receivedTokens[0].expiresAtMs - listener.receivedTokens[0].receivedAtMs,
+          tokenTtl, 'Should have correct TTL');
+        assert.equal(listener.errors.length, 0, 'Should not have errors yet');
+
+        await delay(80);
+
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet');
+        assert.equal(listener.errors.length, 0, 'Should not have any errors');
+
+        await delay(networkDelay);
+
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token after failure');
+        assert.equal(listener.errors.length, 1, 'Should receive error');
+        assert.equal(listener.errors[0].message, 'Fatal error', 'Should have correct error message');
+        assert.equal(listener.errors[0].isRetryable, false, 'Should be a fatal error');
+
+        // verify that the token manager is stopped and no more requests are made after the error and expected refresh time
+        await delay(80);
+
+        assert.equal(identityProvider.getRequestCount(), 2, 'Should not make more requests after error');
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token after error');
+        assert.equal(listener.errors.length, 1, 'Should not receive more errors after error');
+        assert.equal(manager.isRunning(), false, 'Should stop token manager after error');
+
+        disposable?.dispose();
+      });
+
+      it('should handle retries with exponential backoff', async () => {
+        const networkDelay = 20;
+        const tokenTtl = 100;
+
+        const config: TokenManagerConfig = {
+          expirationRefreshRatio: 0.8,
+          retry: {
+            maxAttempts: 3,
+            initialDelayMs: 100,
+            maxDelayMs: 1000,
+            backoffMultiplier: 2,
+            isRetryable: (error: unknown) => error instanceof Error && error.message === 'Temporary failure'
+          }
+        };
+
+        const identityProvider = new ErrorSimulatingProvider(
+          [
+            'initial-token',
+            new Error('Temporary failure'),  // First attempt fails
+            new Error('Temporary failure'),  // First retry fails
+            'recovery-token'                 // Second retry succeeds
+          ],
+          networkDelay,
+          tokenTtl
+        );
+
+        const manager = new TokenManager(identityProvider, config);
+        const listener = new TestListener();
+        const disposable = manager.start(listener);
+
+        // Wait for initial token
+        await delay(networkDelay);
+        assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token');
+        assert.equal(listener.receivedTokens[0].value, 'initial-token', 'Should have correct initial token');
+        assert.equal(listener.receivedTokens[0].expiresAtMs - listener.receivedTokens[0].receivedAtMs,
+          tokenTtl, 'Should have correct TTL');
+        assert.equal(listener.errors.length, 0, 'Should not have errors yet');
+
+        await delay(80);
+
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet');
+        assert.equal(listener.errors.length, 0, 'Should not have any errors');
+
+        await delay(networkDelay);
+
+        // Should have first error but not stop due to retry config
+        assert.equal(listener.errors.length, 1, 'Should have first error');
+        assert.ok(listener.errors[0].message.includes('attempt 1'), 'Error should indicate first attempt');
+        assert.equal(listener.errors[0].isRetryable, true, 'Should not be a fatal error');
+        assert.equal(manager.isRunning(), true, 'Should continue running during retries');
+
+        // Advance past first retry (delay: 100ms due to backoff)
+        await delay(100);
+
+        assert.equal(listener.errors.length, 1, 'Should not have the second error yet');
+
+        await delay(networkDelay);
+
+        assert.equal(listener.errors.length, 2, 'Should have second error');
+        assert.ok(listener.errors[1].message.includes('attempt 2'), 'Error should indicate second attempt');
+        assert.equal(listener.errors[0].isRetryable, true, 'Should not be a fatal error');
+        assert.equal(manager.isRunning(), true, 'Should continue running during retries');
+
+        // Advance past second retry (delay: 200ms due to backoff)
+        await delay(200);
+
+        assert.equal(listener.errors.length, 2, 'Should not have another error');
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet');
+
+        await delay(networkDelay);
+
+        // Should have recovered
+        assert.equal(listener.receivedTokens.length, 2, 'Should receive recovery token');
+        assert.equal(listener.receivedTokens[1].value, 'recovery-token', 'Should have correct recovery token');
+        assert.equal(listener.receivedTokens[1].expiresAtMs - listener.receivedTokens[1].receivedAtMs,
+          tokenTtl, 'Should have correct TTL');
+        assert.equal(manager.isRunning(), true, 'Should continue running after recovery');
+        assert.equal(identityProvider.getRequestCount(), 4, 'Should have made exactly 4 requests');
+
+        disposable?.dispose();
+      });
+
+      it('should stop after max retries exceeded', async () => {
+        const networkDelay = 20;
+        const tokenTtl = 100;
+
+        const config: TokenManagerConfig = {
+          expirationRefreshRatio: 0.8,
+          retry: {
+            maxAttempts: 2,  // Only allow 2 retries
+            initialDelayMs: 100,
+            maxDelayMs: 1000,
+            backoffMultiplier: 2,
+            jitterPercentage: 0,
+            isRetryable: (error: unknown) => error instanceof Error && error.message === 'Temporary failure'
+          }
+        };
+
+        // All attempts must fail
+        const identityProvider = new ErrorSimulatingProvider(
+          [
+            'initial-token',
+            new Error('Temporary failure'),
+            new Error('Temporary failure'),
+            new Error('Temporary failure')
+          ],
+          networkDelay,
+          tokenTtl
+        );
+
+        const manager = new TokenManager(identityProvider, config);
+        const listener = new TestListener();
+        const disposable = manager.start(listener);
+
+        // Wait for initial token
+        await delay(networkDelay);
+        assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token');
+
+        await delay(80);
+
+        assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet');
+        assert.equal(listener.errors.length, 0, 'Should not have any errors');
+
+        //wait for the "network call" to complete
+        await delay(networkDelay);
+
+        // First error
+        assert.equal(listener.errors.length, 1, 'Should have first error');
+        assert.equal(manager.isRunning(), true, 'Should continue running after first error');
+        assert.equal(listener.errors[0].isRetryable, true, 'Should not be a fatal error');
+
+        // Advance past first retry
+        await delay(100);
+
+        assert.equal(listener.errors.length, 1, 'Should not have second error yet');
+
+        //wait for the "network call" to complete
+        await delay(networkDelay);
+
+        // Second error
+        assert.equal(listener.errors.length, 2, 'Should have second error');
+        assert.equal(manager.isRunning(), true, 'Should continue running after second error');
+        assert.equal(listener.errors[1].isRetryable, true, 'Should not be a fatal error');
+
+        // Advance past second retry
+        await delay(200);
+
+        assert.equal(listener.errors.length, 2, 'Should not have third error yet');
+
+        //wait for the "network call" to complete
+        await delay(networkDelay);
+
+        // Should stop after max retries
+        assert.equal(listener.errors.length, 3, 'Should have final error');
+        assert.equal(listener.errors[2].isRetryable, false, 'Should be a fatal error');
+        assert.equal(manager.isRunning(), false, 'Should stop after max retries exceeded');
+        assert.equal(identityProvider.getRequestCount(), 4, 'Should have made exactly 4 requests');
+
+        disposable?.dispose();
+
+      });
+    });
+  });
+
+  describe('TokenManager retry delay calculations', () => {
+    const createManager = (retryConfig: Partial<RetryPolicy>) => {
+      const config: TokenManagerConfig = {
+        expirationRefreshRatio: 0.8,
+        retry: {
+          maxAttempts: 3,
+          initialDelayMs: 100,
+          maxDelayMs: 1000,
+          backoffMultiplier: 2,
+          ...retryConfig
+        }
+      };
+      return new TokenManager(new TestIdentityProvider(), config);
+    };
+
+    describe('calculateRetryDelay', () => {
+
+      it('should apply exponential backoff', () => {
+        const manager = createManager({
+          initialDelayMs: 100,
+          backoffMultiplier: 2,
+          jitterPercentage: 0
+        });
+
+        // Test multiple retry attempts
+        const expectedDelays = [
+          [1, 100],    // First attempt: initialDelay * (2^0) = 100
+          [2, 200],    // Second attempt: initialDelay * (2^1) = 200
+          [3, 400],    // Third attempt: initialDelay * (2^2) = 400
+          [4, 800],    // Fourth attempt: initialDelay * (2^3) = 800
+          [5, 1000]    // Fifth attempt: would be 1600, but capped at maxDelay (1000)
+        ];
+
+        for (const [attempt, expectedDelay] of expectedDelays) {
+          manager['retryAttempt'] = attempt;
+          assert.equal(
+            manager.calculateRetryDelay(),
+            expectedDelay,
+            `Incorrect delay for attempt ${attempt}`
+          );
+        }
+      });
+
+      it('should respect maxDelayMs', () => {
+        const manager = createManager({
+          initialDelayMs: 100,
+          maxDelayMs: 300,
+          backoffMultiplier: 2,
+          jitterPercentage: 0
+        });
+
+        // Test that delays are capped at maxDelayMs
+        const expectedDelays = [
+          [1, 100],    // First attempt: 100
+          [2, 200],    // Second attempt: 200
+          [3, 300],    // Third attempt: would be 400, capped at 300
+          [4, 300],    // Fourth attempt: would be 800, capped at 300
+          [5, 300]     // Fifth attempt: would be 1600, capped at 300
+        ];
+
+        for (const [attempt, expectedDelay] of expectedDelays) {
+          manager['retryAttempt'] = attempt;
+          assert.equal(
+            manager.calculateRetryDelay(),
+            expectedDelay,
+            `Incorrect delay for attempt ${attempt}`
+          );
+        }
+      });
+
+      it('should return 0 when no retry config is present', () => {
+        const manager = new TokenManager(new TestIdentityProvider(), {
+          expirationRefreshRatio: 0.8
+        });
+        manager['retryAttempt'] = 1;
+        assert.equal(manager.calculateRetryDelay(), 0);
+      });
+    });
+  });
+});
+
diff --git a/packages/client/lib/authx/token-manager.ts b/packages/client/lib/authx/token-manager.ts
new file mode 100644
index 00000000000..6532d88317b
--- /dev/null
+++ b/packages/client/lib/authx/token-manager.ts
@@ -0,0 +1,318 @@
+import { IdentityProvider, TokenResponse } from './identity-provider';
+import { Token } from './token';
+import {Disposable} from './disposable';
+
+/**
+ * The configuration for retrying token refreshes.
+ */
+export interface RetryPolicy {
+  /**
+   * The maximum number of attempts to retry token refreshes.
+   */
+  maxAttempts: number;
+
+  /**
+   * The initial delay in milliseconds before the first retry.
+   */
+  initialDelayMs: number;
+
+  /**
+   * The maximum delay in milliseconds between retries.
+   * The calculated delay will be capped at this value.
+   */
+  maxDelayMs: number;
+
+  /**
+   * The multiplier for exponential backoff between retries.
+   * @example
+   * A value of 2 will double the delay each time:
+   * - 1st retry: initialDelayMs
+   * - 2nd retry: initialDelayMs * 2
+   * - 3rd retry: initialDelayMs * 4
+   */
+  backoffMultiplier: number;
+
+  /**
+   * The percentage of jitter to apply to the delay.
+   * @example
+   * A value of 0.1 will add or subtract up to 10% of the delay.
+   */
+  jitterPercentage?: number;
+
+  /**
+   * Function to classify errors from the identity provider as retryable or non-retryable.
+   * Used to determine if a token refresh failure should be retried based on the type of error.
+   *
+   * The default behavior is to retry all types of errors if no function is provided.
+   *
+   * Common use cases:
+   * - Network errors that may be transient (should retry)
+   * - Invalid credentials (should not retry)
+   * - Rate limiting responses (should retry)
+   *
+   * @param error - The error from the identity provider3
+   * @param attempt - Current retry attempt (0-based)
+   * @returns `true` if the error is considered transient and the operation should be retried
+   *
+   * @example
+   * ```typescript
+   * const retryPolicy: RetryPolicy = {
+   *   maxAttempts: 3,
+   *   initialDelayMs: 1000,
+   *   maxDelayMs: 5000,
+   *   backoffMultiplier: 2,
+   *   isRetryable: (error) => {
+   *     // Retry on network errors or rate limiting
+   *     return error instanceof NetworkError ||
+   *            error instanceof RateLimitError;
+   *   }
+   * };
+   * ```
+   */
+  isRetryable?: (error: unknown, attempt: number) => boolean;
+}
+
+/**
+ * the configuration for the TokenManager.
+ */
+export interface TokenManagerConfig {
+
+  /**
+   * Represents the ratio of a token's lifetime at which a refresh should be triggered.
+   * For example, a value of 0.75 means the token should be refreshed when 75% of its lifetime has elapsed (or when
+   * 25% of its lifetime remains).
+   */
+  expirationRefreshRatio: number;
+
+  // The retry policy for token refreshes. If not provided, no retries will be attempted.
+  retry?: RetryPolicy;
+}
+
+/**
+ * IDPError indicates a failure from the identity provider.
+ *
+ * The `isRetryable` flag is determined by the RetryPolicy's error classification function - if an error is
+ * classified as retryable, it will be marked as transient and the token manager will attempt to recover.
+ */
+export class IDPError extends Error {
+  constructor(public readonly message: string, public readonly isRetryable: boolean) {
+    super(message);
+    this.name = 'IDPError';
+  }
+}
+
+/**
+ * TokenStreamListener is an interface for objects that listen to token changes.
+ */
+export type TokenStreamListener<T> = {
+  /**
+   * Called each time a new token is received.
+   * @param token
+   */
+  onNext: (token: Token<T>) => void;
+
+  /**
+   * Called when an error occurs while calling the underlying IdentityProvider. The error can be
+   * transient and the token manager will attempt to obtain a token again if retry policy is configured.
+   *
+   * Only fatal errors will terminate the stream and stop the token manager.
+   *
+   * @param error
+   */
+  onError: (error: IDPError) => void;
+
+}
+
+/**
+ * TokenManager is responsible for obtaining/refreshing tokens and notifying listeners about token changes.
+ * It uses an IdentityProvider to request tokens. The token refresh is scheduled based on the token's TTL and
+ * the expirationRefreshRatio configuration.
+ *
+ * The TokenManager should be disposed when it is no longer needed by calling the dispose method on the Disposable
+ * returned by start.
+ */
+export class TokenManager<T> {
+  private currentToken: Token<T> | null = null;
+  private refreshTimeout: NodeJS.Timeout | null = null;
+  private listener: TokenStreamListener<T> | null = null;
+  private retryAttempt: number = 0;
+
+  constructor(
+    private readonly identityProvider: IdentityProvider<T>,
+    private readonly config: TokenManagerConfig
+  ) {
+    if (this.config.expirationRefreshRatio > 1) {
+      throw new Error('expirationRefreshRatio must be less than or equal to 1');
+    }
+    if (this.config.expirationRefreshRatio < 0) {
+      throw new Error('expirationRefreshRatio must be greater or equal to 0');
+    }
+  }
+
+  /**
+   * Starts the token manager and returns a Disposable that can be used to stop the token manager.
+   *
+   * @param listener The listener that will receive token updates.
+   * @param initialDelayMs The initial delay in milliseconds before the first token refresh.
+   */
+  public start(listener: TokenStreamListener<T>, initialDelayMs: number = 0): Disposable {
+    if (this.listener) {
+      this.stop();
+    }
+
+    this.listener = listener;
+    this.retryAttempt = 0;
+
+    this.scheduleNextRefresh(initialDelayMs);
+
+    return {
+      dispose: () => this.stop()
+    };
+  }
+
+  public calculateRetryDelay(): number {
+    if (!this.config.retry) return 0;
+
+    const { initialDelayMs, maxDelayMs, backoffMultiplier, jitterPercentage } = this.config.retry;
+
+    let delay = initialDelayMs * Math.pow(backoffMultiplier, this.retryAttempt - 1);
+
+    delay = Math.min(delay, maxDelayMs);
+
+    if (jitterPercentage) {
+      const jitterRange = delay * (jitterPercentage / 100);
+      const jitterAmount = Math.random() * jitterRange - (jitterRange / 2);
+      delay += jitterAmount;
+    }
+
+    let result = Math.max(0, Math.floor(delay));
+
+    return result;
+  }
+
+  private shouldRetry(error: unknown): boolean {
+    if (!this.config.retry) return false;
+
+    const { maxAttempts, isRetryable } = this.config.retry;
+
+    if (this.retryAttempt >= maxAttempts) {
+      return false;
+    }
+
+    if (isRetryable) {
+      return isRetryable(error, this.retryAttempt);
+    }
+
+    return false;
+  }
+
+  public isRunning(): boolean {
+    return this.listener !== null;
+  }
+
+  private async refresh(): Promise<void> {
+    if (!this.listener) {
+      throw new Error('TokenManager is not running, but refresh was called');
+    }
+
+    try {
+      await this.identityProvider.requestToken().then(this.handleNewToken);
+      this.retryAttempt = 0;
+    } catch (error) {
+
+      if (this.shouldRetry(error)) {
+        this.retryAttempt++;
+        const retryDelay = this.calculateRetryDelay();
+        this.notifyError(`Token refresh failed (attempt ${this.retryAttempt}), retrying in ${retryDelay}ms: ${error}`, true)
+        this.scheduleNextRefresh(retryDelay);
+      } else {
+        this.notifyError(error, false);
+        this.stop();
+      }
+    }
+  }
+
+  private handleNewToken = async ({ token: nativeToken, ttlMs }: TokenResponse<T>): Promise<void> => {
+    if (!this.listener) {
+      throw new Error('TokenManager is not running, but a new token was received');
+    }
+    const token = this.wrapAndSetCurrentToken(nativeToken, ttlMs);
+    this.listener.onNext(token);
+
+    this.scheduleNextRefresh(this.calculateRefreshTime(token));
+  }
+
+  /**
+   * Creates a Token object from a native token and sets it as the current token.
+   *
+   * @param nativeToken - The raw token received from the identity provider
+   * @param ttlMs - Time-to-live in milliseconds for the token
+   *
+   * @returns A new Token instance containing the wrapped native token and expiration details
+   *
+   */
+  public wrapAndSetCurrentToken(nativeToken: T, ttlMs: number): Token<T> {
+    const now = Date.now();
+    const token = new Token(
+      nativeToken,
+      now + ttlMs,
+      now
+    );
+    this.currentToken = token;
+    return token;
+  }
+
+  private scheduleNextRefresh(delayMs: number): void {
+    if (this.refreshTimeout) {
+      clearTimeout(this.refreshTimeout);
+      this.refreshTimeout = null;
+    }
+    if (delayMs === 0) {
+      this.refresh();
+    } else {
+      this.refreshTimeout = setTimeout(() => this.refresh(), delayMs);
+    }
+
+  }
+
+  /**
+   * Calculates the time in milliseconds when the token should be refreshed
+   * based on the token's TTL and the expirationRefreshRatio configuration.
+   *
+   * @param token The token to calculate the refresh time for.
+   * @param now The current time in milliseconds. Defaults to Date.now().
+   */
+  public calculateRefreshTime(token: Token<T>, now: number = Date.now()): number {
+    const ttlMs = token.getTtlMs(now);
+    return Math.floor(ttlMs * this.config.expirationRefreshRatio);
+  }
+
+  private stop(): void {
+
+    if (this.refreshTimeout) {
+      clearTimeout(this.refreshTimeout);
+      this.refreshTimeout = null;
+    }
+
+    this.listener = null;
+    this.currentToken = null;
+    this.retryAttempt = 0;
+  }
+
+  /**
+   * Returns the current token or null if no token is available.
+   */
+  public getCurrentToken(): Token<T> | null {
+    return this.currentToken;
+  }
+
+  private notifyError(error: unknown, isRetryable: boolean): void {
+    const errorMessage = error instanceof Error ? error.message : String(error);
+
+    if (!this.listener) {
+      throw new Error(`TokenManager is not running but received an error: ${errorMessage}`);
+    }
+
+    this.listener.onError(new IDPError(errorMessage, isRetryable));
+  }
+}
\ No newline at end of file
diff --git a/packages/client/lib/authx/token.ts b/packages/client/lib/authx/token.ts
new file mode 100644
index 00000000000..3d6e6867d84
--- /dev/null
+++ b/packages/client/lib/authx/token.ts
@@ -0,0 +1,23 @@
+/**
+ * A token that can be used to authenticate with a service.
+ */
+export class Token<T> {
+  constructor(
+    public readonly value: T,
+    //represents the token deadline - the time in milliseconds since the Unix epoch at which the token expires
+    public readonly expiresAtMs: number,
+    //represents the time in milliseconds since the Unix epoch at which the token was received
+    public readonly receivedAtMs: number
+  ) {}
+
+  /**
+   * Returns the time-to-live of the token in milliseconds.
+   * @param now The current time in milliseconds since the Unix epoch.
+   */
+  getTtlMs(now: number): number {
+    if (this.expiresAtMs < now) {
+      return 0;
+    }
+    return this.expiresAtMs - now;
+  }
+}
\ No newline at end of file
diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts
index cd2040ec97f..c71cf1a1fad 100644
--- a/packages/client/lib/client/index.spec.ts
+++ b/packages/client/lib/client/index.spec.ts
@@ -1,6 +1,6 @@
 import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
-import RedisClient, { RedisClientType } from '.';
+import RedisClient, { RedisClientOptions, RedisClientType } from '.';
 import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, WatchError } from '../errors';
 import { defineScript } from '../lua-script';
 import { spy } from 'sinon';
@@ -25,36 +25,87 @@ export const SQUARE_SCRIPT = defineScript({
 
 describe('Client', () => {
   describe('parseURL', () => {
-    it('redis://user:secret@localhost:6379/0', () => {
-      assert.deepEqual(
-        RedisClient.parseURL('redis://user:secret@localhost:6379/0'),
-        {
-          socket: {
-            host: 'localhost',
-            port: 6379
-          },
-          username: 'user',
-          password: 'secret',
-          database: 0
+    it('redis://user:secret@localhost:6379/0', async () => {
+      const result = RedisClient.parseURL('redis://user:secret@localhost:6379/0');
+      const expected : RedisClientOptions = {
+        socket: {
+          host: 'localhost',
+          port: 6379
+        },
+        username: 'user',
+        password: 'secret',
+        database: 0,
+        credentialsProvider: {
+          type: 'async-credentials-provider',
+          credentials: async () => ({
+            password: 'secret',
+            username: 'user'
+          })
         }
-      );
+      };
+
+      // Compare everything except the credentials function
+      const { credentialsProvider: resultCredProvider, ...resultRest } = result;
+      const { credentialsProvider: expectedCredProvider, ...expectedRest } = expected;
+
+      // Compare non-function properties
+      assert.deepEqual(resultRest, expectedRest);
+
+      if(result.credentialsProvider.type === 'async-credentials-provider'
+        && expected.credentialsProvider.type === 'async-credentials-provider') {
+
+        // Compare the actual output of the credentials functions
+        const resultCreds = await result.credentialsProvider.credentials();
+        const expectedCreds = await expected.credentialsProvider.credentials();
+        assert.deepEqual(resultCreds, expectedCreds);
+      } else {
+        assert.fail('Credentials provider type mismatch');
+      }
+
+
     });
 
-    it('rediss://user:secret@localhost:6379/0', () => {
-      assert.deepEqual(
-        RedisClient.parseURL('rediss://user:secret@localhost:6379/0'),
-        {
-          socket: {
-            host: 'localhost',
-            port: 6379,
-            tls: true
-          },
-          username: 'user',
-          password: 'secret',
-          database: 0
+    it('rediss://user:secret@localhost:6379/0', async () => {
+      const result = RedisClient.parseURL('rediss://user:secret@localhost:6379/0');
+      const expected: RedisClientOptions = {
+        socket: {
+          host: 'localhost',
+          port: 6379,
+          tls: true
+        },
+        username: 'user',
+        password: 'secret',
+        database: 0,
+        credentialsProvider: {
+          credentials: async () => ({
+            password: 'secret',
+            username: 'user'
+          }),
+          type: 'async-credentials-provider'
         }
-      );
-    });
+      };
+
+      // Compare everything except the credentials function
+      const { credentialsProvider: resultCredProvider, ...resultRest } = result;
+      const { credentialsProvider: expectedCredProvider, ...expectedRest } = expected;
+
+      // Compare non-function properties
+      assert.deepEqual(resultRest, expectedRest);
+      assert.equal(resultCredProvider.type, expectedCredProvider.type);
+
+      if (result.credentialsProvider.type === 'async-credentials-provider' &&
+        expected.credentialsProvider.type === 'async-credentials-provider') {
+
+        // Compare the actual output of the credentials functions
+        const resultCreds = await result.credentialsProvider.credentials();
+        const expectedCreds = await expected.credentialsProvider.credentials();
+        assert.deepEqual(resultCreds, expectedCreds);
+
+      } else {
+        assert.fail('Credentials provider type mismatch');
+      }
+
+    })
 
     it('Invalid protocol', () => {
       assert.throws(
@@ -90,6 +141,21 @@ describe('Client', () => {
       );
     }, GLOBAL.SERVERS.PASSWORD);
 
+    testUtils.testWithClient('Client can authenticate asynchronously ', async client => {
+      assert.equal(
+        await client.ping(),
+        'PONG'
+      );
+    }, GLOBAL.SERVERS.ASYNC_BASIC_AUTH);
+
+    testUtils.testWithClient('Client can authenticate using the streaming credentials provider for initial token acquisition',
+      async client => {
+      assert.equal(
+        await client.ping(),
+        'PONG'
+      );
+    }, GLOBAL.SERVERS.STREAMING_AUTH);
+
     testUtils.testWithClient('should execute AUTH before SELECT', async client => {
       assert.equal(
         (await client.clientInfo()).db,
@@ -294,6 +360,7 @@ describe('Client', () => {
           assert.equal(err.replies.length, 2);
           assert.deepEqual(err.errorIndexes, [1]);
           assert.ok(err.replies[1] instanceof ErrorReply);
+          // @ts-ignore TS2802
           assert.deepEqual([...err.errors()], [err.replies[1]]);
           return true;
         }
diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts
index 55355a133dd..5dae1271ecb 100644
--- a/packages/client/lib/client/index.ts
+++ b/packages/client/lib/client/index.ts
@@ -1,5 +1,6 @@
 import COMMANDS from '../commands';
 import RedisSocket, { RedisSocketOptions } from './socket';
+import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsProvider, UnableToObtainNewCredentialsError, Disposable } from '../authx';
 import RedisCommandsQueue, { CommandOptions } from './commands-queue';
 import { EventEmitter } from 'node:events';
 import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
@@ -42,6 +43,13 @@ export interface RedisClientOptions<
    * ACL password or the old "--requirepass" password
    */
   password?: string;
+
+  /**
+   * Provides credentials for authentication. Can be set directly or will be created internally
+   * if username/password are provided instead. If both are supplied, this credentialsProvider
+   * takes precedence over username/password.
+   */
+  credentialsProvider?: CredentialsProvider;
   /**
    * Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname))
    */
@@ -261,6 +269,17 @@ export default class RedisClient<
       parsed.password = decodeURIComponent(password);
     }
 
+    if (username || password) {
+      parsed.credentialsProvider = {
+        type: 'async-credentials-provider',
+        credentials: async () => (
+          {
+            username: username ? decodeURIComponent(username) : undefined,
+            password: password ? decodeURIComponent(password) : undefined
+          })
+      };
+    }
+
     if (pathname.length > 1) {
       const database = Number(pathname.substring(1));
       if (isNaN(database)) {
@@ -284,6 +303,8 @@ export default class RedisClient<
   #epoch: number;
   #watchEpoch?: number; 
 
+  #credentialsSubscription: Disposable | null = null;
+
   get options(): RedisClientOptions<M, F, S, RESP> | undefined {
     return this._self.#options;
   }
@@ -317,6 +338,19 @@ export default class RedisClient<
   }
 
   #initiateOptions(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>): RedisClientOptions<M, F, S, RESP, TYPE_MAPPING> | undefined {
+
+    // Convert username/password to credentialsProvider if no credentialsProvider is already in place
+    if (!options?.credentialsProvider && (options?.username || options?.password)) {
+
+      options.credentialsProvider = {
+        type: 'async-credentials-provider',
+        credentials: async () => ({
+          username: options.username,
+          password: options.password
+        })
+      };
+    }
+
     if (options?.url) {
       const parsed = RedisClient.parseURL(options.url);
       if (options.socket) {
@@ -345,17 +379,65 @@ export default class RedisClient<
     );
   }
 
-  #handshake(selectedDB: number) {
+  /**
+   * @param credentials
+   */
+  private reAuthenticate = async (credentials: BasicAuth) => {
+    // Re-authentication is not supported on RESP2 with PubSub active
+    if (!(this.isPubSubActive && !this.#options?.RESP)) {
+      await this.sendCommand(
+        parseArgs(COMMANDS.AUTH, {
+          username: credentials.username,
+          password: credentials.password ?? ''
+        })
+      );
+    }
+  }
+
+   #subscribeForStreamingCredentials(cp: StreamingCredentialsProvider): Promise<[BasicAuth, Disposable]> {
+    return cp.subscribe({
+      onNext: credentials => {
+        this.reAuthenticate(credentials).catch(error => {
+          const errorMessage = error instanceof Error ? error.message : String(error);
+          cp.onReAuthenticationError(new CredentialsError(errorMessage));
+        });
+
+      },
+      onError: (e: Error) => {
+        const errorMessage = `Error from streaming credentials provider: ${e.message}`;
+        cp.onReAuthenticationError(new UnableToObtainNewCredentialsError(errorMessage));
+      }
+    });
+  }
+
+  async #handshake(selectedDB: number) {
     const commands = [];
+    const cp = this.#options?.credentialsProvider;
 
     if (this.#options?.RESP) {
       const hello: HelloOptions = {};
 
-      if (this.#options.password) {
-        hello.AUTH = {
-          username: this.#options.username ?? 'default',
-          password: this.#options.password
-        };
+      if (cp && cp.type === 'async-credentials-provider') {
+        const credentials = await cp.credentials();
+        if (credentials.password) {
+          hello.AUTH = {
+            username: credentials.username ?? 'default',
+            password: credentials.password
+          };
+        }
+      }
+
+      if (cp && cp.type === 'streaming-credentials-provider') {
+
+        const [credentials, disposable]  = await this.#subscribeForStreamingCredentials(cp)
+        this.#credentialsSubscription = disposable;
+
+        if (credentials.password) {
+          hello.AUTH = {
+            username: credentials.username ?? 'default',
+            password: credentials.password
+          };
+        }
       }
 
       if (this.#options.name) {
@@ -366,13 +448,34 @@ export default class RedisClient<
         parseArgs(HELLO, this.#options.RESP, hello)
       );
     } else {
-      if (this.#options?.username || this.#options?.password) {
-        commands.push(
-          parseArgs(COMMANDS.AUTH, {
-            username: this.#options.username,
-            password: this.#options.password ?? ''
-          })
-        );
+
+      if (cp && cp.type === 'async-credentials-provider') {
+
+        const credentials = await cp.credentials();
+
+        if (credentials.username || credentials.password) {
+          commands.push(
+            parseArgs(COMMANDS.AUTH, {
+              username: credentials.username,
+              password: credentials.password ?? ''
+            })
+          );
+        }
+      }
+
+      if (cp && cp.type === 'streaming-credentials-provider') {
+
+        const [credentials, disposable]  = await this.#subscribeForStreamingCredentials(cp)
+        this.#credentialsSubscription = disposable;
+
+        if (credentials.username || credentials.password) {
+          commands.push(
+            parseArgs(COMMANDS.AUTH, {
+              username: credentials.username,
+              password: credentials.password ?? ''
+            })
+          );
+        }
       }
 
       if (this.#options?.name) {
@@ -396,7 +499,7 @@ export default class RedisClient<
   }
 
   #initiateSocket(): RedisSocket {
-    const socketInitiator = () => {
+    const socketInitiator = async () => {
       const promises = [],
         chainId = Symbol('Socket Initiator');
 
@@ -418,7 +521,7 @@ export default class RedisClient<
         );
       }
 
-      const commands = this.#handshake(this.#selectedDB);
+      const commands = await this.#handshake(this.#selectedDB);
       for (let i = commands.length - 1; i >= 0; --i) {
         promises.push(
           this.#queue.addCommand(commands[i], {
@@ -1000,7 +1103,9 @@ export default class RedisClient<
     const chainId = Symbol('Reset Chain'),
       promises = [this._self.#queue.reset(chainId)],
       selectedDB = this._self.#options?.database ?? 0;
-    for (const command of this._self.#handshake(selectedDB)) {
+    this._self.#credentialsSubscription?.dispose();
+    this._self.#credentialsSubscription = null;
+    for (const command of (await this._self.#handshake(selectedDB))) {
       promises.push(
         this._self.#queue.addCommand(command, {
           chainId
@@ -1051,6 +1156,8 @@ export default class RedisClient<
    * @deprecated use .close instead
    */
   QUIT(): Promise<string> {
+    this._self.#credentialsSubscription?.dispose();
+    this._self.#credentialsSubscription = null;
     return this._self.#socket.quit(async () => {
       clearTimeout(this._self.#pingTimer);
       const quitPromise = this._self.#queue.addCommand<string>(['QUIT']);
@@ -1089,6 +1196,8 @@ export default class RedisClient<
         resolve();
       };
       this._self.#socket.on('data', maybeClose);
+      this._self.#credentialsSubscription?.dispose();
+      this._self.#credentialsSubscription = null;
     });
   }
 
@@ -1099,6 +1208,8 @@ export default class RedisClient<
     clearTimeout(this._self.#pingTimer);
     this._self.#queue.flushAll(new DisconnectsClientError());
     this._self.#socket.destroy();
+    this._self.#credentialsSubscription?.dispose();
+    this._self.#credentialsSubscription = null;
   }
 
   ref() {
diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts
index 083c9127e5b..2d561dd2e20 100644
--- a/packages/client/lib/test-utils.ts
+++ b/packages/client/lib/test-utils.ts
@@ -1,6 +1,7 @@
 import TestUtils from '@redis/test-utils';
 import { SinonSpy } from 'sinon';
 import { setTimeout } from 'node:timers/promises';
+import { CredentialsProvider } from './authx';
 import { Command } from './RESP/types';
 import { BasicCommandParser } from './client/parser';
 
@@ -16,6 +17,31 @@ const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ?
   ['--enable-debug-command', 'yes'] :
   [];
 
+const asyncBasicAuthCredentialsProvider: CredentialsProvider =
+  {
+    type: 'async-credentials-provider',
+    credentials: async () => ({ password: 'password' })
+  } as const;
+
+const streamingCredentialsProvider: CredentialsProvider =
+  {
+    type: 'streaming-credentials-provider',
+
+    subscribe : (observable) => ( Promise.resolve([
+     { password: 'password' },
+      {
+       dispose: () => {
+          console.log('disposing credentials provider subscription');
+        }
+      }
+    ])),
+
+    onReAuthenticationError: (error) => {
+      console.error('re-authentication error', error);
+    }
+
+  } as const;
+
 export const GLOBAL = {
   SERVERS: {
     OPEN: {
@@ -26,6 +52,18 @@ export const GLOBAL = {
       clientOptions: {
         password: 'password'
       }
+    },
+    ASYNC_BASIC_AUTH: {
+      serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
+      clientOptions: {
+        credentialsProvider: asyncBasicAuthCredentialsProvider
+      }
+    },
+    STREAMING_AUTH: {
+      serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
+      clientOptions: {
+        credentialsProvider: streamingCredentialsProvider
+      }
     }
   },
   CLUSTERS: {
diff --git a/packages/entraid/.nycrc.json b/packages/entraid/.nycrc.json
new file mode 100644
index 00000000000..848af2b5a27
--- /dev/null
+++ b/packages/entraid/.nycrc.json
@@ -0,0 +1,10 @@
+{
+  "extends": "@istanbuljs/nyc-config-typescript",
+  "exclude": [
+    "integration-tests",
+    "samples",
+    "dist",
+    "**/*.spec.ts",
+    "lib/test-utils.ts"
+  ]
+}
diff --git a/packages/entraid/.release-it.json b/packages/entraid/.release-it.json
new file mode 100644
index 00000000000..a5f3a31062e
--- /dev/null
+++ b/packages/entraid/.release-it.json
@@ -0,0 +1,11 @@
+{
+  "git": {
+    "tagName": "entraid@${version}",
+    "commitMessage": "Release ${tagName}",
+    "tagAnnotation": "Release ${tagName}"
+  },
+  "npm": {
+    "versionArgs": ["--workspaces-update=false"],
+    "publishArgs": ["--access", "public"]
+  }
+}
diff --git a/packages/entraid/README.md b/packages/entraid/README.md
new file mode 100644
index 00000000000..e9c7956022e
--- /dev/null
+++ b/packages/entraid/README.md
@@ -0,0 +1,137 @@
+# @redis/entraid
+
+Secure token-based authentication for Redis clients using Microsoft Entra ID (formerly Azure Active Directory).
+
+## Features
+
+- Token-based authentication using Microsoft Entra ID
+- Automatic token refresh before expiration
+- Automatic re-authentication of all connections after token refresh
+- Support for multiple authentication flows:
+  - Managed identities (system-assigned and user-assigned)
+  - Service principals (with or without certificates)
+  - Authorization Code with PKCE flow
+- Built-in retry mechanisms for transient failures
+
+## Installation
+
+```bash
+npm install @redis/client
+npm install @redis/entraid
+```
+
+## Getting Started
+
+The first step to using @redis/entraid is choosing the right credentials provider for your authentication needs. The `EntraIdCredentialsProviderFactory` class provides several factory methods to create the appropriate provider:
+
+- `createForSystemAssignedManagedIdentity`: Use when your application runs in Azure with a system-assigned managed identity
+- `createForUserAssignedManagedIdentity`: Use when your application runs in Azure with a user-assigned managed identity
+- `createForClientCredentials`: Use when authenticating with a service principal using client secret
+- `createForClientCredentialsWithCertificate`: Use when authenticating with a service principal using a certificate
+- `createForAuthorizationCodeWithPKCE`: Use for interactive authentication flows in user applications
+
+## Usage Examples
+
+### Service Principal Authentication
+
+```typescript
+import { createClient } from '@redis/client';
+import { EntraIdCredentialsProviderFactory } from '@redis/entraid';
+
+const provider = EntraIdCredentialsProviderFactory.createForClientCredentials({
+  clientId: 'your-client-id',
+  clientSecret: 'your-client-secret',
+  authorityConfig: {
+    type: 'multi-tenant',
+    tenantId: 'your-tenant-id'
+  },
+  tokenManagerConfig: {
+    expirationRefreshRatio: 0.8 // Refresh token after 80% of its lifetime
+  }
+});
+
+const client = createClient({
+  url: 'redis://your-host',
+  credentialsProvider: provider
+});
+
+await client.connect();
+```
+
+### System-Assigned Managed Identity
+
+```typescript
+const provider = EntraIdCredentialsProviderFactory.createForSystemAssignedManagedIdentity({
+  clientId: 'your-client-id',
+  tokenManagerConfig: {
+    expirationRefreshRatio: 0.8
+  }
+});
+```
+
+### User-Assigned Managed Identity
+
+```typescript
+const provider = EntraIdCredentialsProviderFactory.createForUserAssignedManagedIdentity({
+  clientId: 'your-client-id',
+  userAssignedClientId: 'your-user-assigned-client-id',
+  tokenManagerConfig: {
+    expirationRefreshRatio: 0.8
+  }
+});
+```
+
+## Important Limitations
+
+### RESP2 PUB/SUB Limitations
+
+When using RESP2 (Redis Serialization Protocol 2), there are important limitations with PUB/SUB:
+
+- **No Re-Authentication in PUB/SUB Mode**: In RESP2, once a connection enters PUB/SUB mode, the socket is blocked and cannot process out-of-band commands like AUTH. This means that connections in PUB/SUB mode cannot be re-authenticated when tokens are refreshed.
+- **Connection Eviction**: As a result, PUB/SUB connections will be evicted by the Redis proxy when their tokens expire. The client will need to establish new connections with fresh tokens.
+
+### Transaction Safety
+
+When using token-based authentication, special care must be taken with Redis transactions. The token manager runs in the background and may attempt to re-authenticate connections at any time by sending AUTH commands. This can interfere with manually constructed transactions.
+
+#### ✅ Recommended: Use the Official Transaction API
+
+Always use the official transaction API provided by the client:
+
+```typescript
+// Correct way to handle transactions
+const multi = client.multi();
+multi.set('key1', 'value1');
+multi.set('key2', 'value2');
+await multi.exec();
+```
+
+#### ❌ Avoid: Manual Transaction Construction
+
+Do not manually construct transactions by sending individual MULTI/EXEC commands:
+
+```typescript
+// Incorrect and potentially dangerous
+await client.sendCommand(['MULTI']);
+await client.sendCommand(['SET', 'key1', 'value1']);
+await client.sendCommand(['SET', 'key2', 'value2']);
+await client.sendCommand(['EXEC']); // Risk of AUTH command being injected before EXEC
+```
+
+## Error Handling
+
+The provider includes built-in retry mechanisms for transient errors:
+
+```typescript
+const provider = EntraIdCredentialsProviderFactory.createForClientCredentials({
+  // ... other config ...
+  tokenManagerConfig: {
+    retry: {
+      maxAttempts: 3,
+      initialDelayMs: 100,
+      maxDelayMs: 1000,
+      backoffMultiplier: 2
+    }
+  }
+});
+```
diff --git a/packages/entraid/integration-tests/entraid-integration.spec.ts b/packages/entraid/integration-tests/entraid-integration.spec.ts
new file mode 100644
index 00000000000..deb1d47dec1
--- /dev/null
+++ b/packages/entraid/integration-tests/entraid-integration.spec.ts
@@ -0,0 +1,217 @@
+import { BasicAuth } from '@redis/client/dist/lib/authx';
+import { createClient } from '@redis/client';
+import { EntraIdCredentialsProviderFactory } from '../lib/entra-id-credentials-provider-factory';
+import { strict as assert } from 'node:assert';
+import { spy, SinonSpy } from 'sinon';
+import { randomUUID } from 'crypto';
+import { loadFromFile, RedisEndpointsConfig } from '@redis/test-utils/lib/cae-client-testing';
+import { EntraidCredentialsProvider } from '../lib/entraid-credentials-provider';
+import * as crypto from 'node:crypto';
+
+describe('EntraID Integration Tests', () => {
+
+  it('client configured with client secret should be able to authenticate/re-authenticate', async () => {
+    const config = await readConfigFromEnv();
+    await runAuthenticationTest(() =>
+      EntraIdCredentialsProviderFactory.createForClientCredentials({
+        clientId: config.clientId,
+        clientSecret: config.clientSecret,
+        authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId },
+        tokenManagerConfig: {
+          expirationRefreshRatio: 0.0001
+        }
+      })
+    );
+  });
+
+  it('client configured with client certificate should be able to authenticate/re-authenticate', async () => {
+    const config = await readConfigFromEnv();
+    await runAuthenticationTest(() =>
+      EntraIdCredentialsProviderFactory.createForClientCredentialsWithCertificate({
+        clientId: config.clientId,
+        certificate: convertCertsForMSAL(config.cert, config.privateKey),
+        authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId },
+        tokenManagerConfig: {
+          expirationRefreshRatio: 0.0001
+        }
+      })
+    );
+  });
+
+  it('client with system managed identity should be able to authenticate/re-authenticate', async () => {
+    const config = await readConfigFromEnv();
+    await runAuthenticationTest(() =>
+      EntraIdCredentialsProviderFactory.createForSystemAssignedManagedIdentity({
+        clientId: config.clientId,
+        authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId },
+        tokenManagerConfig: {
+          expirationRefreshRatio: 0.00001
+        }
+      })
+    );
+  });
+
+  interface TestConfig {
+    clientId: string;
+    clientSecret: string;
+    authority: string;
+    tenantId: string;
+    redisScopes: string;
+    cert: string;
+    privateKey: string;
+    userAssignedManagedId: string;
+    endpoints: RedisEndpointsConfig;
+  }
+
+  const readConfigFromEnv = async (): Promise<TestConfig> => {
+    const requiredEnvVars = {
+      AZURE_CLIENT_ID: process.env.AZURE_CLIENT_ID,
+      AZURE_CLIENT_SECRET: process.env.AZURE_CLIENT_SECRET,
+      AZURE_AUTHORITY: process.env.AZURE_AUTHORITY,
+      AZURE_TENANT_ID: process.env.AZURE_TENANT_ID,
+      AZURE_REDIS_SCOPES: process.env.AZURE_REDIS_SCOPES,
+      AZURE_CERT: process.env.AZURE_CERT,
+      AZURE_PRIVATE_KEY: process.env.AZURE_PRIVATE_KEY,
+      AZURE_USER_ASSIGNED_MANAGED_ID: process.env.AZURE_USER_ASSIGNED_MANAGED_ID,
+      REDIS_ENDPOINTS_CONFIG_PATH: process.env.REDIS_ENDPOINTS_CONFIG_PATH
+    };
+
+    Object.entries(requiredEnvVars).forEach(([key, value]) => {
+      if (value == undefined) {
+        throw new Error(`${key} environment variable must be set`);
+      }
+    });
+
+    return {
+      endpoints: await loadFromFile(requiredEnvVars.REDIS_ENDPOINTS_CONFIG_PATH),
+      clientId: requiredEnvVars.AZURE_CLIENT_ID,
+      clientSecret: requiredEnvVars.AZURE_CLIENT_SECRET,
+      authority: requiredEnvVars.AZURE_AUTHORITY,
+      tenantId: requiredEnvVars.AZURE_TENANT_ID,
+      redisScopes: requiredEnvVars.AZURE_REDIS_SCOPES,
+      cert: requiredEnvVars.AZURE_CERT,
+      privateKey: requiredEnvVars.AZURE_PRIVATE_KEY,
+      userAssignedManagedId: requiredEnvVars.AZURE_USER_ASSIGNED_MANAGED_ID
+    };
+  };
+
+  interface TokenDetail {
+    token: string;
+    exp: number;
+    iat: number;
+    lifetime: number;
+    uti: string;
+  }
+
+  const setupTestClient = async (credentialsProvider: EntraidCredentialsProvider) => {
+    const config = await readConfigFromEnv();
+    const client = createClient({
+      url: config.endpoints['standalone-entraid-acl'].endpoints[0],
+      credentialsProvider
+    });
+
+    const clientInstance = (client as any)._self;
+    const reAuthSpy: SinonSpy = spy(clientInstance, 'reAuthenticate');
+
+    return { client, reAuthSpy };
+  };
+
+  const runClientOperations = async (client: any) => {
+    const startTime = Date.now();
+    while (Date.now() - startTime < 1000) {
+      const key = randomUUID();
+      await client.set(key, 'value');
+      const value = await client.get(key);
+      assert.equal(value, 'value');
+      await client.del(key);
+    }
+  };
+
+  const validateTokens = (reAuthSpy: SinonSpy) => {
+    assert(reAuthSpy.callCount >= 1,
+      `reAuthenticate should have been called at least once, but was called ${reAuthSpy.callCount} times`);
+
+    const tokenDetails: TokenDetail[] = reAuthSpy.getCalls().map(call => {
+      const creds = call.args[0] as BasicAuth;
+      const tokenPayload = JSON.parse(
+        Buffer.from(creds.password.split('.')[1], 'base64').toString()
+      );
+
+      return {
+        token: creds.password,
+        exp: tokenPayload.exp,
+        iat: tokenPayload.iat,
+        lifetime: tokenPayload.exp - tokenPayload.iat,
+        uti: tokenPayload.uti
+      };
+    });
+
+    // Verify unique tokens
+    const uniqueTokens = new Set(tokenDetails.map(detail => detail.token));
+    assert.equal(
+      uniqueTokens.size,
+      reAuthSpy.callCount,
+      `Expected ${reAuthSpy.callCount} different tokens, but got ${uniqueTokens.size} unique tokens`
+    );
+
+    // Verify all tokens are not cached (i.e. have the same lifetime)
+    const uniqueLifetimes = new Set(tokenDetails.map(detail => detail.lifetime));
+    assert.equal(
+      uniqueLifetimes.size,
+      1,
+      `Expected all tokens to have the same lifetime, but found ${uniqueLifetimes.size} different lifetimes: ${[uniqueLifetimes].join(', ')} seconds`
+    );
+
+    // Verify that all tokens have different uti (unique token identifier)
+    const uniqueUti = new Set(tokenDetails.map(detail => detail.uti));
+    assert.equal(
+      uniqueUti.size,
+      reAuthSpy.callCount,
+      `Expected all tokens to have different uti, but found ${uniqueUti.size} different uti in: ${[uniqueUti].join(', ')}`
+    );
+  };
+
+  const runAuthenticationTest = async (setupCredentialsProvider: () => any) => {
+    const { client, reAuthSpy } = await setupTestClient(setupCredentialsProvider());
+
+    try {
+      await client.connect();
+      await runClientOperations(client);
+      validateTokens(reAuthSpy);
+    } finally {
+      await client.destroy();
+    }
+  };
+
+});
+
+function getCertificate(certBase64) {
+  try {
+    const decodedCert = Buffer.from(certBase64, 'base64');
+    const cert = new crypto.X509Certificate(decodedCert);
+    return cert;
+  } catch (error) {
+    console.error('Error parsing certificate:', error);
+    throw error;
+  }
+}
+
+function getCertificateThumbprint(certBase64) {
+  const cert = getCertificate(certBase64);
+  return cert.fingerprint.replace(/:/g, '');
+}
+
+function convertCertsForMSAL(certBase64, privateKeyBase64) {
+  const thumbprint = getCertificateThumbprint(certBase64);
+
+  const privateKeyPEM = `-----BEGIN PRIVATE KEY-----\n${privateKeyBase64}\n-----END PRIVATE KEY-----`;
+
+  return {
+    thumbprint: thumbprint,
+    privateKey: privateKeyPEM,
+    x5c: certBase64
+  }
+
+}
+
+
diff --git a/packages/entraid/lib/entra-id-credentials-provider-factory.ts b/packages/entraid/lib/entra-id-credentials-provider-factory.ts
new file mode 100644
index 00000000000..0f89be8039b
--- /dev/null
+++ b/packages/entraid/lib/entra-id-credentials-provider-factory.ts
@@ -0,0 +1,371 @@
+import { NetworkError } from '@azure/msal-common';
+import {
+  LogLevel,
+  ManagedIdentityApplication,
+  ManagedIdentityConfiguration,
+  AuthenticationResult,
+  PublicClientApplication,
+  ConfidentialClientApplication, AuthorizationUrlRequest, AuthorizationCodeRequest, CryptoProvider, Configuration, NodeAuthOptions, AccountInfo
+} from '@azure/msal-node';
+import { RetryPolicy, TokenManager, TokenManagerConfig, ReAuthenticationError } from '@redis/client/dist/lib/authx';
+import { EntraidCredentialsProvider } from './entraid-credentials-provider';
+import { MSALIdentityProvider } from './msal-identity-provider';
+
+/**
+ * This class is used to create credentials providers for different types of authentication flows.
+ */
+export class EntraIdCredentialsProviderFactory {
+
+  /**
+   * This method is used to create a ManagedIdentityProvider for both system-assigned and user-assigned managed identities.
+   *
+   * @param params
+   * @param userAssignedClientId For user-assigned managed identities, the developer needs to pass either the client ID,
+   * full resource identifier, or the object ID of the managed identity when creating ManagedIdentityApplication.
+   *
+   */
+  public static createManagedIdentityProvider(
+    params: CredentialParams, userAssignedClientId?: string
+  ): EntraidCredentialsProvider {
+    const config: ManagedIdentityConfiguration = {
+      // For user-assigned identity, include the client ID
+      ...(userAssignedClientId && {
+        managedIdentityIdParams: {
+          userAssignedClientId
+        }
+      }),
+      system: {
+        loggerOptions
+      }
+    };
+
+    const client = new ManagedIdentityApplication(config);
+
+    const idp = new MSALIdentityProvider(
+      () => client.acquireToken({
+        resource: params.scopes?.[0] ?? REDIS_SCOPE,
+        forceRefresh: true
+      }).then(x => x === null ? Promise.reject('Token is null') : x)
+    );
+
+    return new EntraidCredentialsProvider(
+      new TokenManager(idp, params.tokenManagerConfig),
+      idp,
+      { onReAuthenticationError: params.onReAuthenticationError, credentialsMapper: OID_CREDENTIALS_MAPPER }
+    );
+  }
+
+  /**
+   * This method is used to create a credentials provider for system-assigned managed identities.
+   * @param params
+   */
+  static createForSystemAssignedManagedIdentity(
+    params: CredentialParams
+  ): EntraidCredentialsProvider {
+    return this.createManagedIdentityProvider(params);
+  }
+
+  /**
+   * This method is used to create a credentials provider for user-assigned managed identities.
+   * It will include the client ID as the userAssignedClientId in the ManagedIdentityConfiguration.
+   * @param params
+   */
+  static createForUserAssignedManagedIdentity(
+    params: CredentialParams & { userAssignedClientId: string }
+  ): EntraidCredentialsProvider {
+    return this.createManagedIdentityProvider(params, params.userAssignedClientId);
+  }
+
+  static #createForClientCredentials(
+    authConfig: NodeAuthOptions,
+    params: CredentialParams
+  ): EntraidCredentialsProvider {
+    const config: Configuration = {
+      auth: {
+        ...authConfig,
+        authority: this.getAuthority(params.authorityConfig ?? { type: 'default' })
+      },
+      system: {
+        loggerOptions
+      }
+    };
+
+    const client = new ConfidentialClientApplication(config);
+
+    const idp = new MSALIdentityProvider(
+      () => client.acquireTokenByClientCredential({
+        skipCache: true,
+        scopes: params.scopes ?? [REDIS_SCOPE_DEFAULT]
+      }).then(x => x === null ? Promise.reject('Token is null') : x)
+    );
+
+    return new EntraidCredentialsProvider(new TokenManager(idp, params.tokenManagerConfig), idp,
+      {
+        onReAuthenticationError: params.onReAuthenticationError,
+        credentialsMapper: OID_CREDENTIALS_MAPPER
+      });
+  }
+
+  /**
+   * This method is used to create a credentials provider for service principals using certificate.
+   * @param params
+   */
+  static createForClientCredentialsWithCertificate(
+    params: ClientCredentialsWithCertificateParams
+  ): EntraidCredentialsProvider {
+    return this.#createForClientCredentials(
+      {
+        clientId: params.clientId,
+        clientCertificate: params.certificate
+      },
+      params
+    );
+  }
+
+  /**
+   * This method is used to create a credentials provider for service principals using client secret.
+   * @param params
+   */
+  static createForClientCredentials(
+    params: ClientSecretCredentialsParams
+  ): EntraidCredentialsProvider {
+    return this.#createForClientCredentials(
+      {
+        clientId: params.clientId,
+        clientSecret: params.clientSecret
+      },
+      params
+    );
+  }
+
+  /**
+   * This method is used to create a credentials provider for the Authorization Code Flow with PKCE.
+   * @param params
+   */
+  static createForAuthorizationCodeWithPKCE(
+    params: AuthCodePKCEParams
+  ): {
+    getPKCECodes: () => Promise<{
+      verifier: string;
+      challenge: string;
+      challengeMethod: string;
+    }>;
+    getAuthCodeUrl: (
+      pkceCodes: { challenge: string; challengeMethod: string }
+    ) => Promise<string>;
+    createCredentialsProvider: (
+      params: PKCEParams
+    ) => EntraidCredentialsProvider;
+  } {
+
+    const requiredScopes = ['user.read', 'offline_access'];
+    const scopes = [...new Set([...(params.scopes || []), ...requiredScopes])];
+
+    const authFlow = AuthCodeFlowHelper.create({
+      clientId: params.clientId,
+      redirectUri: params.redirectUri,
+      scopes: scopes,
+      authorityConfig: params.authorityConfig
+    });
+
+    return {
+      getPKCECodes: AuthCodeFlowHelper.generatePKCE,
+      getAuthCodeUrl: (pkceCodes) => authFlow.getAuthCodeUrl(pkceCodes),
+      createCredentialsProvider: (pkceParams) => {
+
+        // This is used to store the initial credentials account to be used
+        // for silent token acquisition after the initial token acquisition.
+        let initialCredentialsAccount: AccountInfo | null = null;
+
+        const idp = new MSALIdentityProvider(
+          async () => {
+            if (!initialCredentialsAccount) {
+              let authResult = await authFlow.acquireTokenByCode(pkceParams);
+              initialCredentialsAccount = authResult.account;
+              return authResult;
+            } else {
+              return authFlow.client.acquireTokenSilent({
+                forceRefresh: true,
+                account: initialCredentialsAccount,
+                scopes
+              });
+            }
+
+          }
+        );
+        const tm = new TokenManager(idp, params.tokenManagerConfig);
+        return new EntraidCredentialsProvider(tm, idp, { onReAuthenticationError: params.onReAuthenticationError });
+      }
+    };
+  }
+
+  static getAuthority(config: AuthorityConfig): string {
+    switch (config.type) {
+      case 'multi-tenant':
+        return `https://login.microsoftonline.com/${config.tenantId}`;
+      case 'custom':
+        return config.authorityUrl;
+      case 'default':
+        return 'https://login.microsoftonline.com/common';
+      default:
+        throw new Error('Invalid authority configuration');
+    }
+  }
+
+}
+
+const REDIS_SCOPE_DEFAULT = 'https://redis.azure.com/.default';
+const REDIS_SCOPE = 'https://redis.azure.com'
+
+export type AuthorityConfig =
+  | { type: 'multi-tenant'; tenantId: string }
+  | { type: 'custom'; authorityUrl: string }
+  | { type: 'default' };
+
+export type PKCEParams = {
+  code: string;
+  verifier: string;
+  clientInfo?: string;
+}
+
+export type CredentialParams = {
+  clientId: string;
+  scopes?: string[];
+  authorityConfig?: AuthorityConfig;
+
+  tokenManagerConfig: TokenManagerConfig
+  onReAuthenticationError?: (error: ReAuthenticationError) => void;
+}
+
+export type AuthCodePKCEParams = CredentialParams & {
+  redirectUri: string;
+};
+
+export type ClientSecretCredentialsParams = CredentialParams & {
+  clientSecret: string;
+};
+
+export type ClientCredentialsWithCertificateParams = CredentialParams & {
+  certificate: {
+    thumbprint: string;
+    privateKey: string;
+    x5c?: string;
+  };
+};
+
+const loggerOptions = {
+  loggerCallback(loglevel: LogLevel, message: string, containsPii: boolean) {
+    if (!containsPii) console.log(message);
+  },
+  piiLoggingEnabled: false,
+  logLevel: LogLevel.Error
+}
+
+/**
+ * The most important part of the RetryPolicy is the `isRetryable` function. This function is used to determine if a request should be retried based
+ * on the error returned from the identity provider. The default for is to retry on network errors only.
+ */
+export const DEFAULT_RETRY_POLICY: RetryPolicy = {
+  // currently only retry on network errors
+  isRetryable: (error: unknown) => error instanceof NetworkError,
+  maxAttempts: 10,
+  initialDelayMs: 100,
+  maxDelayMs: 100000,
+  backoffMultiplier: 2,
+  jitterPercentage: 0.1
+
+};
+
+export const DEFAULT_TOKEN_MANAGER_CONFIG: TokenManagerConfig = {
+  retry: DEFAULT_RETRY_POLICY,
+  expirationRefreshRatio: 0.7 // Refresh token when 70% of the token has expired
+}
+
+/**
+ * This class is used to help with the Authorization Code Flow with PKCE.
+ * It provides methods to generate PKCE codes, get the authorization URL, and create the credential provider.
+ */
+export class AuthCodeFlowHelper {
+  private constructor(
+    readonly client: PublicClientApplication,
+    readonly scopes: string[],
+    readonly redirectUri: string
+  ) {}
+
+  async getAuthCodeUrl(pkceCodes: {
+    challenge: string;
+    challengeMethod: string;
+  }): Promise<string> {
+    const authCodeUrlParameters: AuthorizationUrlRequest = {
+      scopes: this.scopes,
+      redirectUri: this.redirectUri,
+      codeChallenge: pkceCodes.challenge,
+      codeChallengeMethod: pkceCodes.challengeMethod
+    };
+
+    return this.client.getAuthCodeUrl(authCodeUrlParameters);
+  }
+
+  async acquireTokenByCode(params: PKCEParams): Promise<AuthenticationResult> {
+    const tokenRequest: AuthorizationCodeRequest = {
+      code: params.code,
+      scopes: this.scopes,
+      redirectUri: this.redirectUri,
+      codeVerifier: params.verifier,
+      clientInfo: params.clientInfo
+    };
+
+    return this.client.acquireTokenByCode(tokenRequest);
+  }
+
+  static async generatePKCE(): Promise<{
+    verifier: string;
+    challenge: string;
+    challengeMethod: string;
+  }> {
+    const cryptoProvider = new CryptoProvider();
+    const { verifier, challenge } = await cryptoProvider.generatePkceCodes();
+    return {
+      verifier,
+      challenge,
+      challengeMethod: 'S256'
+    };
+  }
+
+  static create(params: {
+    clientId: string;
+    redirectUri: string;
+    scopes?: string[];
+    authorityConfig?: AuthorityConfig;
+  }): AuthCodeFlowHelper {
+    const config = {
+      auth: {
+        clientId: params.clientId,
+        authority: EntraIdCredentialsProviderFactory.getAuthority(params.authorityConfig ?? { type: 'default' })
+      },
+      system: {
+        loggerOptions
+      }
+    };
+
+    return new AuthCodeFlowHelper(
+      new PublicClientApplication(config),
+      params.scopes ?? ['user.read'],
+      params.redirectUri
+    );
+  }
+}
+
+const OID_CREDENTIALS_MAPPER = (token: AuthenticationResult) => {
+
+  // Client credentials flow is app-only authentication (no user context),
+  // so only access token is provided without user-specific claims (uniqueId, idToken, ...)
+  // this means that we need to extract the oid from the access token manually
+  const accessToken = JSON.parse(Buffer.from(token.accessToken.split('.')[1], 'base64').toString());
+
+  return ({
+    username: accessToken.oid,
+    password: token.accessToken
+  })
+
+}
diff --git a/packages/entraid/lib/entraid-credentials-provider.spec.ts b/packages/entraid/lib/entraid-credentials-provider.spec.ts
new file mode 100644
index 00000000000..1bdf4e9b65f
--- /dev/null
+++ b/packages/entraid/lib/entraid-credentials-provider.spec.ts
@@ -0,0 +1,199 @@
+import { AuthenticationResult } from '@azure/msal-node';
+import { IdentityProvider, TokenManager, TokenResponse, BasicAuth } from '@redis/client/dist/lib/authx';
+import { EntraidCredentialsProvider } from './entraid-credentials-provider';
+import { setTimeout } from 'timers/promises';
+import { strict as assert } from 'node:assert';
+import { GLOBAL, testUtils } from './test-utils'
+
+
+describe('EntraID authentication in cluster mode', () => {
+
+  testUtils.testWithCluster('sendCommand', async cluster => {
+    assert.equal(
+      await cluster.sendCommand(undefined, true, ['PING']),
+      'PONG'
+    );
+  }, GLOBAL.CLUSTERS.PASSWORD_WITH_REPLICAS);
+})
+
+describe('EntraID CredentialsProvider Subscription Behavior', () => {
+
+  it('should properly handle token refresh sequence for multiple subscribers', async () => {
+    const networkDelay = 20;
+    const tokenTTL = 100;
+    const refreshRatio = 0.5; // Refresh at 50% of TTL
+
+    const idp = new SequenceEntraIDProvider(tokenTTL, networkDelay);
+    const tokenManager = new TokenManager<AuthenticationResult>(idp, {
+      expirationRefreshRatio: refreshRatio
+    });
+    const entraid = new EntraidCredentialsProvider(tokenManager, idp);
+
+    // Create two initial subscribers
+    const subscriber1 = new TestSubscriber('subscriber1');
+    const subscriber2 = new TestSubscriber('subscriber2');
+
+    assert.equal(entraid.hasActiveSubscriptions(), false, 'There should be no active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 0, 'There should be 0 subscriptions');
+
+    // Start the first two subscriptions almost simultaneously
+    const [sub1Initial, sub2Initial] = await Promise.all([
+      entraid.subscribe(subscriber1),
+      entraid.subscribe(subscriber2)]
+    );
+
+    assertCredentials(sub1Initial[0], 'initial-token', 'Subscriber 1 should receive initial token');
+    assertCredentials(sub2Initial[0], 'initial-token', 'Subscriber 2 should receive initial token');
+
+    assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 2, 'There should be 2 subscriptions');
+
+    // add a third subscriber after a very short delay
+    const subscriber3 = new TestSubscriber('subscriber3');
+    await setTimeout(1);
+    const sub3Initial = await entraid.subscribe(subscriber3)
+
+    assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 3, 'There should be 3 subscriptions');
+
+    // make sure the third subscriber gets the initial token as well
+    assertCredentials(sub3Initial[0], 'initial-token', 'Subscriber 3 should receive initial token');
+
+    // Wait for first refresh (50% of TTL + network delay + small buffer)
+    await setTimeout((tokenTTL * refreshRatio) + networkDelay + 15);
+
+    // All 3 subscribers should receive refresh-token-1
+    assertCredentials(subscriber1.credentials[0], 'refresh-token-1', 'Subscriber 1 should receive first refresh token');
+    assertCredentials(subscriber2.credentials[0], 'refresh-token-1', 'Subscriber 2 should receive first refresh token');
+    assertCredentials(subscriber3.credentials[0], 'refresh-token-1', 'Subscriber 3 should receive first refresh token');
+
+    // Add a late subscriber - should immediately get refresh-token-1
+    const subscriber4 = new TestSubscriber('subscriber4');
+    const sub4Initial = await entraid.subscribe(subscriber4);
+
+    assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 4, 'There should be 4 subscriptions');
+
+    assertCredentials(sub4Initial[0], 'refresh-token-1', 'Late subscriber should receive refresh-token-1');
+
+    // Wait for second refresh
+    await setTimeout((tokenTTL * refreshRatio) + networkDelay + 15);
+
+    assertCredentials(subscriber1.credentials[1], 'refresh-token-2', 'Subscriber 1 should receive second refresh token');
+    assertCredentials(subscriber2.credentials[1], 'refresh-token-2', 'Subscriber 2 should receive second refresh token');
+    assertCredentials(subscriber3.credentials[1], 'refresh-token-2', 'Subscriber 3 should receive second refresh token');
+
+    assertCredentials(subscriber4.credentials[0], 'refresh-token-2', 'Subscriber 4 should receive second refresh token');
+
+    // Verify refreshes happen after minimum expected time
+    const minimumRefreshInterval = tokenTTL * 0.4; // 40% of TTL as safety margin
+
+    verifyRefreshTiming(subscriber1, minimumRefreshInterval);
+    verifyRefreshTiming(subscriber2, minimumRefreshInterval);
+    verifyRefreshTiming(subscriber3, minimumRefreshInterval);
+    verifyRefreshTiming(subscriber4, minimumRefreshInterval);
+
+    // Cleanup
+
+    assert.equal(tokenManager.isRunning(), true);
+    sub1Initial[1].dispose();
+    sub2Initial[1].dispose();
+    sub3Initial[1].dispose();
+    assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 1, 'There should be 1 subscriptions');
+    sub4Initial[1].dispose();
+    assert.equal(entraid.hasActiveSubscriptions(), false, 'There should be no active subscriptions');
+    assert.equal(entraid.getSubscriptionsCount(), 0, 'There should be 0 subscriptions');
+    assert.equal(tokenManager.isRunning(), false)
+  });
+
+  const verifyRefreshTiming = (
+    subscriber: TestSubscriber,
+    expectedMinimumInterval: number,
+    message?: string
+  ) => {
+    const intervals = [];
+    for (let i = 1; i < subscriber.timestamps.length; i++) {
+      intervals.push(subscriber.timestamps[i] - subscriber.timestamps[i - 1]);
+    }
+
+    intervals.forEach((interval, index) => {
+      assert.ok(
+        interval > expectedMinimumInterval,
+        message || `Refresh ${index + 1} for ${subscriber.name} should happen after minimum interval of ${expectedMinimumInterval}ms`
+      );
+    });
+  };
+
+  class SequenceEntraIDProvider implements IdentityProvider<AuthenticationResult> {
+    private currentIndex = 0;
+
+    constructor(
+      private readonly tokenTTL: number = 100,
+      private tokenDeliveryDelayMs: number = 0,
+      private readonly tokenSequence: AuthenticationResult[] = [
+        {
+          accessToken: 'initial-token',
+          uniqueId: 'test-user'
+        } as AuthenticationResult,
+        {
+          accessToken: 'refresh-token-1',
+          uniqueId: 'test-user'
+        } as AuthenticationResult,
+        {
+          accessToken: 'refresh-token-2',
+          uniqueId: 'test-user'
+        } as AuthenticationResult
+      ]
+    ) {}
+
+    setTokenDeliveryDelay(delayMs: number): void {
+      this.tokenDeliveryDelayMs = delayMs;
+    }
+
+    async requestToken(): Promise<TokenResponse<AuthenticationResult>> {
+      if (this.tokenDeliveryDelayMs > 0) {
+        await setTimeout(this.tokenDeliveryDelayMs);
+      }
+
+      if (this.currentIndex >= this.tokenSequence.length) {
+        throw new Error('No more tokens in sequence');
+      }
+
+      return {
+        token: this.tokenSequence[this.currentIndex++],
+        ttlMs: this.tokenTTL
+      };
+    }
+  }
+
+  class TestSubscriber {
+    public readonly credentials: Array<BasicAuth> = [];
+    public readonly errors: Error[] = [];
+    public readonly timestamps: number[] = [];
+
+    constructor(public readonly name: string = 'unnamed') {}
+
+    onNext = (creds: BasicAuth) => {
+      this.credentials.push(creds);
+      this.timestamps.push(Date.now());
+    }
+
+    onError = (error: Error) => {
+      this.errors.push(error);
+    }
+  }
+
+  /**
+   * Assert that the actual credentials match the expected token
+   * @param actual
+   * @param expectedToken
+   * @param message
+   */
+  const assertCredentials = (actual: BasicAuth, expectedToken: string, message: string) => {
+    assert.deepEqual(actual, {
+      username: 'test-user',
+      password: expectedToken
+    }, message);
+  };
+});
\ No newline at end of file
diff --git a/packages/entraid/lib/entraid-credentials-provider.ts b/packages/entraid/lib/entraid-credentials-provider.ts
new file mode 100644
index 00000000000..115d6dbff3a
--- /dev/null
+++ b/packages/entraid/lib/entraid-credentials-provider.ts
@@ -0,0 +1,140 @@
+import { AuthenticationResult } from '@azure/msal-common/node';
+import {
+  BasicAuth, StreamingCredentialsProvider, IdentityProvider, TokenManager,
+  ReAuthenticationError, StreamingCredentialsListener, IDPError, Token, Disposable
+} from '@redis/client/dist/lib/authx';
+
+/**
+ * A streaming credentials provider that uses the Entraid identity provider to provide credentials.
+ * Please use one of the factory functions in `entraid-credetfactories.ts` to create an instance of this class for the different
+ * type of authentication flows.
+ */
+export class EntraidCredentialsProvider implements StreamingCredentialsProvider {
+  readonly type = 'streaming-credentials-provider';
+
+  readonly #listeners: Set<StreamingCredentialsListener<BasicAuth>> = new Set();
+
+  #tokenManagerDisposable: Disposable | null = null;
+  #isStarting: boolean = false;
+
+  #pendingSubscribers: Array<{
+    resolve: (value: [BasicAuth, Disposable]) => void;
+    reject: (error: Error) => void;
+    pendingListener: StreamingCredentialsListener<BasicAuth>;
+  }> = [];
+
+  constructor(
+    public readonly tokenManager: TokenManager<AuthenticationResult>,
+    public readonly idp: IdentityProvider<AuthenticationResult>,
+    private readonly options: {
+      onReAuthenticationError?: (error: ReAuthenticationError) => void;
+      credentialsMapper?: (token: AuthenticationResult) => BasicAuth;
+      onRetryableError?: (error: string) => void;
+    } = {}
+  ) {
+    this.onReAuthenticationError = options.onReAuthenticationError ?? DEFAULT_ERROR_HANDLER;
+    this.#credentialsMapper = options.credentialsMapper ?? DEFAULT_CREDENTIALS_MAPPER;
+  }
+
+  async subscribe(
+    listener: StreamingCredentialsListener<BasicAuth>
+  ): Promise<[BasicAuth, Disposable]> {
+
+    const currentToken = this.tokenManager.getCurrentToken();
+
+    if (currentToken) {
+      return [this.#credentialsMapper(currentToken.value), this.#createDisposable(listener)];
+    }
+
+    if (this.#isStarting) {
+      return new Promise((resolve, reject) => {
+        this.#pendingSubscribers.push({ resolve, reject, pendingListener: listener });
+      });
+    }
+
+    this.#isStarting = true;
+    try {
+      const initialToken = await this.#startTokenManagerAndObtainInitialToken();
+
+      this.#pendingSubscribers.forEach(({ resolve, pendingListener }) => {
+        resolve([this.#credentialsMapper(initialToken.value), this.#createDisposable(pendingListener)]);
+      });
+      this.#pendingSubscribers = [];
+
+      return [this.#credentialsMapper(initialToken.value), this.#createDisposable(listener)];
+    } finally {
+      this.#isStarting = false;
+    }
+  }
+
+  onReAuthenticationError: (error: ReAuthenticationError) => void;
+
+  #credentialsMapper: (token: AuthenticationResult) => BasicAuth;
+
+  #createTokenManagerListener(subscribers: Set<StreamingCredentialsListener<BasicAuth>>) {
+    return {
+      onError: (error: IDPError): void => {
+        if (!error.isRetryable) {
+          subscribers.forEach(listener => listener.onError(error));
+        } else {
+          this.options.onRetryableError?.(error.message);
+        }
+      },
+      onNext: (token: { value: AuthenticationResult }): void => {
+        const credentials = this.#credentialsMapper(token.value);
+        subscribers.forEach(listener => listener.onNext(credentials));
+      }
+    };
+  }
+
+  #createDisposable(listener: StreamingCredentialsListener<BasicAuth>): Disposable {
+    this.#listeners.add(listener);
+
+    return {
+      dispose: () => {
+        this.#listeners.delete(listener);
+        if (this.#listeners.size === 0 && this.#tokenManagerDisposable) {
+          this.#tokenManagerDisposable.dispose();
+          this.#tokenManagerDisposable = null;
+        }
+      }
+    };
+  }
+
+  async #startTokenManagerAndObtainInitialToken(): Promise<Token<AuthenticationResult>> {
+    const initialResponse = await this.idp.requestToken();
+    const token = this.tokenManager.wrapAndSetCurrentToken(initialResponse.token, initialResponse.ttlMs);
+
+    this.#tokenManagerDisposable = this.tokenManager.start(
+      this.#createTokenManagerListener(this.#listeners),
+      this.tokenManager.calculateRefreshTime(token)
+    );
+    return token;
+  }
+
+  public hasActiveSubscriptions(): boolean {
+    return this.#tokenManagerDisposable !== null && this.#listeners.size > 0;
+  }
+
+  public getSubscriptionsCount(): number {
+    return this.#listeners.size;
+  }
+
+  public getTokenManager() {
+    return this.tokenManager;
+  }
+
+  public getCurrentCredentials(): BasicAuth | null {
+    const currentToken = this.tokenManager.getCurrentToken();
+    return currentToken ? this.#credentialsMapper(currentToken.value) : null;
+  }
+
+}
+
+const DEFAULT_CREDENTIALS_MAPPER = (token: AuthenticationResult): BasicAuth => ({
+  username: token.uniqueId,
+  password: token.accessToken
+});
+
+const DEFAULT_ERROR_HANDLER = (error: ReAuthenticationError) =>
+  console.error('ReAuthenticationError', error);
\ No newline at end of file
diff --git a/packages/entraid/lib/index.ts b/packages/entraid/lib/index.ts
new file mode 100644
index 00000000000..4873c9935c5
--- /dev/null
+++ b/packages/entraid/lib/index.ts
@@ -0,0 +1,3 @@
+export * from './entra-id-credentials-provider-factory';
+export * from './entraid-credentials-provider';
+export * from './msal-identity-provider';
\ No newline at end of file
diff --git a/packages/entraid/lib/msal-identity-provider.ts b/packages/entraid/lib/msal-identity-provider.ts
new file mode 100644
index 00000000000..59b38d18ec6
--- /dev/null
+++ b/packages/entraid/lib/msal-identity-provider.ts
@@ -0,0 +1,31 @@
+import {
+  AuthenticationResult
+} from '@azure/msal-node';
+import { IdentityProvider, TokenResponse } from '@redis/client/dist/lib/authx';
+
+export class MSALIdentityProvider implements IdentityProvider<AuthenticationResult> {
+  private readonly getToken: () => Promise<AuthenticationResult>;
+
+  constructor(getToken: () => Promise<AuthenticationResult>) {
+    this.getToken = getToken;
+  }
+
+  async requestToken(): Promise<TokenResponse<AuthenticationResult>> {
+    try {
+      const result = await this.getToken();
+
+      if (!result?.accessToken || !result?.expiresOn) {
+        throw new Error('Invalid token response');
+      }
+      return {
+        token: result,
+        ttlMs: result.expiresOn.getTime() - Date.now()
+      };
+    } catch (error) {
+      throw error;
+    }
+  }
+
+}
+
+
diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts
new file mode 100644
index 00000000000..eecec2d4d6d
--- /dev/null
+++ b/packages/entraid/lib/test-utils.ts
@@ -0,0 +1,46 @@
+import { AuthenticationResult } from '@azure/msal-node';
+import { IdentityProvider, StreamingCredentialsProvider, TokenManager, TokenResponse } from '@redis/client/dist/lib/authx';
+import TestUtils from '@redis/test-utils';
+import { EntraidCredentialsProvider } from './entraid-credentials-provider';
+
+export const testUtils = new TestUtils({
+  dockerImageName: 'redis/redis-stack',
+  dockerImageVersionArgument: 'redis-version',
+  defaultDockerVersion: '7.4.0-v1'
+});
+
+const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ?
+  ['--enable-debug-command', 'yes'] :
+  [];
+
+const idp: IdentityProvider<AuthenticationResult> = {
+  requestToken(): Promise<TokenResponse<AuthenticationResult>> {
+    // @ts-ignore
+    return Promise.resolve({
+      ttlMs: 100000,
+      token: {
+        accessToken: 'password'
+      }
+    })
+  }
+}
+
+const tokenManager = new TokenManager<AuthenticationResult>(idp, { expirationRefreshRatio: 0.8 });
+const entraIdCredentialsProvider: StreamingCredentialsProvider = new EntraidCredentialsProvider(tokenManager, idp)
+
+const PASSWORD_WITH_REPLICAS = {
+  serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
+  numberOfMasters: 2,
+  numberOfReplicas: 1,
+  clusterConfiguration: {
+    defaults: {
+      credentialsProvider: entraIdCredentialsProvider
+    }
+  }
+}
+
+export const GLOBAL = {
+  CLUSTERS: {
+    PASSWORD_WITH_REPLICAS
+  }
+}
diff --git a/packages/entraid/package.json b/packages/entraid/package.json
new file mode 100644
index 00000000000..571d78c0417
--- /dev/null
+++ b/packages/entraid/package.json
@@ -0,0 +1,47 @@
+{
+  "name": "@redis/entraid",
+  "version": "5.0.0-next.5",
+  "license": "MIT",
+  "main": "./dist/index.js",
+  "types": "./dist/index.d.ts",
+  "files": [
+    "dist/",
+    "!dist/tsconfig.tsbuildinfo"
+  ],
+  "scripts": {
+    "clean": "rimraf dist",
+    "build": "npm run clean && tsc",
+    "start:auth-pkce": "tsx --tsconfig tsconfig.samples.json ./samples/auth-code-pkce/index.ts",
+    "test-integration": "mocha -r tsx --tsconfig tsconfig.integration-tests.json './integration-tests/**/*.spec.ts'",
+    "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'"
+  },
+  "dependencies": {
+    "@azure/msal-node": "^2.16.1"
+  },
+  "peerDependencies": {
+    "@redis/client": "^5.0.0-next.5"
+  },
+  "devDependencies": {
+    "@types/express": "^4.17.21",
+    "@types/express-session": "^1.18.0",
+    "@types/node": "^22.9.0",
+    "dotenv": "^16.3.1",
+    "express": "^4.21.1",
+    "express-session": "^1.18.1",
+    "@redis/test-utils": "*"
+  },
+  "engines": {
+    "node": ">= 18"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/redis/node-redis.git"
+  },
+  "bugs": {
+    "url": "https://github.com/redis/node-redis/issues"
+  },
+  "homepage": "https://github.com/redis/node-redis/tree/master/packages/entraid",
+  "keywords": [
+    "redis"
+  ]
+}
diff --git a/packages/entraid/samples/auth-code-pkce/index.ts b/packages/entraid/samples/auth-code-pkce/index.ts
new file mode 100644
index 00000000000..25429269c44
--- /dev/null
+++ b/packages/entraid/samples/auth-code-pkce/index.ts
@@ -0,0 +1,153 @@
+import express, { Request, Response } from 'express';
+import session from 'express-session';
+import dotenv from 'dotenv';
+import { DEFAULT_TOKEN_MANAGER_CONFIG, EntraIdCredentialsProviderFactory } from '../../lib/entra-id-credentials-provider-factory';
+
+dotenv.config();
+
+if (!process.env.SESSION_SECRET) {
+  throw new Error('SESSION_SECRET environment variable must be set');
+}
+
+interface PKCESession extends session.Session {
+  pkceCodes?: {
+    verifier: string;
+    challenge: string;
+    challengeMethod: string;
+  };
+}
+
+interface AuthRequest extends Request {
+  session: PKCESession;
+}
+
+const app = express();
+
+const sessionConfig = {
+  secret: process.env.SESSION_SECRET,
+  resave: false,
+  saveUninitialized: false,
+  cookie: {
+    secure: process.env.NODE_ENV === 'production', // Only use secure in production
+    httpOnly: true,
+    sameSite: 'lax',
+    maxAge: 3600000 // 1 hour
+  }
+} as const;
+
+app.use(session(sessionConfig));
+
+if (!process.env.MSAL_CLIENT_ID || !process.env.MSAL_TENANT_ID) {
+  throw new Error('MSAL_CLIENT_ID and MSAL_TENANT_ID environment variables must be set');
+}
+
+// Initialize MSAL provider with authorization code PKCE flow
+const {
+  getPKCECodes,
+  createCredentialsProvider,
+  getAuthCodeUrl
+} = EntraIdCredentialsProviderFactory.createForAuthorizationCodeWithPKCE({
+  clientId: process.env.MSAL_CLIENT_ID,
+  redirectUri: process.env.REDIRECT_URI || 'http://localhost:3000/redirect',
+  authorityConfig: { type: 'multi-tenant', tenantId: process.env.MSAL_TENANT_ID },
+  tokenManagerConfig: DEFAULT_TOKEN_MANAGER_CONFIG
+});
+
+app.get('/login', async (req: AuthRequest, res: Response) => {
+  try {
+    // Generate PKCE Codes before starting the authorization flow
+    const pkceCodes = await getPKCECodes();
+
+    // Store PKCE codes in session
+    req.session.pkceCodes = pkceCodes
+
+    await new Promise<void>((resolve, reject) => {
+      req.session.save((err) => {
+        if (err) reject(err);
+        else resolve();
+      });
+    });
+
+    const authUrl = await getAuthCodeUrl({
+      challenge: pkceCodes.challenge,
+      challengeMethod: pkceCodes.challengeMethod
+    });
+
+    res.redirect(authUrl);
+  } catch (error) {
+    console.error('Login flow failed:', error);
+    res.status(500).send('Authentication failed');
+  }
+});
+
+app.get('/redirect', async (req: AuthRequest, res: Response) => {
+  try {
+
+    // The authorization code is in req.query.code
+    const { code, client_info } = req.query;
+    const { pkceCodes } = req.session;
+
+    if (!pkceCodes) {
+      console.error('Session state:', {
+        hasSession: !!req.session,
+        sessionID: req.sessionID,
+        pkceCodes: req.session.pkceCodes
+      });
+      return res.status(400).send('PKCE codes not found in session');
+    }
+
+    // Check both possible error scenarios
+    if (req.query.error) {
+      console.error('OAuth error:', req.query.error, req.query.error_description);
+      return res.status(400).send(`OAuth error: ${req.query.error_description || req.query.error}`);
+    }
+
+    if (!code) {
+      console.error('Missing authorization code. Query parameters received:', req.query);
+      return res.status(400).send('Authorization code not found in request. Query params: ' + JSON.stringify(req.query));
+    }
+
+    // Configure with the received code
+    const entraidCredentialsProvider = createCredentialsProvider(
+      {
+        code: code as string,
+        verifier: pkceCodes.verifier,
+        clientInfo: client_info as string | undefined
+      },
+    );
+
+    const initialCredentials = entraidCredentialsProvider.subscribe({
+      onNext: (token) => {
+        console.log('Token acquired:', token);
+      },
+      onError: (error) => {
+        console.error('Token acquisition failed:', error);
+      }
+    });
+
+    const [credentials] = await initialCredentials;
+
+    console.log('Credentials acquired:', credentials)
+
+    // Clear sensitive data
+    delete req.session.pkceCodes;
+
+    await new Promise<void>((resolve, reject) => {
+      req.session.save((err) => {
+        if (err) reject(err);
+        else resolve();
+      });
+    });
+
+    res.json({ message: 'Authentication successful' });
+  } catch (error) {
+    console.error('Token acquisition failed:', error);
+    res.status(500).send('Failed to acquire token');
+  }
+});
+
+const PORT = process.env.PORT || 3000;
+app.listen(PORT, () => {
+  console.log(`Server running on port ${PORT}`);
+  console.log(`Login URL: http://localhost:${PORT}/login`);
+});
\ No newline at end of file
diff --git a/packages/entraid/tsconfig.integration-tests.json b/packages/entraid/tsconfig.integration-tests.json
new file mode 100644
index 00000000000..5d15f4f2753
--- /dev/null
+++ b/packages/entraid/tsconfig.integration-tests.json
@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.json",
+  "include": [
+    "./integration-tests/**/*.ts",
+    "./lib/**/*.ts"
+  ],
+  "compilerOptions": {
+    "noEmit": true
+  },
+}
\ No newline at end of file
diff --git a/packages/entraid/tsconfig.json b/packages/entraid/tsconfig.json
new file mode 100644
index 00000000000..414dc1fe755
--- /dev/null
+++ b/packages/entraid/tsconfig.json
@@ -0,0 +1,20 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "outDir": "./dist"
+  },
+  "include": [
+    "./lib/**/*.ts"
+  ],
+  "exclude": [
+    "./lib/**/*.spec.ts",
+    "./lib/test-util.ts",
+  ],
+  "typedocOptions": {
+    "entryPoints": [
+      "./lib"
+    ],
+    "entryPointStrategy": "expand",
+    "out": "../../documentation/entraid"
+  }
+}
diff --git a/packages/entraid/tsconfig.samples.json b/packages/entraid/tsconfig.samples.json
new file mode 100644
index 00000000000..0eb936369ff
--- /dev/null
+++ b/packages/entraid/tsconfig.samples.json
@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.json",
+  "include": [
+    "./samples/**/*.ts",
+    "./lib/**/*.ts"
+  ],
+  "compilerOptions": {
+    "noEmit": true
+  }
+}
\ No newline at end of file
diff --git a/packages/test-utils/lib/cae-client-testing.ts b/packages/test-utils/lib/cae-client-testing.ts
new file mode 100644
index 00000000000..92b846dd37e
--- /dev/null
+++ b/packages/test-utils/lib/cae-client-testing.ts
@@ -0,0 +1,30 @@
+import { readFile } from 'node:fs/promises';
+
+interface RawRedisEndpoint {
+  username?: string;
+  password?: string;
+  tls: boolean;
+  endpoints: string[];
+}
+
+export type RedisEndpointsConfig = Record<string, RawRedisEndpoint>;
+
+export function loadFromJson(jsonString: string): RedisEndpointsConfig {
+  try {
+    return JSON.parse(jsonString) as RedisEndpointsConfig;
+  } catch (error) {
+    throw new Error(`Invalid JSON configuration: ${error}`);
+  }
+}
+
+export async function loadFromFile(path: string): Promise<RedisEndpointsConfig> {
+  try {
+    const configFile = await readFile(path, 'utf-8');
+    return loadFromJson(configFile);
+  } catch (error) {
+    if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
+      throw new Error(`Config file not found at path: ${path}`);
+    }
+    throw error;
+  }
+}
\ No newline at end of file
diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts
index a1cb63eb7bf..bfb66603750 100644
--- a/packages/test-utils/lib/dockers.ts
+++ b/packages/test-utils/lib/dockers.ts
@@ -1,3 +1,4 @@
+import { RedisClusterClientOptions } from '@redis/client/dist/lib/cluster';
 import { createConnection } from 'node:net';
 import { once } from 'node:events';
 import { createClient } from '@redis/client/index';
@@ -102,7 +103,8 @@ async function spawnRedisClusterNodeDockers(
   dockersConfig: RedisClusterDockersConfig,
   serverArguments: Array<string>,
   fromSlot: number,
-  toSlot: number
+  toSlot: number,
+  clientConfig?: Partial<RedisClusterClientOptions>
 ) {
   const range: Array<number> = [];
   for (let i = fromSlot; i < toSlot; i++) {
@@ -111,7 +113,8 @@ async function spawnRedisClusterNodeDockers(
 
   const master = await spawnRedisClusterNodeDocker(
     dockersConfig,
-    serverArguments
+    serverArguments,
+    clientConfig
   );
 
   await master.client.clusterAddSlots(range);
@@ -127,7 +130,13 @@ async function spawnRedisClusterNodeDockers(
         'yes',
         '--cluster-node-timeout',
         '5000'
-      ]).then(async replica => {
+      ], clientConfig).then(async replica => {
+
+        const requirePassIndex = serverArguments.findIndex((x)=>x==='--requirepass');
+        if(requirePassIndex!==-1) {
+          const password = serverArguments[requirePassIndex+1];
+          await replica.client.configSet({'masterauth': password})
+        }
         await replica.client.clusterMeet('127.0.0.1', master.docker.port);
 
         while ((await replica.client.clusterSlots()).length === 0) {
@@ -151,7 +160,8 @@ async function spawnRedisClusterNodeDockers(
 
 async function spawnRedisClusterNodeDocker(
   dockersConfig: RedisClusterDockersConfig,
-  serverArguments: Array<string>
+  serverArguments: Array<string>,
+  clientConfig?: Partial<RedisClusterClientOptions>
 ) {
   const docker = await spawnRedisServerDocker(dockersConfig, [
       ...serverArguments,
@@ -163,7 +173,8 @@ async function spawnRedisClusterNodeDocker(
     client = createClient({
       socket: {
         port: docker.port
-      }
+      },
+      ...clientConfig
     });
 
   await client.connect();
@@ -178,7 +189,8 @@ const SLOTS = 16384;
 
 async function spawnRedisClusterDockers(
   dockersConfig: RedisClusterDockersConfig,
-  serverArguments: Array<string>
+  serverArguments: Array<string>,
+  clientConfig?: Partial<RedisClusterClientOptions>
 ): Promise<Array<RedisServerDocker>> {
   const numberOfMasters = dockersConfig.numberOfMasters ?? 2,
     slotsPerNode = Math.floor(SLOTS / numberOfMasters),
@@ -191,7 +203,8 @@ async function spawnRedisClusterDockers(
         dockersConfig,
         serverArguments,
         fromSlot,
-        toSlot
+        toSlot,
+        clientConfig
       )
     );
   }
@@ -234,13 +247,18 @@ function totalNodes(slots: any) {
 
 const RUNNING_CLUSTERS = new Map<Array<string>, ReturnType<typeof spawnRedisClusterDockers>>();
 
-export function spawnRedisCluster(dockersConfig: RedisClusterDockersConfig, serverArguments: Array<string>): Promise<Array<RedisServerDocker>> {
+export function spawnRedisCluster(
+  dockersConfig: RedisClusterDockersConfig,
+  serverArguments: Array<string>,
+  clientConfig?: Partial<RedisClusterClientOptions>): Promise<Array<RedisServerDocker>> {
+
   const runningCluster = RUNNING_CLUSTERS.get(serverArguments);
   if (runningCluster) {
     return runningCluster;
   }
 
-  const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments);
+  const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments,clientConfig);
+
   RUNNING_CLUSTERS.set(serverArguments, dockersPromise);
   return dockersPromise;
 }
diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts
index 87ba34db7ef..9dee350e31e 100644
--- a/packages/test-utils/lib/index.ts
+++ b/packages/test-utils/lib/index.ts
@@ -290,7 +290,8 @@ export default class TestUtils {
           ...dockerImage,
           numberOfMasters: options.numberOfMasters,
           numberOfReplicas: options.numberOfReplicas
-        }, options.serverArguments);
+        }, options.serverArguments,
+          options.clusterConfiguration?.defaults);
         return dockersPromise;
       });
     }
diff --git a/tsconfig.json b/tsconfig.json
index a578fefa54f..8f43ab41d22 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,20 +1,32 @@
 {
   "files": [],
-  "references": [{
-    "path": "./packages/client"
-  }, {
-    "path": "./packages/test-utils"
-  }, {
-    "path": "./packages/bloom"
-  }, {
-    "path": "./packages/graph"
-  }, {
-    "path": "./packages/json"
-  }, {
-    "path": "./packages/search"
-  }, {
-    "path": "./packages/time-series"
-  }, {
-    "path": "./packages/redis"
-  }]
+  "references": [
+    {
+      "path": "./packages/client"
+    },
+    {
+      "path": "./packages/test-utils"
+    },
+    {
+      "path": "./packages/bloom"
+    },
+    {
+      "path": "./packages/graph"
+    },
+    {
+      "path": "./packages/json"
+    },
+    {
+      "path": "./packages/search"
+    },
+    {
+      "path": "./packages/time-series"
+    },
+    {
+      "path": "./packages/entraid"
+    },
+    {
+      "path": "./packages/redis"
+    }
+  ]
 }