You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-06 02:15:48 +03:00
feat(entraid): add support for azure identity (#2901)
This PR adds support for using Azure Identity's credential classes with Redis Enterprise Entra ID authentication. The main changes include: - Add a new factory method createForDefaultAzureCredential to enable using Azure Identity credentials - Add @azure/identity as a dependency to support the new authentication flow - Add support for DefaultAzureCredential, EnvironmentCredential, and any other TokenCredential implementation - Create a new AzureIdentityProvider to support DefaultAzureCredential - Update documentation and README with usage examples for DefaultAzureCredential - Add integration tests for the new authentication methods - Include a sample application demonstrating interactive browser authentication - Export constants for Redis scopes / credential mappers to simplify authentication configuration
This commit is contained in:
291
package-lock.json
generated
291
package-lock.json
generated
@@ -33,6 +33,214 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/abort-controller": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
|
||||
"integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-auth": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
|
||||
"integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-util": "^1.11.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-client": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz",
|
||||
"integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-auth": "^1.4.0",
|
||||
"@azure/core-rest-pipeline": "^1.9.1",
|
||||
"@azure/core-tracing": "^1.0.0",
|
||||
"@azure/core-util": "^1.6.1",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-rest-pipeline": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.0.tgz",
|
||||
"integrity": "sha512-bM3308LRyg5g7r3Twprtqww0R/r7+GyVxj4BafcmVPo4WQoGt5JXuaqxHEFjw2o3rvFZcUPiqJMg6WuvEEeVUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-auth": "^1.8.0",
|
||||
"@azure/core-tracing": "^1.0.1",
|
||||
"@azure/core-util": "^1.11.0",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-tracing": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz",
|
||||
"integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-util": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz",
|
||||
"integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.7.0.tgz",
|
||||
"integrity": "sha512-6z/S2KorkbKaZ0DgZFVRdu7RCuATmMSTjKpuhj7YpjxkJ0vnJ7kTM3cpNgzFgk9OPYfZ31wrBEtC/iwAS4jQDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-auth": "^1.9.0",
|
||||
"@azure/core-client": "^1.9.2",
|
||||
"@azure/core-rest-pipeline": "^1.17.0",
|
||||
"@azure/core-tracing": "^1.0.0",
|
||||
"@azure/core-util": "^1.11.0",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"@azure/msal-browser": "^4.2.0",
|
||||
"@azure/msal-node": "^3.2.1",
|
||||
"events": "^3.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"open": "^10.1.0",
|
||||
"stoppable": "^1.1.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/@azure/msal-common": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz",
|
||||
"integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/@azure/msal-node": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.2.3.tgz",
|
||||
"integrity": "sha512-0eaPqBIWEAizeYiXdeHb09Iq0tvHJ17ztvNEaLdr/KcJJhJxbpkkEQf09DB+vKlFE0tzYi7j4rYLTXtES/InEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.2.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/jwa": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
|
||||
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/open": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz",
|
||||
"integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"default-browser": "^5.2.1",
|
||||
"define-lazy-prop": "^3.0.0",
|
||||
"is-inside-container": "^1.0.0",
|
||||
"is-wsl": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/logger": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz",
|
||||
"integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-browser": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.4.0.tgz",
|
||||
"integrity": "sha512-rU6juYXk67CKQmpgi6fDgZoPQ9InZ1760z1BSAH7RbeIc4lHZM/Tu+H0CyRk7cnrfvTkexyYE4pjYhMghpzheA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-browser/node_modules/@azure/msal-common": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz",
|
||||
"integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-common": {
|
||||
"version": "14.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz",
|
||||
@@ -847,10 +1055,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/authx": {
|
||||
"resolved": "packages/authx",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
"resolved": "packages/bloom",
|
||||
"link": true
|
||||
@@ -1128,7 +1332,6 @@
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
@@ -1676,7 +1879,6 @@
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"run-applescript": "^7.0.0"
|
||||
@@ -2161,7 +2363,6 @@
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
@@ -2177,7 +2378,6 @@
|
||||
},
|
||||
"node_modules/debug/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
@@ -2223,7 +2423,6 @@
|
||||
},
|
||||
"node_modules/default-browser": {
|
||||
"version": "5.2.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bundle-name": "^4.1.0",
|
||||
@@ -2238,7 +2437,6 @@
|
||||
},
|
||||
"node_modules/default-browser-id": {
|
||||
"version": "5.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -2300,7 +2498,6 @@
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
"version": "3.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -2736,6 +2933,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "8.0.1",
|
||||
"dev": true,
|
||||
@@ -3689,7 +3895,6 @@
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
@@ -3713,7 +3918,6 @@
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.0.2",
|
||||
@@ -4087,7 +4291,6 @@
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "3.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
@@ -4142,7 +4345,6 @@
|
||||
},
|
||||
"node_modules/is-inside-container": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^3.0.0"
|
||||
@@ -4391,7 +4593,6 @@
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "3.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-inside-container": "^1.0.0"
|
||||
@@ -6654,7 +6855,6 @@
|
||||
},
|
||||
"node_modules/run-applescript": {
|
||||
"version": "7.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -7146,6 +7346,16 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/stoppable": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
|
||||
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"dev": true,
|
||||
@@ -7368,7 +7578,6 @@
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
@@ -8129,6 +8338,7 @@
|
||||
"packages/authx": {
|
||||
"name": "@redis/authx",
|
||||
"version": "5.0.0-next.5",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^2.16.1"
|
||||
@@ -8143,7 +8353,7 @@
|
||||
},
|
||||
"packages/bloom": {
|
||||
"name": "@redis/bloom",
|
||||
"version": "5.0.0-next.5",
|
||||
"version": "5.0.0-next.6",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@redis/test-utils": "*"
|
||||
@@ -8152,12 +8362,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.0.0-next.5"
|
||||
"@redis/client": "^5.0.0-next.6"
|
||||
}
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@redis/client",
|
||||
"version": "5.0.0-next.5",
|
||||
"version": "5.0.0-next.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2"
|
||||
@@ -8169,16 +8379,14 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/authx": "^5.0.0-next.5"
|
||||
}
|
||||
},
|
||||
"packages/entraid": {
|
||||
"name": "@redis/entraid",
|
||||
"version": "5.0.0-next.5",
|
||||
"version": "5.0.0-next.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/identity": "4.7.0",
|
||||
"@azure/msal-node": "^2.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -8194,8 +8402,7 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/authx": "^5.0.0-next.5",
|
||||
"@redis/client": "^5.0.0-next.5"
|
||||
"@redis/client": "^5.0.0-next.6"
|
||||
}
|
||||
},
|
||||
"packages/entraid/node_modules/@types/node": {
|
||||
@@ -8217,7 +8424,7 @@
|
||||
},
|
||||
"packages/graph": {
|
||||
"name": "@redis/graph",
|
||||
"version": "5.0.0-next.5",
|
||||
"version": "5.0.0-next.6",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@redis/test-utils": "*"
|
||||
@@ -8226,12 +8433,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.0.0-next.5"
|
||||
"@redis/client": "^5.0.0-next.6"
|
||||
}
|
||||
},
|
||||
"packages/json": {
|
||||
"name": "@redis/json",
|
||||
"version": "5.0.0-next.5",
|
||||
"version": "5.0.0-next.6",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@redis/test-utils": "*"
|
||||
@@ -8240,19 +8447,19 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.0.0-next.5"
|
||||
"@redis/client": "^5.0.0-next.6"
|
||||
}
|
||||
},
|
||||
"packages/redis": {
|
||||
"version": "5.0.0-next.5",
|
||||
"version": "5.0.0-next.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@redis/bloom": "5.0.0-next.5",
|
||||
"@redis/client": "5.0.0-next.5",
|
||||
"@redis/graph": "5.0.0-next.5",
|
||||
"@redis/json": "5.0.0-next.5",
|
||||
"@redis/search": "5.0.0-next.5",
|
||||
"@redis/time-series": "5.0.0-next.5"
|
||||
"@redis/bloom": "5.0.0-next.6",
|
||||
"@redis/client": "5.0.0-next.6",
|
||||
"@redis/graph": "5.0.0-next.6",
|
||||
"@redis/json": "5.0.0-next.6",
|
||||
"@redis/search": "5.0.0-next.6",
|
||||
"@redis/time-series": "5.0.0-next.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
@@ -8260,7 +8467,7 @@
|
||||
},
|
||||
"packages/search": {
|
||||
"name": "@redis/search",
|
||||
"version": "5.0.0-next.5",
|
||||
"version": "5.0.0-next.6",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@redis/test-utils": "*"
|
||||
@@ -8269,7 +8476,7 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.0.0-next.5"
|
||||
"@redis/client": "^5.0.0-next.6"
|
||||
}
|
||||
},
|
||||
"packages/test-utils": {
|
||||
@@ -8338,7 +8545,7 @@
|
||||
},
|
||||
"packages/time-series": {
|
||||
"name": "@redis/time-series",
|
||||
"version": "5.0.0-next.5",
|
||||
"version": "5.0.0-next.6",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@redis/test-utils": "*"
|
||||
@@ -8347,7 +8554,7 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.0.0-next.5"
|
||||
"@redis/client": "^5.0.0-next.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ Secure token-based authentication for Redis clients using Microsoft Entra ID (fo
|
||||
- Managed identities (system-assigned and user-assigned)
|
||||
- Service principals (with or without certificates)
|
||||
- Authorization Code with PKCE flow
|
||||
- DefaultAzureCredential from @azure/identity
|
||||
- Built-in retry mechanisms for transient failures
|
||||
|
||||
## Installation
|
||||
@@ -30,6 +31,7 @@ The first step to using @redis/entraid is choosing the right credentials provide
|
||||
- `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
|
||||
- `createForDefaultAzureCredential`: Use when you want to leverage Azure Identity's DefaultAzureCredential
|
||||
|
||||
## Usage Examples
|
||||
|
||||
@@ -82,6 +84,54 @@ const provider = EntraIdCredentialsProviderFactory.createForUserAssignedManagedI
|
||||
});
|
||||
```
|
||||
|
||||
### DefaultAzureCredential Authentication
|
||||
|
||||
tip: see a real sample here: [samples/interactive-browser/index.ts](./samples/interactive-browser/index.ts)
|
||||
|
||||
The DefaultAzureCredential from @azure/identity provides a simplified authentication experience that automatically tries different authentication methods based on the environment. This is especially useful for applications that need to work in different environments (local development, CI/CD, and production).
|
||||
|
||||
```typescript
|
||||
import { createClient } from '@redis/client';
|
||||
import { DefaultAzureCredential } from '@azure/identity';
|
||||
import { EntraIdCredentialsProviderFactory, REDIS_SCOPE_DEFAULT } from '@redis/entraid/dist/lib/entra-id-credentials-provider-factory';
|
||||
|
||||
// Create a DefaultAzureCredential instance
|
||||
const credential = new DefaultAzureCredential();
|
||||
|
||||
// Create a provider using DefaultAzureCredential
|
||||
const provider = EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({
|
||||
// Use the same parameters you would pass to credential.getToken()
|
||||
credential,
|
||||
scopes: REDIS_SCOPE_DEFAULT, // The Redis scope
|
||||
// Optional additional parameters for getToken
|
||||
options: {
|
||||
// Any options you would normally pass to credential.getToken()
|
||||
},
|
||||
tokenManagerConfig: {
|
||||
expirationRefreshRatio: 0.8
|
||||
}
|
||||
});
|
||||
|
||||
const client = createClient({
|
||||
url: 'redis://your-host',
|
||||
credentialsProvider: provider
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
```
|
||||
|
||||
#### Important Notes on Using DefaultAzureCredential
|
||||
|
||||
When using the `createForDefaultAzureCredential` method, you need to:
|
||||
|
||||
1. Create your own instance of `DefaultAzureCredential`
|
||||
2. Pass the same parameters to the factory method that you would use with the `getToken()` method:
|
||||
- `scopes`: The Redis scope (use the exported `REDIS_SCOPE_DEFAULT` constant)
|
||||
- `options`: Any additional options for the getToken method
|
||||
|
||||
This factory method creates a wrapper around DefaultAzureCredential that adapts it to the Redis client's
|
||||
authentication system, while maintaining all the flexibility of the original Azure Identity authentication.
|
||||
|
||||
## Important Limitations
|
||||
|
||||
### RESP2 PUB/SUB Limitations
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { DefaultAzureCredential, EnvironmentCredential } from '@azure/identity';
|
||||
import { BasicAuth } from '@redis/client/dist/lib/authx';
|
||||
import { createClient } from '@redis/client';
|
||||
import { EntraIdCredentialsProviderFactory } from '../lib/entra-id-credentials-provider-factory';
|
||||
import { EntraIdCredentialsProviderFactory, REDIS_SCOPE_DEFAULT } from '../lib/entra-id-credentials-provider-factory';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { spy, SinonSpy } from 'sinon';
|
||||
import { randomUUID } from 'crypto';
|
||||
@@ -51,6 +52,35 @@ describe('EntraID Integration Tests', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('client with DefaultAzureCredential should be able to authenticate/re-authenticate', async () => {
|
||||
|
||||
const azureCredential = new DefaultAzureCredential();
|
||||
|
||||
await runAuthenticationTest(() =>
|
||||
EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({
|
||||
credential: azureCredential,
|
||||
scopes: REDIS_SCOPE_DEFAULT,
|
||||
tokenManagerConfig: {
|
||||
expirationRefreshRatio: 0.00001
|
||||
}
|
||||
})
|
||||
, { testingDefaultAzureCredential: true });
|
||||
});
|
||||
|
||||
it('client with EnvironmentCredential should be able to authenticate/re-authenticate', async () => {
|
||||
const envCredential = new EnvironmentCredential();
|
||||
|
||||
await runAuthenticationTest(() =>
|
||||
EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({
|
||||
credential: envCredential,
|
||||
scopes: REDIS_SCOPE_DEFAULT,
|
||||
tokenManagerConfig: {
|
||||
expirationRefreshRatio: 0.00001
|
||||
}
|
||||
})
|
||||
, { testingDefaultAzureCredential: true });
|
||||
});
|
||||
|
||||
interface TestConfig {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
@@ -83,15 +113,15 @@ describe('EntraID Integration Tests', () => {
|
||||
});
|
||||
|
||||
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
|
||||
endpoints: await loadFromFile(requiredEnvVars.REDIS_ENDPOINTS_CONFIG_PATH as string),
|
||||
clientId: requiredEnvVars.AZURE_CLIENT_ID as string,
|
||||
clientSecret: requiredEnvVars.AZURE_CLIENT_SECRET as string,
|
||||
authority: requiredEnvVars.AZURE_AUTHORITY as string,
|
||||
tenantId: requiredEnvVars.AZURE_TENANT_ID as string,
|
||||
redisScopes: requiredEnvVars.AZURE_REDIS_SCOPES as string,
|
||||
cert: requiredEnvVars.AZURE_CERT as string,
|
||||
privateKey: requiredEnvVars.AZURE_PRIVATE_KEY as string,
|
||||
userAssignedManagedId: requiredEnvVars.AZURE_USER_ASSIGNED_MANAGED_ID as string
|
||||
};
|
||||
};
|
||||
|
||||
@@ -127,12 +157,22 @@ describe('EntraID Integration Tests', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const validateTokens = (reAuthSpy: SinonSpy) => {
|
||||
/**
|
||||
* Validates authentication tokens generated during re-authentication
|
||||
*
|
||||
* @param reAuthSpy - The Sinon spy on the reAuthenticate method
|
||||
* @param skipUniqueCheckForDefaultAzureCredential - Skip the unique check for DefaultAzureCredential as there are no guarantees that the tokens will be unique
|
||||
* if the test is using default azure credential
|
||||
*/
|
||||
const validateTokens = (reAuthSpy: SinonSpy, skipUniqueCheckForDefaultAzureCredential: boolean) => {
|
||||
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;
|
||||
if (!creds.password) {
|
||||
throw new Error('Expected password to be set in BasicAuth credentials');
|
||||
}
|
||||
const tokenPayload = JSON.parse(
|
||||
Buffer.from(creds.password.split('.')[1], 'base64').toString()
|
||||
);
|
||||
@@ -146,6 +186,8 @@ describe('EntraID Integration Tests', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// we can't guarantee that the tokens will be unique when using DefaultAzureCredential
|
||||
if (!skipUniqueCheckForDefaultAzureCredential) {
|
||||
// Verify unique tokens
|
||||
const uniqueTokens = new Set(tokenDetails.map(detail => detail.token));
|
||||
assert.equal(
|
||||
@@ -159,7 +201,7 @@ describe('EntraID Integration Tests', () => {
|
||||
assert.equal(
|
||||
uniqueLifetimes.size,
|
||||
1,
|
||||
`Expected all tokens to have the same lifetime, but found ${uniqueLifetimes.size} different lifetimes: ${[uniqueLifetimes].join(', ')} seconds`
|
||||
`Expected all tokens to have the same lifetime, but found ${uniqueLifetimes.size} different lifetimes: ${(Array.from(uniqueLifetimes).join(','))} seconds`
|
||||
);
|
||||
|
||||
// Verify that all tokens have different uti (unique token identifier)
|
||||
@@ -167,17 +209,20 @@ describe('EntraID Integration Tests', () => {
|
||||
assert.equal(
|
||||
uniqueUti.size,
|
||||
reAuthSpy.callCount,
|
||||
`Expected all tokens to have different uti, but found ${uniqueUti.size} different uti in: ${[uniqueUti].join(', ')}`
|
||||
`Expected all tokens to have different uti, but found ${uniqueUti.size} different uti in: ${(Array.from(uniqueUti).join(','))}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const runAuthenticationTest = async (setupCredentialsProvider: () => any) => {
|
||||
const runAuthenticationTest = async (setupCredentialsProvider: () => any, options: {
|
||||
testingDefaultAzureCredential: boolean
|
||||
} = { testingDefaultAzureCredential: false }) => {
|
||||
const { client, reAuthSpy } = await setupTestClient(setupCredentialsProvider());
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
await runClientOperations(client);
|
||||
validateTokens(reAuthSpy);
|
||||
validateTokens(reAuthSpy, options.testingDefaultAzureCredential);
|
||||
} finally {
|
||||
await client.destroy();
|
||||
}
|
||||
|
22
packages/entraid/lib/azure-identity-provider.ts
Normal file
22
packages/entraid/lib/azure-identity-provider.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { AccessToken } from '@azure/core-auth';
|
||||
|
||||
import { IdentityProvider, TokenResponse } from '@redis/client/dist/lib/authx';
|
||||
|
||||
export class AzureIdentityProvider implements IdentityProvider<AccessToken> {
|
||||
private readonly getToken: () => Promise<AccessToken>;
|
||||
|
||||
constructor(getToken: () => Promise<AccessToken>) {
|
||||
this.getToken = getToken;
|
||||
}
|
||||
|
||||
async requestToken(): Promise<TokenResponse<AccessToken>> {
|
||||
const result = await this.getToken();
|
||||
return {
|
||||
token: result,
|
||||
ttlMs: result.expiresOnTimestamp - Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import type { GetTokenOptions, TokenCredential } from '@azure/core-auth';
|
||||
import { NetworkError } from '@azure/msal-common';
|
||||
import {
|
||||
LogLevel,
|
||||
@@ -7,8 +8,9 @@ import {
|
||||
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 { RetryPolicy, TokenManager, TokenManagerConfig, ReAuthenticationError, BasicAuth } from '@redis/client/dist/lib/authx';
|
||||
import { AzureIdentityProvider } from './azure-identity-provider';
|
||||
import { AuthenticationResponse, DEFAULT_CREDENTIALS_MAPPER, EntraidCredentialsProvider, OID_CREDENTIALS_MAPPER } from './entraid-credentials-provider';
|
||||
import { MSALIdentityProvider } from './msal-identity-provider';
|
||||
|
||||
/**
|
||||
@@ -51,7 +53,11 @@ export class EntraIdCredentialsProviderFactory {
|
||||
return new EntraidCredentialsProvider(
|
||||
new TokenManager(idp, params.tokenManagerConfig),
|
||||
idp,
|
||||
{ onReAuthenticationError: params.onReAuthenticationError, credentialsMapper: OID_CREDENTIALS_MAPPER }
|
||||
{
|
||||
onReAuthenticationError: params.onReAuthenticationError,
|
||||
credentialsMapper: params.credentialsMapper ?? OID_CREDENTIALS_MAPPER,
|
||||
onRetryableError: params.onRetryableError
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +108,8 @@ export class EntraIdCredentialsProviderFactory {
|
||||
return new EntraidCredentialsProvider(new TokenManager(idp, params.tokenManagerConfig), idp,
|
||||
{
|
||||
onReAuthenticationError: params.onReAuthenticationError,
|
||||
credentialsMapper: OID_CREDENTIALS_MAPPER
|
||||
credentialsMapper: params.credentialsMapper ?? OID_CREDENTIALS_MAPPER,
|
||||
onRetryableError: params.onRetryableError
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,6 +145,42 @@ export class EntraIdCredentialsProviderFactory {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to create a credentials provider using DefaultAzureCredential.
|
||||
*
|
||||
* The user needs to create a configured instance of DefaultAzureCredential ( or any other class that implements TokenCredential )and pass it to this method.
|
||||
*
|
||||
* The default credentials mapper for this method is OID_CREDENTIALS_MAPPER which extracts the object ID from JWT
|
||||
* encoded token.
|
||||
*
|
||||
* Depending on the actual flow that DefaultAzureCredential uses, the user may need to provide different
|
||||
* credential mapper via the credentialsMapper parameter.
|
||||
*
|
||||
*/
|
||||
static createForDefaultAzureCredential(
|
||||
{
|
||||
credential,
|
||||
scopes,
|
||||
options,
|
||||
tokenManagerConfig,
|
||||
onReAuthenticationError,
|
||||
credentialsMapper,
|
||||
onRetryableError
|
||||
}: DefaultAzureCredentialsParams
|
||||
): EntraidCredentialsProvider {
|
||||
|
||||
const idp = new AzureIdentityProvider(
|
||||
() => credential.getToken(scopes, options).then(x => x === null ? Promise.reject('Token is null') : x)
|
||||
);
|
||||
|
||||
return new EntraidCredentialsProvider(new TokenManager(idp, tokenManagerConfig), idp,
|
||||
{
|
||||
onReAuthenticationError: onReAuthenticationError,
|
||||
credentialsMapper: credentialsMapper ?? OID_CREDENTIALS_MAPPER,
|
||||
onRetryableError: onRetryableError
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to create a credentials provider for the Authorization Code Flow with PKCE.
|
||||
* @param params
|
||||
@@ -194,7 +237,11 @@ export class EntraIdCredentialsProviderFactory {
|
||||
}
|
||||
);
|
||||
const tm = new TokenManager(idp, params.tokenManagerConfig);
|
||||
return new EntraidCredentialsProvider(tm, idp, { onReAuthenticationError: params.onReAuthenticationError });
|
||||
return new EntraidCredentialsProvider(tm, idp, {
|
||||
onReAuthenticationError: params.onReAuthenticationError,
|
||||
credentialsMapper: params.credentialsMapper ?? DEFAULT_CREDENTIALS_MAPPER,
|
||||
onRetryableError: params.onRetryableError
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -214,8 +261,8 @@ export class EntraIdCredentialsProviderFactory {
|
||||
|
||||
}
|
||||
|
||||
const REDIS_SCOPE_DEFAULT = 'https://redis.azure.com/.default';
|
||||
const REDIS_SCOPE = 'https://redis.azure.com'
|
||||
export const REDIS_SCOPE_DEFAULT = 'https://redis.azure.com/.default';
|
||||
export const REDIS_SCOPE = 'https://redis.azure.com'
|
||||
|
||||
export type AuthorityConfig =
|
||||
| { type: 'multi-tenant'; tenantId: string }
|
||||
@@ -234,7 +281,19 @@ export type CredentialParams = {
|
||||
authorityConfig?: AuthorityConfig;
|
||||
|
||||
tokenManagerConfig: TokenManagerConfig
|
||||
onReAuthenticationError?: (error: ReAuthenticationError) => void;
|
||||
onReAuthenticationError?: (error: ReAuthenticationError) => void
|
||||
credentialsMapper?: (token: AuthenticationResponse) => BasicAuth
|
||||
onRetryableError?: (error: string) => void
|
||||
}
|
||||
|
||||
export type DefaultAzureCredentialsParams = {
|
||||
scopes: string | string[],
|
||||
options?: GetTokenOptions,
|
||||
credential: TokenCredential
|
||||
tokenManagerConfig: TokenManagerConfig
|
||||
onReAuthenticationError?: (error: ReAuthenticationError) => void
|
||||
credentialsMapper?: (token: AuthenticationResponse) => BasicAuth
|
||||
onRetryableError?: (error: string) => void
|
||||
}
|
||||
|
||||
export type AuthCodePKCEParams = CredentialParams & {
|
||||
@@ -356,16 +415,3 @@ export class AuthCodeFlowHelper {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { AuthenticationResult } from '@azure/msal-common/node';
|
||||
import { AccessToken } from '@azure/core-auth';
|
||||
import {
|
||||
BasicAuth, StreamingCredentialsProvider, IdentityProvider, TokenManager,
|
||||
ReAuthenticationError, StreamingCredentialsListener, IDPError, Token, Disposable
|
||||
@@ -9,6 +10,9 @@ import {
|
||||
* 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 type AuthenticationResponse = AuthenticationResult | AccessToken
|
||||
|
||||
export class EntraidCredentialsProvider implements StreamingCredentialsProvider {
|
||||
readonly type = 'streaming-credentials-provider';
|
||||
|
||||
@@ -24,11 +28,11 @@ export class EntraidCredentialsProvider implements StreamingCredentialsProvider
|
||||
}> = [];
|
||||
|
||||
constructor(
|
||||
public readonly tokenManager: TokenManager<AuthenticationResult>,
|
||||
public readonly idp: IdentityProvider<AuthenticationResult>,
|
||||
public readonly tokenManager: TokenManager<AuthenticationResponse>,
|
||||
public readonly idp: IdentityProvider<AuthenticationResponse>,
|
||||
private readonly options: {
|
||||
onReAuthenticationError?: (error: ReAuthenticationError) => void;
|
||||
credentialsMapper?: (token: AuthenticationResult) => BasicAuth;
|
||||
credentialsMapper?: (token: AuthenticationResponse) => BasicAuth;
|
||||
onRetryableError?: (error: string) => void;
|
||||
} = {}
|
||||
) {
|
||||
@@ -69,7 +73,7 @@ export class EntraidCredentialsProvider implements StreamingCredentialsProvider
|
||||
|
||||
onReAuthenticationError: (error: ReAuthenticationError) => void;
|
||||
|
||||
#credentialsMapper: (token: AuthenticationResult) => BasicAuth;
|
||||
#credentialsMapper: (token: AuthenticationResponse) => BasicAuth;
|
||||
|
||||
#createTokenManagerListener(subscribers: Set<StreamingCredentialsListener<BasicAuth>>) {
|
||||
return {
|
||||
@@ -80,7 +84,7 @@ export class EntraidCredentialsProvider implements StreamingCredentialsProvider
|
||||
this.options.onRetryableError?.(error.message);
|
||||
}
|
||||
},
|
||||
onNext: (token: { value: AuthenticationResult }): void => {
|
||||
onNext: (token: { value: AuthenticationResult | AccessToken }): void => {
|
||||
const credentials = this.#credentialsMapper(token.value);
|
||||
subscribers.forEach(listener => listener.onNext(credentials));
|
||||
}
|
||||
@@ -101,10 +105,10 @@ export class EntraidCredentialsProvider implements StreamingCredentialsProvider
|
||||
};
|
||||
}
|
||||
|
||||
async #startTokenManagerAndObtainInitialToken(): Promise<Token<AuthenticationResult>> {
|
||||
const initialResponse = await this.idp.requestToken();
|
||||
const token = this.tokenManager.wrapAndSetCurrentToken(initialResponse.token, initialResponse.ttlMs);
|
||||
async #startTokenManagerAndObtainInitialToken(): Promise<Token<AuthenticationResponse>> {
|
||||
const { ttlMs, token: initialToken } = await this.idp.requestToken();
|
||||
|
||||
const token = this.tokenManager.wrapAndSetCurrentToken(initialToken, ttlMs);
|
||||
this.#tokenManagerDisposable = this.tokenManager.start(
|
||||
this.#createTokenManagerListener(this.#listeners),
|
||||
this.tokenManager.calculateRefreshTime(token)
|
||||
@@ -131,10 +135,61 @@ export class EntraidCredentialsProvider implements StreamingCredentialsProvider
|
||||
|
||||
}
|
||||
|
||||
const DEFAULT_CREDENTIALS_MAPPER = (token: AuthenticationResult): BasicAuth => ({
|
||||
export const DEFAULT_CREDENTIALS_MAPPER = (token: AuthenticationResponse): BasicAuth => {
|
||||
if (isAuthenticationResult(token)) {
|
||||
return {
|
||||
username: token.uniqueId,
|
||||
password: token.accessToken
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return OID_CREDENTIALS_MAPPER(token)
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_ERROR_HANDLER = (error: ReAuthenticationError) =>
|
||||
console.error('ReAuthenticationError', error);
|
||||
|
||||
export const OID_CREDENTIALS_MAPPER = (token: (AuthenticationResult | AccessToken)) => {
|
||||
|
||||
if (isAuthenticationResult(token)) {
|
||||
// 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
|
||||
})
|
||||
} else {
|
||||
const accessToken = JSON.parse(Buffer.from(token.token.split('.')[1], 'base64').toString());
|
||||
|
||||
return ({
|
||||
username: accessToken.oid,
|
||||
password: token.token
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a token is an MSAL AuthenticationResult
|
||||
*
|
||||
* @param auth - The token to check
|
||||
* @returns true if the token is an AuthenticationResult
|
||||
*/
|
||||
export function isAuthenticationResult(auth: AuthenticationResult | AccessToken): auth is AuthenticationResult {
|
||||
return typeof (auth as AuthenticationResult).accessToken === 'string' &&
|
||||
!('token' in auth)
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a token is an Azure Identity AccessToken
|
||||
*
|
||||
* @param auth - The token to check
|
||||
* @returns true if the token is an AccessToken
|
||||
*/
|
||||
export function isAccessToken(auth: AuthenticationResult | AccessToken): auth is AccessToken {
|
||||
return typeof (auth as AccessToken).token === 'string' &&
|
||||
!('accessToken' in auth);
|
||||
}
|
@@ -11,7 +11,6 @@ export class MSALIdentityProvider implements IdentityProvider<AuthenticationResu
|
||||
}
|
||||
|
||||
async requestToken(): Promise<TokenResponse<AuthenticationResult>> {
|
||||
try {
|
||||
const result = await this.getToken();
|
||||
|
||||
if (!result?.accessToken || !result?.expiresOn) {
|
||||
@@ -21,11 +20,6 @@ export class MSALIdentityProvider implements IdentityProvider<AuthenticationResu
|
||||
token: result,
|
||||
ttlMs: result.expiresOn.getTime() - Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -12,10 +12,12 @@
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && tsc",
|
||||
"start:auth-pkce": "tsx --tsconfig tsconfig.samples.json ./samples/auth-code-pkce/index.ts",
|
||||
"start:interactive-browser": "tsx --tsconfig tsconfig.samples.json ./samples/interactive-browser/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/identity": "4.7.0",
|
||||
"@azure/msal-node": "^2.16.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
111
packages/entraid/samples/interactive-browser/index.ts
Normal file
111
packages/entraid/samples/interactive-browser/index.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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';
|
||||
import { InteractiveBrowserCredential } from '@azure/identity';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
if (!process.env.SESSION_SECRET) {
|
||||
throw new Error('SESSION_SECRET environment variable must be set');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
app.get('/login', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// Create an instance of InteractiveBrowserCredential
|
||||
const credential = new InteractiveBrowserCredential({
|
||||
clientId: process.env.MSAL_CLIENT_ID!,
|
||||
tenantId: process.env.MSAL_TENANT_ID!,
|
||||
loginStyle: 'popup',
|
||||
redirectUri: 'http://localhost:3000/redirect'
|
||||
});
|
||||
|
||||
// Create Redis client using the EntraID credentials provider
|
||||
const entraidCredentialsProvider = EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({
|
||||
credential,
|
||||
scopes: ['user.read'],
|
||||
tokenManagerConfig: DEFAULT_TOKEN_MANAGER_CONFIG
|
||||
});
|
||||
|
||||
// Subscribe to credentials updates
|
||||
const initialCredentials = entraidCredentialsProvider.subscribe({
|
||||
onNext: (token) => {
|
||||
// Never log the full token in production
|
||||
console.log('Token acquired successfully');
|
||||
console.log('Username:', token.username);
|
||||
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Token acquisition failed:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the initial credentials
|
||||
const [credentials] = await initialCredentials;
|
||||
|
||||
// Return success response
|
||||
res.json({
|
||||
status: 'success',
|
||||
message: 'Authentication successful',
|
||||
credentials: {
|
||||
username: credentials.username,
|
||||
password: credentials.password
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Authentication failed:', error);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
message: 'Authentication failed',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Create a simple status page
|
||||
app.get('/', (req: Request, res: Response) => {
|
||||
res.send(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Interactive Browser Credential Demo</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
|
||||
.button { display: inline-block; padding: 10px 20px; background: #0078d4; color: white; text-decoration: none; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Interactive Browser Credential Demo</h1>
|
||||
<p>This example demonstrates using the InteractiveBrowserCredential from @azure/identity to authenticate with Microsoft Entra ID.</p>
|
||||
<p>When you click the button below, you'll be redirected to the Microsoft login page.</p>
|
||||
<a href="/login" class="button">Login with Microsoft</a>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
console.log(`Open http://localhost:${PORT} in your browser to start`);
|
||||
});
|
Reference in New Issue
Block a user