diff --git a/.eslintrc.json b/.eslintrc.json index 8ba57bebe..f1ed55072 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -82,7 +82,9 @@ "no-case-declarations": ["error"], "no-empty": ["error"], "@typescript-eslint/no-empty-function": ["error"], - "@typescript-eslint/ban-types": ["error"], + "@typescript-eslint/no-empty-object-type": ["error"], + "@typescript-eslint/no-unsafe-function-type": ["error"], + "@typescript-eslint/no-wrapper-object-types": ["error"], "no-useless-escape": ["error"], "no-prototype-builtins": ["error"], "prefer-spread": ["error"], diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1dc613ef..e21e96068 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,20 +77,20 @@ jobs: path: angular-auth-oidc-client-artefact - name: Install AngularCLI globally - run: sudo npm install -g @angular/cli + run: npm install -g @angular/cli - name: Show ng Version run: ng version - name: Create Angular Project - run: sudo ng new angular-auth-oidc-client-test --skip-git + run: ng new angular-auth-oidc-client-test --skip-git - name: Npm Install & Install Library from local artefact run: | - sudo cp -R angular-auth-oidc-client-artefact angular-auth-oidc-client-test/ + cp -R angular-auth-oidc-client-artefact angular-auth-oidc-client-test/ cd angular-auth-oidc-client-test - sudo npm install --unsafe-perm=true - sudo ng add ./angular-auth-oidc-client-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation + npm install --unsafe-perm=true + ng add ./angular-auth-oidc-client-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation - name: Test Angular Application working-directory: ./angular-auth-oidc-client-test @@ -98,7 +98,7 @@ jobs: - name: Build Angular Application working-directory: ./angular-auth-oidc-client-test - run: sudo npm run build + run: npm run build AngularLatestVersionWithSchematics: needs: build_job @@ -117,20 +117,20 @@ jobs: path: angular-auth-oidc-client-artefact - name: Install AngularCLI globally - run: sudo npm install -g @angular/cli + run: npm install -g @angular/cli - name: Show ng Version run: ng version - name: Create Angular Project - run: sudo ng new angular-auth-oidc-client-test --skip-git + run: ng new angular-auth-oidc-client-test --skip-git - name: Npm Install & Install Library from local artefact run: | - sudo cp -R angular-auth-oidc-client-artefact angular-auth-oidc-client-test/ + cp -R angular-auth-oidc-client-artefact angular-auth-oidc-client-test/ cd angular-auth-oidc-client-test - sudo npm install --unsafe-perm=true - sudo ng add ./angular-auth-oidc-client-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "Default config" --use-local-package=true --skip-confirmation + npm install --unsafe-perm=true + ng add ./angular-auth-oidc-client-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "Default config" --use-local-package=true --skip-confirmation - name: Test Angular Application working-directory: ./angular-auth-oidc-client-test @@ -138,7 +138,7 @@ jobs: - name: Build Angular Application working-directory: ./angular-auth-oidc-client-test - run: sudo npm run build + run: npm run build AngularLatestVersionWithNgModuleSchematics: needs: build_job @@ -157,20 +157,20 @@ jobs: path: angular-auth-oidc-client-artefact - name: Install AngularCLI globally - run: sudo npm install -g @angular/cli + run: npm install -g @angular/cli - name: Show ng Version run: ng version - name: Create Angular Project - run: sudo ng new angular-auth-oidc-client-test --skip-git --standalone=false + run: ng new angular-auth-oidc-client-test --skip-git --standalone=false - name: Npm Install & Install Library from local artefact run: | - sudo cp -R angular-auth-oidc-client-artefact angular-auth-oidc-client-test/ + cp -R angular-auth-oidc-client-artefact angular-auth-oidc-client-test/ cd angular-auth-oidc-client-test - sudo npm install --unsafe-perm=true - sudo ng add ./angular-auth-oidc-client-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation + npm install --unsafe-perm=true + ng add ./angular-auth-oidc-client-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation - name: Test Angular Application working-directory: ./angular-auth-oidc-client-test @@ -178,7 +178,7 @@ jobs: - name: Build Angular Application working-directory: ./angular-auth-oidc-client-test - run: sudo npm run build + run: npm run build Angular16VersionWithRxJs6: needs: build_job @@ -197,24 +197,24 @@ jobs: path: angular-auth-oidc-client-artefact - name: Install AngularCLI globally - run: sudo npm install -g @angular/cli@16 + run: npm install -g @angular/cli@16 - name: Show ng Version run: ng version - name: Create Angular Project - run: sudo ng new angular-auth-oidc-client-test --skip-git + run: ng new angular-auth-oidc-client-test --skip-git - name: npm install RxJs 6 working-directory: ./angular-auth-oidc-client-test - run: sudo npm install rxjs@6.5.3 + run: npm install rxjs@6.5.3 - name: Npm Install & Install Library from local artefact run: | - sudo cp -R angular-auth-oidc-client-artefact angular-auth-oidc-client-test/ + cp -R angular-auth-oidc-client-artefact angular-auth-oidc-client-test/ cd angular-auth-oidc-client-test - sudo npm install --unsafe-perm=true - sudo ng add ./angular-auth-oidc-client-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation + npm install --unsafe-perm=true + ng add ./angular-auth-oidc-client-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation - name: Test Angular Application working-directory: ./angular-auth-oidc-client-test @@ -222,7 +222,7 @@ jobs: - name: Build Angular Application working-directory: ./angular-auth-oidc-client-test - run: sudo npm run build + run: npm run build LibWithAngularV16: needs: build_job @@ -241,20 +241,20 @@ jobs: path: angular-auth-oidc-client-artefact - name: Install AngularCLI globally - run: sudo npm install -g @angular/cli@16 + run: npm install -g @angular/cli@16 - name: Show ng Version run: ng version - name: Create Angular Project - run: sudo ng new angular-auth-oidc-client-test --skip-git + run: ng new angular-auth-oidc-client-test --skip-git - name: Npm Install & Install Library from local artefact run: | - sudo cp -R angular-auth-oidc-client-artefact angular-auth-oidc-client-test/ + cp -R angular-auth-oidc-client-artefact angular-auth-oidc-client-test/ cd angular-auth-oidc-client-test - sudo npm install --unsafe-perm=true - sudo ng add ./angular-auth-oidc-client-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation + npm install --unsafe-perm=true + ng add ./angular-auth-oidc-client-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation - name: Test Angular Application working-directory: ./angular-auth-oidc-client-test @@ -262,4 +262,4 @@ jobs: - name: Build Angular Application working-directory: ./angular-auth-oidc-client-test - run: sudo npm run build + run: npm run build diff --git a/angular.json b/angular.json index 58b0baa67..eed004818 100644 --- a/angular.json +++ b/angular.json @@ -2041,6 +2041,119 @@ } } } + }, + "integration-tests": { + "projectType": "application", + "schematics": { + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:component": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "projects/integration-tests", + "sourceRoot": "projects/integration-tests/src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/integration-tests", + "index": "projects/integration-tests/src/index.html", + "browser": "projects/integration-tests/src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "projects/integration-tests/tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "projects/integration-tests/public" + } + ], + "styles": [ + "projects/integration-tests/src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "integration-tests:build:production" + }, + "development": { + "buildTarget": "integration-tests:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "projects/integration-tests/tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "projects/integration-tests/src/assets", + "output": "/assets" + } + ], + "styles": [], + "scripts": [] + } + } + } } }, "cli": { diff --git a/docs/site/angular-auth-oidc-client/docs/documentation/silent-renew.md b/docs/site/angular-auth-oidc-client/docs/documentation/silent-renew.md index f1c86d531..f0d28199b 100644 --- a/docs/site/angular-auth-oidc-client/docs/documentation/silent-renew.md +++ b/docs/site/angular-auth-oidc-client/docs/documentation/silent-renew.md @@ -97,13 +97,21 @@ Both the access token and the id_token are used to start this process. window.onload = function () { /* The parent window hosts the Angular application */ const parent = window.parent; + /* Send the id_token information to the oidc message handler */ - const event = new CustomEvent('oidc-silent-renew-message', { detail: window.location }); + const event = new CustomEvent('oidc-silent-renew-message', { + detail: { + url: window.location, + srcFrameId: window.frameElement?.id + } + }); parent.dispatchEvent(event); }; ``` +**Note:** When using multiple authentication configurations, each iframe is created with a unique identifier that includes the configId. The silent-renew.html script includes the iframe's id in the event as `srcFrameId`, allowing the library to extract the configId and correctly route the authentication response to the appropriate configuration. + If you are working with the [Angular CLI](https://angular.io/cli) make sure you add the `silent-renew.html` file to the assets configuration in your `angular.json`. This has already been done for you if you used the `ng add` schematics to install and setup the library. ```json @@ -119,8 +127,14 @@ If you are working with the [Angular CLI](https://angular.io/cli) make sure you window.onload = function () { /* The parent window hosts the Angular application */ const parent = window.parent; + /* Send the id_token information to the oidc message handler */ - const event = new CustomEvent('oidc-silent-renew-message', { detail: window.location.hash.substr(1) }); + const event = new CustomEvent('oidc-silent-renew-message', { + detail: { + url: window.location.hash.substr(1), + srcFrameId: window.frameElement?.id + } + }); parent.dispatchEvent(event); }; diff --git a/package-lock.json b/package-lock.json index 226a44acc..a160ae46c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,8 +36,8 @@ "@evilmartians/lefthook": "^1.0.3", "@types/jasmine": "^4.0.0", "@types/node": "^22.10.1", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", + "@typescript-eslint/eslint-plugin": "^8.20.0", + "@typescript-eslint/parser": "^8.20.0", "copyfiles": "^2.4.1", "coveralls": "^3.1.0", "eslint": "^8.57.0", @@ -52,6 +52,7 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.0.0", + "karma-server-side": "^1.8.0", "ng-packagr": "^19.0.1", "prettier": "^2.2.1", "rfc4648": "^1.5.0", @@ -4210,16 +4211,20 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -6795,139 +6800,159 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz", - "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", + "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.11.0", - "@typescript-eslint/type-utils": "7.11.0", - "@typescript-eslint/utils": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/type-utils": "8.35.1", + "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.35.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", - "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", + "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", + "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.11.0", - "@typescript-eslint/utils": "7.11.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", + "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", - "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", + "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.11.0", - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/typescript-estree": "7.11.0" + "@typescript-eslint/tsconfig-utils": "^8.35.1", + "@typescript-eslint/types": "^8.35.1", + "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", - "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", + "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "7.11.0", - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/typescript-estree": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0", - "debug": "^4.3.4" + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", + "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "eslint": "^8.56.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", - "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", + "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0" + "@typescript-eslint/typescript-estree": "8.35.1", + "@typescript-eslint/utils": "8.35.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", - "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", + "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", "dev": true, + "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -6935,47 +6960,50 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", - "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", + "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0", + "@typescript-eslint/project-service": "8.35.1", + "@typescript-eslint/tsconfig-utils": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -6987,17 +7015,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz", - "integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", + "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.17.0", - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/typescript-estree": "8.17.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7007,88 +7034,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz", - "integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz", - "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz", - "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz", - "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", + "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/types": "8.17.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.35.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7098,24 +7056,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7123,40 +7069,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript-eslint/utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", - "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.11.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -7408,6 +7320,13 @@ "node": ">=8.9.0" } }, + "node_modules/after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==", + "dev": true, + "license": "MIT" + }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -7641,15 +7560,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/array.prototype.flat": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", @@ -7707,6 +7617,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "dev": true, + "license": "MIT" + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -7725,6 +7642,13 @@ "node": ">=0.8" } }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -7876,11 +7800,27 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha512-437oANT9tP582zZMwSvZGy2nmSeAb8DW2me3y+Uv1Wp2Rulr8Mqlyrv3E7MLxmsiaPSMMDmiDVzgE+e8zlMx9g==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -7942,6 +7882,18 @@ "postcss-media-query-parser": "^0.2.3" } }, + "node_modules/better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha512-bYeph2DFlpK1XmGs6fvlLRUN29QISM3GBuUwSFsMY2XRx4AvC0WNCS57j4c/xGrK2RS24C1w3YoBOsw9fT46tQ==", + "dev": true, + "dependencies": { + "callsite": "1.0.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -7995,6 +7947,13 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -8355,6 +8314,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -8707,6 +8675,25 @@ "dev": true, "license": "MIT" }, + "node_modules/component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==", + "dev": true + }, + "node_modules/component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==", + "dev": true + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -9332,6 +9319,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -9403,18 +9403,6 @@ "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -9611,6 +9599,67 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.3.tgz", + "integrity": "sha512-PXIgpzb1brtBzh8Q6vCjzCMeu4nfEPmaDm+L3Qb2sVHwLkxC1qRiBMSjOB0NJNjZ0hbPNUKQa+s8J2XxLOIEeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.1", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.6.3", + "yeast": "0.1.2" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/engine.io-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", + "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "node_modules/engine.io-client/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/engine.io-client/node_modules/ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/engine.io-parser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", @@ -11127,26 +11176,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -11241,6 +11270,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "2.0.1" + } + }, + "node_modules/has-binary2/node_modules/isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==", + "dev": true, + "license": "MIT" + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -11697,6 +11750,12 @@ "node": ">=8" } }, + "node_modules/indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==", + "dev": true + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -11791,6 +11850,19 @@ "node": ">= 10" } }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -11875,6 +11947,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -11890,6 +11975,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -12612,6 +12711,18 @@ "karma-jasmine": "^5.0.0" } }, + "node_modules/karma-server-side": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/karma-server-side/-/karma-server-side-1.8.0.tgz", + "integrity": "sha512-l82gcTphvmgTf9ev9so2CqWZEEidMcunJ/cVA3Ed5q9pClpiV4NcfXaI0gvd7PYRdiP1Z+S4kDSjYdQbnPRXeQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "lowscore": "1.17.0", + "parse-function": "^2.3.2", + "socket.io-client": "2.2.0" + } + }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -13360,6 +13471,13 @@ "node": ">=8.0" } }, + "node_modules/lowscore": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/lowscore/-/lowscore-1.17.0.tgz", + "integrity": "sha512-HB+RehFPhk5bB19hfy92F1KpsH8D1MnyySp+aXhprtDTBvjcBq3SF90PAqpmIEz3VLw5rcjrCQrtmMbNcPW3Rg==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -14765,6 +14883,12 @@ "node": ">=0.10.0" } }, + "node_modules/object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha512-S0sN3agnVh2SZNEIGc0N1X4Z5K0JeFbGBrnuZpsxuUh5XLF0BnvWkMjRXo/zGKLd/eghvNIKcx1pQkmUjXIyrA==", + "dev": true + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -15132,6 +15256,30 @@ "node": ">=6" } }, + "node_modules/parse-function": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/parse-function/-/parse-function-2.3.2.tgz", + "integrity": "sha512-8q0EP7LoexHl3yCX/gIpiG5c/1WolBRkXL7j6ou7H45ddzT0/btN/3ZD9mklx+U2go12PaPJIG1QdJO2Q6Vn+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^3.1.0", + "define-property": "^0.2.5" + } + }, + "node_modules/parse-function/node_modules/acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -15206,6 +15354,26 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha512-B3Nrjw2aL7aI4TDujOzfA4NsEc4u1lVcIRE0xesutH8kjeWF70uk+W5cBlIQx04zUH9NTBvuN36Y9xLRPK6Jjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha512-ijhdxJu6l5Ru12jF0JvzXVPvsC+VibqeaExlNoMhWN6VQ79PGjkmc7oA4W1lp00sFkNyj0fx6ivPLdV51/UMog==", + "dev": true, + "license": "MIT", + "dependencies": { + "better-assert": "~1.0.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -15277,15 +15445,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -16603,15 +16762,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -16680,6 +16830,75 @@ "ws": "~8.11.0" } }, + "node_modules/socket.io-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz", + "integrity": "sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "engine.io-client": "~3.3.1", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/socket.io-client/node_modules/isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/socket.io-client/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/socket.io-client/node_modules/socket.io-parser": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.4.tgz", + "integrity": "sha512-z/pFQB3x+EZldRRzORYW1vwVO8m/3ILkswtnpoeU6Ve3cbMWkmHEWDAVJn4QJtchiiFTo5j7UG2QvwxvaA9vow==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "isarray": "2.0.1" + } + }, + "node_modules/socket.io-client/node_modules/socket.io-parser/node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -17412,6 +17631,12 @@ "node": ">=8.17.0" } }, + "node_modules/to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==", + "dev": true + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -17474,15 +17699,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-node": { @@ -18792,6 +19018,15 @@ } } }, + "node_modules/xmlhttprequest-ssl": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", + "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -18844,6 +19079,13 @@ "node": ">=12" } }, + "node_modules/yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==", + "dev": true, + "license": "MIT" + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 073f6e775..9559afbad 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "build-lib-prod": "ng build angular-auth-oidc-client --configuration production && npm run schematics-build && npm run copy-files", "test-lib": "ng test angular-auth-oidc-client --code-coverage", "test-lib-ci": "ng test angular-auth-oidc-client --watch=false --browsers=ChromeHeadlessNoSandbox --code-coverage", + "test-integration": "ng test integration-tests --watch=false", "lint-lib-fix": "ng lint angular-auth-oidc-client --fix", "lint-lib": "ng lint angular-auth-oidc-client", "pack-lib": "npm run build-lib-prod && npm pack ./dist/angular-auth-oidc-client", @@ -82,8 +83,8 @@ "@evilmartians/lefthook": "^1.0.3", "@types/jasmine": "^4.0.0", "@types/node": "^22.10.1", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", + "@typescript-eslint/eslint-plugin": "^8.20.0", + "@typescript-eslint/parser": "^8.20.0", "copyfiles": "^2.4.1", "coveralls": "^3.1.0", "eslint": "^8.57.0", @@ -98,6 +99,7 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.0.0", + "karma-server-side": "^1.8.0", "ng-packagr": "^19.0.1", "prettier": "^2.2.1", "rfc4648": "^1.5.0", diff --git a/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.ts b/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.ts index a8e219192..56f09d558 100644 --- a/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.ts +++ b/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.ts @@ -4,4 +4,5 @@ export interface AuthStateResult { isAuthenticated: boolean; validationResult: ValidationResult; isRenewProcess: boolean; + configId?: string; } diff --git a/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.spec.ts index 30b1c96fd..636ab2690 100644 --- a/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.spec.ts @@ -1,657 +1,666 @@ -import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { of, throwError } from 'rxjs'; -import { delay } from 'rxjs/operators'; -import { mockProvider } from '../../test/auto-mock'; -import { AuthStateService } from '../auth-state/auth-state.service'; -import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service'; -import { CallbackContext } from '../flows/callback-context'; -import { FlowsDataService } from '../flows/flows-data.service'; -import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service'; -import { SilentRenewService } from '../iframe/silent-renew.service'; -import { LoggerService } from '../logging/logger.service'; -import { LoginResponse } from '../login/login-response'; -import { PublicEventsService } from '../public-events/public-events.service'; -import { StoragePersistenceService } from '../storage/storage-persistence.service'; -import { UserService } from '../user-data/user.service'; -import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; -import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service'; -import { - MAX_RETRY_ATTEMPTS, - RefreshSessionService, -} from './refresh-session.service'; - -describe('RefreshSessionService ', () => { - let refreshSessionService: RefreshSessionService; - let flowHelper: FlowHelper; - let authStateService: AuthStateService; - let silentRenewService: SilentRenewService; - let storagePersistenceService: StoragePersistenceService; - let flowsDataService: FlowsDataService; - let refreshSessionIframeService: RefreshSessionIframeService; - let refreshSessionRefreshTokenService: RefreshSessionRefreshTokenService; - let authWellKnownService: AuthWellKnownService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [], - providers: [ - FlowHelper, - mockProvider(FlowsDataService), - RefreshSessionService, - mockProvider(LoggerService), - mockProvider(SilentRenewService), - mockProvider(AuthStateService), - mockProvider(AuthWellKnownService), - mockProvider(RefreshSessionIframeService), - mockProvider(StoragePersistenceService), - mockProvider(RefreshSessionRefreshTokenService), - mockProvider(UserService), - mockProvider(PublicEventsService), - ], - }); - }); - - beforeEach(() => { - refreshSessionService = TestBed.inject(RefreshSessionService); - flowsDataService = TestBed.inject(FlowsDataService); - flowHelper = TestBed.inject(FlowHelper); - authStateService = TestBed.inject(AuthStateService); - refreshSessionIframeService = TestBed.inject(RefreshSessionIframeService); - refreshSessionRefreshTokenService = TestBed.inject( - RefreshSessionRefreshTokenService - ); - silentRenewService = TestBed.inject(SilentRenewService); - authWellKnownService = TestBed.inject(AuthWellKnownService); - storagePersistenceService = TestBed.inject(StoragePersistenceService); - }); - - it('should create', () => { - expect(refreshSessionService).toBeTruthy(); - }); - - describe('userForceRefreshSession', () => { - it('should persist params refresh when extra custom params given and useRefreshToken is true', waitForAsync(() => { - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(true); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(of(null)); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - const writeSpy = spyOn(storagePersistenceService, 'write'); - const allConfigs = [ - { - configId: 'configId1', - useRefreshToken: true, - silentRenewTimeoutInSeconds: 10, - }, - ]; const extraCustomParams = { extra: 'custom' }; - - refreshSessionService - .userForceRefreshSession(allConfigs[0], allConfigs, extraCustomParams) - .subscribe(() => { - expect(writeSpy).toHaveBeenCalledOnceWith( - 'storageCustomParamsRefresh', - extraCustomParams, - allConfigs[0] - ); - }); - })); - - it('should persist storageCustomParamsAuthRequest when extra custom params given and useRefreshToken is false', waitForAsync(() => { - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(true); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(of(null)); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - const allConfigs = [ - { - configId: 'configId1', - useRefreshToken: false, - silentRenewTimeoutInSeconds: 10, - }, - ]; - const writeSpy = spyOn(storagePersistenceService, 'write'); const extraCustomParams = { extra: 'custom' }; - - refreshSessionService - .userForceRefreshSession(allConfigs[0], allConfigs, extraCustomParams) - .subscribe(() => { - expect(writeSpy).toHaveBeenCalledOnceWith( - 'storageCustomParamsAuthRequest', - extraCustomParams, - allConfigs[0] - ); - }); - })); - - it('should NOT persist customparams if no customparams are given', waitForAsync(() => { - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(true); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(of(null)); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - const allConfigs = [ - { - configId: 'configId1', - useRefreshToken: false, - silentRenewTimeoutInSeconds: 10, - }, - ]; - const writeSpy = spyOn(storagePersistenceService, 'write'); - - refreshSessionService - .userForceRefreshSession(allConfigs[0], allConfigs) - .subscribe(() => { - expect(writeSpy).not.toHaveBeenCalled(); - }); - })); - - it('should call resetSilentRenewRunning in case of an error', waitForAsync(() => { - spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( - throwError(() => new Error('error')) - ); - spyOn(flowsDataService, 'resetSilentRenewRunning'); - const allConfigs = [ - { - configId: 'configId1', - useRefreshToken: false, - silentRenewTimeoutInSeconds: 10, - }, - ]; - - refreshSessionService - .userForceRefreshSession(allConfigs[0], allConfigs) - .subscribe({ - next: () => { - fail('It should not return any result.'); - }, - error: (error) => { - expect(error).toBeInstanceOf(Error); - }, - complete: () => { - expect( - flowsDataService.resetSilentRenewRunning - ).toHaveBeenCalledOnceWith(allConfigs[0]); - }, - }); - })); - - it('should call resetSilentRenewRunning in case of no error', waitForAsync(() => { - spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( - of({} as LoginResponse) - ); - spyOn(flowsDataService, 'resetSilentRenewRunning'); - const allConfigs = [ - { - configId: 'configId1', - useRefreshToken: false, - silentRenewTimeoutInSeconds: 10, - }, - ]; - - refreshSessionService - .userForceRefreshSession(allConfigs[0], allConfigs) - .subscribe({ - error: () => { - fail('It should not return any error.'); - }, - complete: () => { - expect( - flowsDataService.resetSilentRenewRunning - ).toHaveBeenCalledOnceWith(allConfigs[0]); - }, - }); - })); - }); - - describe('forceRefreshSession', () => { - it('only calls start refresh session and returns idToken and accessToken if auth is true', waitForAsync(() => { - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(true); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(of(null)); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - spyOn(authStateService, 'getIdToken').and.returnValue('id-token'); - spyOn(authStateService, 'getAccessToken').and.returnValue('access-token'); - const allConfigs = [ - { - configId: 'configId1', - silentRenewTimeoutInSeconds: 10, - }, - ]; - - refreshSessionService - .forceRefreshSession(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result.idToken).toEqual('id-token'); - expect(result.accessToken).toEqual('access-token'); - }); - })); - - it('only calls start refresh session and returns null if auth is false', waitForAsync(() => { - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(true); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(of(null)); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - const allConfigs = [ - { - configId: 'configId1', - silentRenewTimeoutInSeconds: 10, - }, - ]; - - refreshSessionService - .forceRefreshSession(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - isAuthenticated: false, - errorMessage: '', - userData: null, - idToken: '', - accessToken: '', - configId: 'configId1', - }); - }); - })); - - it('calls start refresh session and waits for completed, returns idtoken and accesstoken if auth is true', waitForAsync(() => { - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(false); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(of(null)); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - true - ); - spyOnProperty( - silentRenewService, - 'refreshSessionWithIFrameCompleted$' - ).and.returnValue( - of({ - authResult: { - id_token: 'some-id_token', - access_token: 'some-access_token', - }, - } as CallbackContext) - ); - const allConfigs = [ - { - configId: 'configId1', - silentRenewTimeoutInSeconds: 10, - }, - ]; - - refreshSessionService - .forceRefreshSession(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result.idToken).toBeDefined(); - expect(result.accessToken).toBeDefined(); - }); - })); - - it('calls start refresh session and waits for completed, returns LoginResponse if auth is false', waitForAsync(() => { - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(false); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(of(null)); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - spyOnProperty( - silentRenewService, - 'refreshSessionWithIFrameCompleted$' - ).and.returnValue(of(null)); - const allConfigs = [ - { - configId: 'configId1', - silentRenewTimeoutInSeconds: 10, - }, - ]; - - refreshSessionService - .forceRefreshSession(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - isAuthenticated: false, - errorMessage: '', - userData: null, - idToken: '', - accessToken: '', - configId: 'configId1', - }); - }); - })); - - it('occurs timeout error and retry mechanism exhausted max retry count throws error', fakeAsync(() => { - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(false); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(of(null)); - spyOnProperty( - silentRenewService, - 'refreshSessionWithIFrameCompleted$' - ).and.returnValue(of(null).pipe(delay(11000))); - - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - const allConfigs = [ - { - configId: 'configId1', - silentRenewTimeoutInSeconds: 10, - }, - ]; const resetSilentRenewRunningSpy = spyOn( - flowsDataService, - 'resetSilentRenewRunning' - ); - const expectedInvokeCount = MAX_RETRY_ATTEMPTS; - - refreshSessionService - .forceRefreshSession(allConfigs[0], allConfigs) - .subscribe({ - next: () => { - fail('It should not return any result.'); - }, - error: (error) => { - expect(error).toBeInstanceOf(Error); - expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes( - expectedInvokeCount - ); - }, - }); - - tick(allConfigs[0].silentRenewTimeoutInSeconds * 10000); - })); - - it('occurs unknown error throws it to subscriber', fakeAsync(() => { - const allConfigs = [ - { - configId: 'configId1', - silentRenewTimeoutInSeconds: 10, - }, - ]; const expectedErrorMessage = 'Test error message'; - - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(false); - spyOnProperty( - silentRenewService, - 'refreshSessionWithIFrameCompleted$' - ).and.returnValue(of(null)); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(throwError(() => new Error(expectedErrorMessage))); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - - const resetSilentRenewRunningSpy = spyOn( - flowsDataService, - 'resetSilentRenewRunning' - ); - - refreshSessionService - .forceRefreshSession(allConfigs[0], allConfigs) - .subscribe({ - next: () => { - fail('It should not return any result.'); - }, - error: (error) => { - expect(error).toBeInstanceOf(Error); - expect(error.message).toEqual(`Error: ${expectedErrorMessage}`); - expect(resetSilentRenewRunningSpy).not.toHaveBeenCalled(); - }, - }); - })); - - describe('NOT isCurrentFlowCodeFlowWithRefreshTokens', () => { - it('does return null when not authenticated', waitForAsync(() => { - const allConfigs = [ - { - configId: 'configId1', - silentRenewTimeoutInSeconds: 10, - }, - ]; - - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(false); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(of(null)); - spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( - false - ); - spyOnProperty( - silentRenewService, - 'refreshSessionWithIFrameCompleted$' - ).and.returnValue(of(null)); - - refreshSessionService - .forceRefreshSession(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - isAuthenticated: false, - errorMessage: '', - userData: null, - idToken: '', - accessToken: '', - configId: 'configId1', - }); - }); - })); - - it('return value only returns once', waitForAsync(() => { - const allConfigs = [ - { - configId: 'configId1', - silentRenewTimeoutInSeconds: 10, - }, - ]; - - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(false); - spyOn( - refreshSessionService as any, - 'startRefreshSession' - ).and.returnValue(of(null)); - spyOnProperty( - silentRenewService, - 'refreshSessionWithIFrameCompleted$' - ).and.returnValue( - of({ - authResult: { - id_token: 'some-id_token', - access_token: 'some-access_token', - }, - } as CallbackContext) - ); - const spyInsideMap = spyOn( - authStateService, - 'areAuthStorageTokensValid' - ).and.returnValue(true); - - refreshSessionService - .forceRefreshSession(allConfigs[0], allConfigs) - .subscribe((result) => { - expect(result).toEqual({ - idToken: 'some-id_token', - accessToken: 'some-access_token', - isAuthenticated: true, - userData: undefined, - configId: 'configId1', - }); - expect(spyInsideMap).toHaveBeenCalledTimes(1); - }); - })); - }); - }); - - describe('startRefreshSession', () => { - it('returns null if no auth well known endpoint defined', waitForAsync(() => { - spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true); - - (refreshSessionService as any) - .startRefreshSession() - .subscribe((result: any) => { - expect(result).toBe(null); - }); - })); - - it('returns null if silent renew Is running', waitForAsync(() => { - spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true); - - (refreshSessionService as any) - .startRefreshSession() - .subscribe((result: any) => { - expect(result).toBe(null); - }); - })); - - it('calls `setSilentRenewRunning` when should be executed', waitForAsync(() => { - const setSilentRenewRunningSpy = spyOn( - flowsDataService, - 'setSilentRenewRunning' - ); - const allConfigs = [ - { - configId: 'configId1', - authWellknownEndpointUrl: 'https://authWell', - }, - ]; - - spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); - spyOn( - authWellKnownService, - 'queryAndStoreAuthWellKnownEndPoints' - ).and.returnValue(of({})); - - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(true); - spyOn( - refreshSessionRefreshTokenService, - 'refreshSessionWithRefreshTokens' - ).and.returnValue(of({} as CallbackContext)); - - (refreshSessionService as any) - .startRefreshSession(allConfigs[0], allConfigs) - .subscribe(() => { - expect(setSilentRenewRunningSpy).toHaveBeenCalled(); - }); - })); - - it('calls refreshSessionWithRefreshTokens when current flow is codeflow with refresh tokens', waitForAsync(() => { - spyOn(flowsDataService, 'setSilentRenewRunning'); - const allConfigs = [ - { - configId: 'configId1', - authWellknownEndpointUrl: 'https://authWell', - }, - ]; - - spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); - spyOn( - authWellKnownService, - 'queryAndStoreAuthWellKnownEndPoints' - ).and.returnValue(of({})); - - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(true); - const refreshSessionWithRefreshTokensSpy = spyOn( - refreshSessionRefreshTokenService, - 'refreshSessionWithRefreshTokens' - ).and.returnValue(of({} as CallbackContext)); - - (refreshSessionService as any) - .startRefreshSession(allConfigs[0], allConfigs) - .subscribe(() => { - expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled(); - }); - })); - - it('calls refreshSessionWithIframe when current flow is NOT codeflow with refresh tokens', waitForAsync(() => { - spyOn(flowsDataService, 'setSilentRenewRunning'); - const allConfigs = [ - { - configId: 'configId1', - authWellknownEndpointUrl: 'https://authWell', - }, - ]; - - spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); - spyOn( - authWellKnownService, - 'queryAndStoreAuthWellKnownEndPoints' - ).and.returnValue(of({})); - - spyOn( - flowHelper, - 'isCurrentFlowCodeFlowWithRefreshTokens' - ).and.returnValue(false); - const refreshSessionWithRefreshTokensSpy = spyOn( - refreshSessionRefreshTokenService, - 'refreshSessionWithRefreshTokens' - ).and.returnValue(of({} as CallbackContext)); const refreshSessionWithIframeSpy = spyOn( - refreshSessionIframeService, - 'refreshSessionWithIframe' - ).and.returnValue(of(false)); - - (refreshSessionService as any) - .startRefreshSession(allConfigs[0], allConfigs) - .subscribe(() => { - expect(refreshSessionWithRefreshTokensSpy).not.toHaveBeenCalled(); - expect(refreshSessionWithIframeSpy).toHaveBeenCalled(); - }); - })); - }); -}); +import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { of, throwError } from 'rxjs'; +import { delay } from 'rxjs/operators'; +import { mockProvider } from '../../test/auto-mock'; +import { AuthStateService } from '../auth-state/auth-state.service'; +import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service'; +import { CallbackContext } from '../flows/callback-context'; +import { FlowsDataService } from '../flows/flows-data.service'; +import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service'; +import { SilentRenewService } from '../iframe/silent-renew.service'; +import { LoggerService } from '../logging/logger.service'; +import { LoginResponse } from '../login/login-response'; +import { PublicEventsService } from '../public-events/public-events.service'; +import { StoragePersistenceService } from '../storage/storage-persistence.service'; +import { UserService } from '../user-data/user.service'; +import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; +import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service'; +import { + MAX_RETRY_ATTEMPTS, + RefreshSessionService, +} from './refresh-session.service'; + +describe('RefreshSessionService ', () => { + let refreshSessionService: RefreshSessionService; + let flowHelper: FlowHelper; + let authStateService: AuthStateService; + let silentRenewService: SilentRenewService; + let storagePersistenceService: StoragePersistenceService; + let flowsDataService: FlowsDataService; + let refreshSessionIframeService: RefreshSessionIframeService; + let refreshSessionRefreshTokenService: RefreshSessionRefreshTokenService; + let authWellKnownService: AuthWellKnownService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + FlowHelper, + mockProvider(FlowsDataService), + RefreshSessionService, + mockProvider(LoggerService), + mockProvider(SilentRenewService), + mockProvider(AuthStateService), + mockProvider(AuthWellKnownService), + mockProvider(RefreshSessionIframeService), + mockProvider(StoragePersistenceService), + mockProvider(RefreshSessionRefreshTokenService), + mockProvider(UserService), + mockProvider(PublicEventsService), + ], + }); + }); + + beforeEach(() => { + refreshSessionService = TestBed.inject(RefreshSessionService); + flowsDataService = TestBed.inject(FlowsDataService); + flowHelper = TestBed.inject(FlowHelper); + authStateService = TestBed.inject(AuthStateService); + refreshSessionIframeService = TestBed.inject(RefreshSessionIframeService); + refreshSessionRefreshTokenService = TestBed.inject( + RefreshSessionRefreshTokenService + ); + silentRenewService = TestBed.inject(SilentRenewService); + authWellKnownService = TestBed.inject(AuthWellKnownService); + storagePersistenceService = TestBed.inject(StoragePersistenceService); + }); + + it('should create', () => { + expect(refreshSessionService).toBeTruthy(); + }); + + describe('userForceRefreshSession', () => { + it('should persist params refresh when extra custom params given and useRefreshToken is true', waitForAsync(() => { + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(true); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + const writeSpy = spyOn(storagePersistenceService, 'write'); + const allConfigs = [ + { + configId: 'configId1', + useRefreshToken: true, + silentRenewTimeoutInSeconds: 10, + }, + ]; + const extraCustomParams = { extra: 'custom' }; + + refreshSessionService + .userForceRefreshSession(allConfigs[0], allConfigs, extraCustomParams) + .subscribe(() => { + expect(writeSpy).toHaveBeenCalledOnceWith( + 'storageCustomParamsRefresh', + extraCustomParams, + allConfigs[0] + ); + }); + })); + + it('should persist storageCustomParamsAuthRequest when extra custom params given and useRefreshToken is false', waitForAsync(() => { + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(true); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + const allConfigs = [ + { + configId: 'configId1', + useRefreshToken: false, + silentRenewTimeoutInSeconds: 10, + }, + ]; + const writeSpy = spyOn(storagePersistenceService, 'write'); + const extraCustomParams = { extra: 'custom' }; + + refreshSessionService + .userForceRefreshSession(allConfigs[0], allConfigs, extraCustomParams) + .subscribe(() => { + expect(writeSpy).toHaveBeenCalledOnceWith( + 'storageCustomParamsAuthRequest', + extraCustomParams, + allConfigs[0] + ); + }); + })); + + it('should NOT persist customparams if no customparams are given', waitForAsync(() => { + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(true); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + const allConfigs = [ + { + configId: 'configId1', + useRefreshToken: false, + silentRenewTimeoutInSeconds: 10, + }, + ]; + const writeSpy = spyOn(storagePersistenceService, 'write'); + + refreshSessionService + .userForceRefreshSession(allConfigs[0], allConfigs) + .subscribe(() => { + expect(writeSpy).not.toHaveBeenCalled(); + }); + })); + + it('should call resetSilentRenewRunning in case of an error', waitForAsync(() => { + spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( + throwError(() => new Error('error')) + ); + spyOn(flowsDataService, 'resetSilentRenewRunning'); + const allConfigs = [ + { + configId: 'configId1', + useRefreshToken: false, + silentRenewTimeoutInSeconds: 10, + }, + ]; + + refreshSessionService + .userForceRefreshSession(allConfigs[0], allConfigs) + .subscribe({ + next: () => { + fail('It should not return any result.'); + }, + error: (error) => { + expect(error).toBeInstanceOf(Error); + }, + complete: () => { + expect( + flowsDataService.resetSilentRenewRunning + ).toHaveBeenCalledOnceWith(allConfigs[0]); + }, + }); + })); + + it('should call resetSilentRenewRunning in case of no error', waitForAsync(() => { + spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( + of({} as LoginResponse) + ); + spyOn(flowsDataService, 'resetSilentRenewRunning'); + const allConfigs = [ + { + configId: 'configId1', + useRefreshToken: false, + silentRenewTimeoutInSeconds: 10, + }, + ]; + + refreshSessionService + .userForceRefreshSession(allConfigs[0], allConfigs) + .subscribe({ + error: () => { + fail('It should not return any error.'); + }, + complete: () => { + expect( + flowsDataService.resetSilentRenewRunning + ).toHaveBeenCalledOnceWith(allConfigs[0]); + }, + }); + })); + }); + + describe('forceRefreshSession', () => { + it('only calls start refresh session and returns idToken and accessToken if auth is true', waitForAsync(() => { + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(true); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOn(authStateService, 'getIdToken').and.returnValue('id-token'); + spyOn(authStateService, 'getAccessToken').and.returnValue('access-token'); + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result.idToken).toEqual('id-token'); + expect(result.accessToken).toEqual('access-token'); + }); + })); + + it('only calls start refresh session and returns null if auth is false', waitForAsync(() => { + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(true); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + isAuthenticated: false, + errorMessage: '', + userData: null, + idToken: '', + accessToken: '', + configId: 'configId1', + }); + }); + })); + + it('calls start refresh session and waits for completed, returns idtoken and accesstoken if auth is true', waitForAsync(() => { + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(false); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + true + ); + spyOnProperty( + silentRenewService, + 'refreshSessionWithIFrameCompleted$' + ).and.returnValue( + of({ + success: true, + authResult: { + id_token: 'some-id_token', + access_token: 'some-access_token', + }, + configId: 'configId1' + }) + ); + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result.idToken).toBeDefined(); + expect(result.accessToken).toBeDefined(); + }); + })); + + it('calls start refresh session and waits for completed, returns LoginResponse if auth is false', waitForAsync(() => { + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(false); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + spyOnProperty( + silentRenewService, + 'refreshSessionWithIFrameCompleted$' + ).and.returnValue(of({ success: false, configId: 'configId1' })); + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + isAuthenticated: false, + errorMessage: '', + userData: null, + idToken: '', + accessToken: '', + configId: 'configId1', + }); + }); + })); + + it('occurs timeout error and retry mechanism exhausted max retry count throws error', fakeAsync(() => { + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(false); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOnProperty( + silentRenewService, + 'refreshSessionWithIFrameCompleted$' + ).and.returnValue(of({success: false, configId: 'configId1' } as const).pipe(delay(11000))); + + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + const resetSilentRenewRunningSpy = spyOn( + flowsDataService, + 'resetSilentRenewRunning' + ); + const expectedInvokeCount = MAX_RETRY_ATTEMPTS; + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe({ + next: () => { + fail('It should not return any result.'); + }, + error: (error) => { + expect(error).toBeInstanceOf(Error); + expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes( + expectedInvokeCount + ); + }, + }); + + tick(allConfigs[0].silentRenewTimeoutInSeconds * 10000); + })); + + it('occurs unknown error throws it to subscriber', fakeAsync(() => { + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + const expectedErrorMessage = 'Test error message'; + + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(false); + spyOnProperty( + silentRenewService, + 'refreshSessionWithIFrameCompleted$' + ).and.returnValue(of({ success: false, configId: 'configId1' })); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(throwError(() => new Error(expectedErrorMessage))); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + + const resetSilentRenewRunningSpy = spyOn( + flowsDataService, + 'resetSilentRenewRunning' + ); + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe({ + next: () => { + fail('It should not return any result.'); + }, + error: (error) => { + expect(error).toBeInstanceOf(Error); + expect(error.message).toEqual(`Error: ${expectedErrorMessage}`); + expect(resetSilentRenewRunningSpy).not.toHaveBeenCalled(); + }, + }); + })); + + describe('NOT isCurrentFlowCodeFlowWithRefreshTokens', () => { + it('does return null when not authenticated', waitForAsync(() => { + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(false); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( + false + ); + spyOnProperty( + silentRenewService, + 'refreshSessionWithIFrameCompleted$' + ).and.returnValue(of({ success: false, configId: 'configId1' })); + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + isAuthenticated: false, + errorMessage: '', + userData: null, + idToken: '', + accessToken: '', + configId: 'configId1', + }); + }); + })); + + it('return value only returns once', waitForAsync(() => { + const allConfigs = [ + { + configId: 'configId1', + silentRenewTimeoutInSeconds: 10, + }, + ]; + + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(false); + spyOn( + refreshSessionService as any, + 'startRefreshSession' + ).and.returnValue(of(null)); + spyOnProperty( + silentRenewService, + 'refreshSessionWithIFrameCompleted$' + ).and.returnValue( + of({ + success: true, + authResult: { + id_token: 'some-id_token', + access_token: 'some-access_token', + }, + configId: 'configId1' + }) + ); + const spyInsideMap = spyOn( + authStateService, + 'areAuthStorageTokensValid' + ).and.returnValue(true); + + refreshSessionService + .forceRefreshSession(allConfigs[0], allConfigs) + .subscribe((result) => { + expect(result).toEqual({ + idToken: 'some-id_token', + accessToken: 'some-access_token', + isAuthenticated: true, + userData: undefined, + configId: 'configId1', + }); + expect(spyInsideMap).toHaveBeenCalledTimes(1); + }); + })); + }); + }); + + describe('startRefreshSession', () => { + it('returns null if no auth well known endpoint defined', waitForAsync(() => { + spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true); + + (refreshSessionService as any) + .startRefreshSession() + .subscribe((result: any) => { + expect(result).toBe(null); + }); + })); + + it('returns null if silent renew Is running', waitForAsync(() => { + spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true); + + (refreshSessionService as any) + .startRefreshSession() + .subscribe((result: any) => { + expect(result).toBe(null); + }); + })); + + it('calls `setSilentRenewRunning` when should be executed', waitForAsync(() => { + const setSilentRenewRunningSpy = spyOn( + flowsDataService, + 'setSilentRenewRunning' + ); + const allConfigs = [ + { + configId: 'configId1', + authWellknownEndpointUrl: 'https://authWell', + }, + ]; + + spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); + spyOn( + authWellKnownService, + 'queryAndStoreAuthWellKnownEndPoints' + ).and.returnValue(of({})); + + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(true); + spyOn( + refreshSessionRefreshTokenService, + 'refreshSessionWithRefreshTokens' + ).and.returnValue(of({} as CallbackContext)); + + (refreshSessionService as any) + .startRefreshSession(allConfigs[0], allConfigs) + .subscribe(() => { + expect(setSilentRenewRunningSpy).toHaveBeenCalled(); + }); + })); + + it('calls refreshSessionWithRefreshTokens when current flow is codeflow with refresh tokens', waitForAsync(() => { + spyOn(flowsDataService, 'setSilentRenewRunning'); + const allConfigs = [ + { + configId: 'configId1', + authWellknownEndpointUrl: 'https://authWell', + }, + ]; + + spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); + spyOn( + authWellKnownService, + 'queryAndStoreAuthWellKnownEndPoints' + ).and.returnValue(of({})); + + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(true); + const refreshSessionWithRefreshTokensSpy = spyOn( + refreshSessionRefreshTokenService, + 'refreshSessionWithRefreshTokens' + ).and.returnValue(of({} as CallbackContext)); + + (refreshSessionService as any) + .startRefreshSession(allConfigs[0], allConfigs) + .subscribe(() => { + expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled(); + }); + })); + + it('calls refreshSessionWithIframe when current flow is NOT codeflow with refresh tokens', waitForAsync(() => { + spyOn(flowsDataService, 'setSilentRenewRunning'); + const allConfigs = [ + { + configId: 'configId1', + authWellknownEndpointUrl: 'https://authWell', + }, + ]; + + spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); + spyOn( + authWellKnownService, + 'queryAndStoreAuthWellKnownEndPoints' + ).and.returnValue(of({})); + + spyOn( + flowHelper, + 'isCurrentFlowCodeFlowWithRefreshTokens' + ).and.returnValue(false); + const refreshSessionWithRefreshTokensSpy = spyOn( + refreshSessionRefreshTokenService, + 'refreshSessionWithRefreshTokens' + ).and.returnValue(of({} as CallbackContext)); + const refreshSessionWithIframeSpy = spyOn( + refreshSessionIframeService, + 'refreshSessionWithIframe' + ).and.returnValue(of(false)); + + (refreshSessionService as any) + .startRefreshSession(allConfigs[0], allConfigs) + .subscribe(() => { + expect(refreshSessionWithRefreshTokensSpy).not.toHaveBeenCalled(); + expect(refreshSessionWithIframeSpy).toHaveBeenCalled(); + }); + })); + }); +}); diff --git a/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.ts b/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.ts index d7fbf800b..8f098bb25 100644 --- a/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/callback/refresh-session.service.ts @@ -8,6 +8,7 @@ import { timer, } from 'rxjs'; import { + filter, map, mergeMap, retryWhen, @@ -116,7 +117,10 @@ export class RefreshSessionService { return forkJoin([ this.startRefreshSession(config, allConfigs, extraCustomParams), - this.silentRenewService.refreshSessionWithIFrameCompleted$.pipe(take(1)), + this.silentRenewService.refreshSessionWithIFrameCompleted$.pipe( + filter((result) => result?.configId === config.configId), + take(1) + ), ]).pipe( timeout(timeOutTime), retryWhen((errors) => { @@ -143,14 +147,16 @@ export class RefreshSessionService { }) ); }), - map(([_, callbackContext]) => { + map(([_, refreshCompleted]) => { const isAuthenticated = this.authStateService.areAuthStorageTokensValid(config); if (isAuthenticated) { + const authResult = refreshCompleted.success ? refreshCompleted.authResult : null + return { - idToken: callbackContext?.authResult?.id_token ?? '', - accessToken: callbackContext?.authResult?.access_token ?? '', + idToken: authResult?.id_token ?? '', + accessToken: authResult?.access_token ?? '', userData: this.userService.getUserDataFromStore(config), isAuthenticated, configId, diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/history-jwt-keys-callback-handler.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/history-jwt-keys-callback-handler.service.spec.ts index 8b68f5bd7..d5e33b645 100644 --- a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/history-jwt-keys-callback-handler.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/history-jwt-keys-callback-handler.service.spec.ts @@ -362,6 +362,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { isAuthenticated: false, validationResult: ValidationResult.SecureTokenServerError, isRenewProcess: false, + configId: 'configId1', }); }, }); @@ -401,6 +402,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { isAuthenticated: false, validationResult: ValidationResult.LoginRequired, isRenewProcess: false, + configId: 'configId1', }); }, }); diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/history-jwt-keys-callback-handler.service.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/history-jwt-keys-callback-handler.service.ts index 3334f9f18..daf946a55 100644 --- a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/history-jwt-keys-callback-handler.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/history-jwt-keys-callback-handler.service.ts @@ -74,7 +74,8 @@ export class HistoryJwtKeysCallbackHandlerService { this.flowsDataService.setNonce('', config); this.handleResultErrorFromCallback( callbackContext.authResult, - callbackContext.isRenewProcess + callbackContext.isRenewProcess, + config.configId ); return throwError(() => new Error(errorMessage)); @@ -132,7 +133,8 @@ export class HistoryJwtKeysCallbackHandlerService { private handleResultErrorFromCallback( result: unknown, - isRenewProcess: boolean + isRenewProcess: boolean, + configId?: string ): void { let validationResult = ValidationResult.SecureTokenServerError; @@ -149,6 +151,7 @@ export class HistoryJwtKeysCallbackHandlerService { isAuthenticated: false, validationResult, isRenewProcess, + configId, }); } diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/state-validation-callback-handler.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/state-validation-callback-handler.service.spec.ts index eb09a14ce..e715332dd 100644 --- a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/state-validation-callback-handler.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/state-validation-callback-handler.service.spec.ts @@ -138,6 +138,7 @@ describe('StateValidationCallbackHandlerService', () => { isAuthenticated: false, validationResult: ValidationResult.LoginRequired, isRenewProcess: true, + configId: 'configId1', }); }, }); diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/state-validation-callback-handler.service.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/state-validation-callback-handler.service.ts index 83f5b2387..e1fb2cb60 100644 --- a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/state-validation-callback-handler.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/state-validation-callback-handler.service.ts @@ -49,7 +49,8 @@ export class StateValidationCallbackHandlerService { ); this.publishUnauthorizedState( callbackContext.validationResult, - callbackContext.isRenewProcess + callbackContext.isRenewProcess, + configuration.configId ); throw new Error(errorMessage); @@ -60,12 +61,14 @@ export class StateValidationCallbackHandlerService { private publishUnauthorizedState( stateValidationResult: StateValidationResult, - isRenewProcess: boolean + isRenewProcess: boolean, + configId?: string ): void { this.authStateService.updateAndPublishAuthState({ isAuthenticated: false, validationResult: stateValidationResult.state, isRenewProcess, + configId, }); } } diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/user-callback-handler.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/user-callback-handler.service.spec.ts index 21e41870c..3c482830b 100644 --- a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/user-callback-handler.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/user-callback-handler.service.spec.ts @@ -206,6 +206,7 @@ describe('UserCallbackHandlerService', () => { isAuthenticated: true, validationResult: ValidationResult.NotSet, isRenewProcess: false, + configId: 'configId1', }); expect(resultCallbackContext).toEqual(callbackContext); }); @@ -292,6 +293,7 @@ describe('UserCallbackHandlerService', () => { isAuthenticated: true, validationResult: ValidationResult.MaxOffsetExpired, isRenewProcess: false, + configId: 'configId1', }); expect(resultCallbackContext).toEqual(callbackContext); }); @@ -379,6 +381,7 @@ describe('UserCallbackHandlerService', () => { isAuthenticated: false, validationResult: ValidationResult.MaxOffsetExpired, isRenewProcess: false, + configId: 'configId1', }); expect(err.message).toEqual( 'Failed to retrieve user info with error: Error: Called for userData but they were null' diff --git a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/user-callback-handler.service.ts b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/user-callback-handler.service.ts index e4f027ad2..42069d8ff 100644 --- a/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/user-callback-handler.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/user-callback-handler.service.ts @@ -47,7 +47,7 @@ export class UserCallbackHandlerService { ); } - this.publishAuthState(validationResult, isRenewProcess); + this.publishAuthState(validationResult, isRenewProcess, configuration.configId); return of(callbackContext); } @@ -70,7 +70,7 @@ export class UserCallbackHandlerService { ); } - this.publishAuthState(validationResult, isRenewProcess); + this.publishAuthState(validationResult, isRenewProcess, configuration.configId); return of(callbackContext); } else { @@ -78,7 +78,7 @@ export class UserCallbackHandlerService { configuration, allConfigs ); - this.publishUnauthenticatedState(validationResult, isRenewProcess); + this.publishUnauthenticatedState(validationResult, isRenewProcess, configuration.configId); const errorMessage = `Called for userData but they were ${userData}`; this.loggerService.logWarning(configuration, errorMessage); @@ -98,7 +98,8 @@ export class UserCallbackHandlerService { private publishAuthState( stateValidationResult: StateValidationResult | null, - isRenewProcess: boolean + isRenewProcess: boolean, + configId?: string ): void { if (!stateValidationResult) { return; @@ -108,12 +109,14 @@ export class UserCallbackHandlerService { isAuthenticated: true, validationResult: stateValidationResult.state, isRenewProcess, + configId, }); } private publishUnauthenticatedState( stateValidationResult: StateValidationResult | null, - isRenewProcess: boolean + isRenewProcess: boolean, + configId?: string ): void { if (!stateValidationResult) { return; @@ -123,6 +126,7 @@ export class UserCallbackHandlerService { isAuthenticated: false, validationResult: stateValidationResult.state, isRenewProcess, + configId, }); } } diff --git a/projects/angular-auth-oidc-client/src/lib/iframe/existing-iframe.service.ts b/projects/angular-auth-oidc-client/src/lib/iframe/existing-iframe.service.ts index 6aa092e99..2ed308a99 100644 --- a/projects/angular-auth-oidc-client/src/lib/iframe/existing-iframe.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/iframe/existing-iframe.service.ts @@ -51,7 +51,7 @@ export class IFrameService { } return null; - } catch (e) { + } catch (_e) { return null; } } diff --git a/projects/angular-auth-oidc-client/src/lib/iframe/refresh-session-iframe.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/iframe/refresh-session-iframe.service.spec.ts index 1dd2fcafb..890a3615d 100644 --- a/projects/angular-auth-oidc-client/src/lib/iframe/refresh-session-iframe.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/iframe/refresh-session-iframe.service.spec.ts @@ -4,7 +4,7 @@ import { mockProvider } from '../../test/auto-mock'; import { LoggerService } from '../logging/logger.service'; import { UrlService } from '../utils/url/url.service'; import { RefreshSessionIframeService } from './refresh-session-iframe.service'; -import { SilentRenewService } from './silent-renew.service'; +import { SilentRenewService, getFrameId } from './silent-renew.service'; describe('RefreshSessionIframeService ', () => { let refreshSessionIframeService: RefreshSessionIframeService; @@ -54,14 +54,91 @@ describe('RefreshSessionIframeService ', () => { describe('initSilentRenewRequest', () => { it('dispatches customevent to window object', waitForAsync(() => { const dispatchEventSpy = spyOn(window, 'dispatchEvent'); + const config = { configId: 'testConfigId' }; + const allConfigs = [config]; - (refreshSessionIframeService as any).initSilentRenewRequest(); + (refreshSessionIframeService as any).initSilentRenewRequest(config, allConfigs); expect(dispatchEventSpy).toHaveBeenCalledOnceWith( - new CustomEvent('oidc-silent-renew-init', { - detail: jasmine.any(Number), + jasmine.objectContaining({ + type: 'oidc-silent-renew-init', + detail: jasmine.objectContaining({ + instanceId: jasmine.any(Number), + configId: 'testConfigId' + }), }) ); })); }); + + describe('shouldProcessRenewMessage', () => { + it('returns true when srcFrameId contains matching configId', () => { + const config = { configId: 'testConfigId' }; + const event = new CustomEvent('oidc-silent-renew-message', { + detail: { url: 'http://example.com', srcFrameId: getFrameId('testConfigId') } + }); + const result = (refreshSessionIframeService as any).shouldProcessRenewMessage(event, config); + + expect(result).toBe(true); + }); + + it('returns false when srcFrameId contains different configId', () => { + const config = { configId: 'testConfigId' }; + const event = new CustomEvent('oidc-silent-renew-message', { + detail: { url: 'http://example.com', srcFrameId: getFrameId('differentConfigId') } + }); + const result = (refreshSessionIframeService as any).shouldProcessRenewMessage(event, config); + + expect(result).toBe(false); + }); + + it('returns false when srcFrameId does not start with expected prefix', () => { + const config = { configId: 'testConfigId' }; + const event = new CustomEvent('oidc-silent-renew-message', { + detail: { url: 'http://example.com', srcFrameId: 'someOtherFrame_testConfigId' } + }); + const result = (refreshSessionIframeService as any).shouldProcessRenewMessage(event, config); + + expect(result).toBe(false); + }); + + it('returns true for backward compatibility when event has no srcFrameId', () => { + const config = { configId: 'testConfigId' }; + const event = new CustomEvent('oidc-silent-renew-message', { + detail: 'http://example.com' + }); + const result = (refreshSessionIframeService as any).shouldProcessRenewMessage(event, config); + + expect(result).toBe(true); + }); + + it('returns false when event has no detail', () => { + const config = { configId: 'testConfigId' }; + const event = new CustomEvent('oidc-silent-renew-message'); + const result = (refreshSessionIframeService as any).shouldProcessRenewMessage(event, config); + + expect(result).toBe(false); + }); + }); + + describe('convertToLegacyEvent', () => { + it('converts new format event to legacy format', () => { + const newFormatEvent = new CustomEvent('oidc-silent-renew-message', { + detail: { url: 'http://example.com?code=123', srcFrameId: getFrameId('testConfigId') } + }); + const result = (refreshSessionIframeService as any).convertToLegacyEvent(newFormatEvent); + + expect(result.type).toBe('oidc-silent-renew-message'); + expect(result.detail).toBe('http://example.com?code=123'); + }); + + it('returns event as-is if already in legacy format', () => { + const legacyEvent = new CustomEvent('oidc-silent-renew-message', { + detail: 'http://example.com?code=123' + }); + const result = (refreshSessionIframeService as any).convertToLegacyEvent(legacyEvent); + + expect(result).toBe(legacyEvent); + }); + }); }); diff --git a/projects/angular-auth-oidc-client/src/lib/iframe/refresh-session-iframe.service.ts b/projects/angular-auth-oidc-client/src/lib/iframe/refresh-session-iframe.service.ts index e2b3756dd..556f8f3ab 100644 --- a/projects/angular-auth-oidc-client/src/lib/iframe/refresh-session-iframe.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/iframe/refresh-session-iframe.service.ts @@ -5,7 +5,7 @@ import { switchMap } from 'rxjs/operators'; import { OpenIdConfiguration } from '../config/openid-configuration'; import { LoggerService } from '../logging/logger.service'; import { UrlService } from '../utils/url/url.service'; -import { SilentRenewService } from './silent-renew.service'; +import { SilentRenewService, getFrameId } from './silent-renew.service'; @Injectable({ providedIn: 'root' }) export class RefreshSessionIframeService { @@ -75,11 +75,23 @@ export class RefreshSessionIframeService { allConfigs: OpenIdConfiguration[] ): void { const instanceId = Math.random(); + + this.loggerService.logDebug( + config, + `Creating new silent renew handlers for config: ${config.configId}, instance: ${instanceId}` + ); + const initDestroyHandler = this.renderer.listen( 'window', 'oidc-silent-renew-init', (e: CustomEvent) => { - if (e.detail !== instanceId) { + const eventData = e.detail; + + if (eventData.configId === config.configId && eventData.instanceId !== instanceId) { + this.loggerService.logDebug( + config, + `Destroying old handlers for config: ${config.configId} (old instance: ${instanceId}, new instance: ${eventData.instanceId})` + ); initDestroyHandler(); renewDestroyHandler(); } @@ -88,14 +100,69 @@ export class RefreshSessionIframeService { const renewDestroyHandler = this.renderer.listen( 'window', 'oidc-silent-renew-message', - (e) => - this.silentRenewService.silentRenewEventHandler(e, config, allConfigs) + (e: CustomEvent) => { + + if (this.shouldProcessRenewMessage(e, config)) { + const eventToPass = this.convertToLegacyEvent(e); + + this.silentRenewService.silentRenewEventHandler(eventToPass, config, allConfigs); + } + } ); + this.document.defaultView?.dispatchEvent( new CustomEvent('oidc-silent-renew-init', { - detail: instanceId, + detail: { + instanceId, + configId: config.configId + } }) ); } + + private shouldProcessRenewMessage( + e: CustomEvent, + config: OpenIdConfiguration + ): boolean { + + if (!e?.detail) { + this.loggerService.logDebug( + config, + `Silent renew event has no valid payload: ${e?.detail}` + ); + + return false; + } + + if (e.detail.srcFrameId) { + const shouldProcess = getFrameId(config.configId) === e.detail.srcFrameId; + + this.loggerService.logDebug( + config, + `Silent renew event from frame: ${e.detail.srcFrameId}, current configId: ${config.configId}, processing: ${shouldProcess}` + ); + + return shouldProcess; + } + + // Fallback for backward compatibility - if no srcFrameId but has detail (legacy format) + this.loggerService.logDebug( + config, + 'Silent renew event without srcFrameId - processing for backward compatibility' + ); + + return true; + } + + private convertToLegacyEvent(e: CustomEvent): CustomEvent { + // If event has the new format with url property, convert it to legacy format + if (e?.detail?.url) { + return new CustomEvent(e.type, { detail: e.detail.url }); + } + + // Otherwise, return as-is (already in legacy format) + return e; + } + } diff --git a/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.spec.ts b/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.spec.ts index fdb04c22c..295c64cde 100644 --- a/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.spec.ts @@ -1,365 +1,374 @@ -import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { Observable, of, throwError } from 'rxjs'; -import { mockProvider } from '../../test/auto-mock'; -import { AuthStateService } from '../auth-state/auth-state.service'; -import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.service'; -import { IntervalService } from '../callback/interval.service'; -import { CallbackContext } from '../flows/callback-context'; -import { FlowsDataService } from '../flows/flows-data.service'; -import { FlowsService } from '../flows/flows.service'; -import { ResetAuthDataService } from '../flows/reset-auth-data.service'; -import { LoggerService } from '../logging/logger.service'; -import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; -import { ValidationResult } from '../validation/validation-result'; -import { IFrameService } from './existing-iframe.service'; -import { SilentRenewService } from './silent-renew.service'; - -describe('SilentRenewService ', () => { - let silentRenewService: SilentRenewService; - let flowHelper: FlowHelper; - let implicitFlowCallbackService: ImplicitFlowCallbackService; - let iFrameService: IFrameService; - let flowsDataService: FlowsDataService; - let loggerService: LoggerService; - let flowsService: FlowsService; - let authStateService: AuthStateService; - let resetAuthDataService: ResetAuthDataService; - let intervalService: IntervalService; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - SilentRenewService, - IFrameService, - mockProvider(FlowsService), - mockProvider(ResetAuthDataService), - mockProvider(FlowsDataService), - mockProvider(AuthStateService), - mockProvider(LoggerService), - mockProvider(ImplicitFlowCallbackService), - mockProvider(IntervalService), - FlowHelper, - ], - }); - }); - - beforeEach(() => { - silentRenewService = TestBed.inject(SilentRenewService); - iFrameService = TestBed.inject(IFrameService); - flowHelper = TestBed.inject(FlowHelper); - implicitFlowCallbackService = TestBed.inject(ImplicitFlowCallbackService); - flowsDataService = TestBed.inject(FlowsDataService); - flowsService = TestBed.inject(FlowsService); - loggerService = TestBed.inject(LoggerService); - authStateService = TestBed.inject(AuthStateService); - resetAuthDataService = TestBed.inject(ResetAuthDataService); - intervalService = TestBed.inject(IntervalService); - }); - - it('should create', () => { - expect(silentRenewService).toBeTruthy(); - }); - - describe('refreshSessionWithIFrameCompleted', () => { - it('is of type observable', () => { - expect(silentRenewService.refreshSessionWithIFrameCompleted$).toEqual( - jasmine.any(Observable) - ); - }); - }); - - describe('isSilentRenewConfigured', () => { - it('returns true if refreshToken is configured false and silentRenew is configured true', () => { - const config = { useRefreshToken: false, silentRenew: true }; - const result = silentRenewService.isSilentRenewConfigured(config); - - expect(result).toBe(true); - }); - - it('returns false if refreshToken is configured true and silentRenew is configured true', () => { - const config = { useRefreshToken: true, silentRenew: true }; const result = silentRenewService.isSilentRenewConfigured(config); - - expect(result).toBe(false); - }); - - it('returns false if refreshToken is configured false and silentRenew is configured false', () => { - const config = { useRefreshToken: false, silentRenew: false }; const result = silentRenewService.isSilentRenewConfigured(config); - - expect(result).toBe(false); - }); - }); - - describe('getOrCreateIframe', () => { - it('returns iframe if iframe is truthy', () => { - spyOn(silentRenewService as any, 'getExistingIframe').and.returnValue({ - name: 'anything', - }); - - const result = silentRenewService.getOrCreateIframe({ - configId: 'configId1', - }); - - expect(result).toEqual({ name: 'anything' } as HTMLIFrameElement); - }); - - it('adds iframe to body if existing iframe is falsy', () => { - const config = { configId: 'configId1' }; - - spyOn(silentRenewService as any, 'getExistingIframe').and.returnValue( - null - ); - - const spy = spyOn(iFrameService, 'addIFrameToWindowBody').and.returnValue( - { name: 'anything' } as HTMLIFrameElement - ); const result = silentRenewService.getOrCreateIframe(config); - - expect(result).toEqual({ name: 'anything' } as HTMLIFrameElement); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledOnceWith('myiFrameForSilentRenew', config); - }); - }); - - describe('codeFlowCallbackSilentRenewIframe', () => { - it('calls processSilentRenewCodeFlowCallback with correct arguments', waitForAsync(() => { - const config = { configId: 'configId1' }; - const allConfigs = [config]; const spy = spyOn( - flowsService, - 'processSilentRenewCodeFlowCallback' - ).and.returnValue(of({} as CallbackContext)); - const expectedContext = { - code: 'some-code', - refreshToken: '', - state: 'some-state', - sessionState: 'some-session-state', - authResult: null, - isRenewProcess: true, - jwtKeys: null, - validationResult: null, - existingIdToken: null, - } as CallbackContext; - const url = 'url-part-1'; - const urlParts = - 'code=some-code&state=some-state&session_state=some-session-state'; - - silentRenewService - .codeFlowCallbackSilentRenewIframe([url, urlParts], config, allConfigs) - .subscribe(() => { - expect(spy).toHaveBeenCalledOnceWith( - expectedContext, - config, - allConfigs - ); - }); - })); - - it('throws error if url has error param and resets everything on error', waitForAsync(() => { - const config = { configId: 'configId1' }; - const allConfigs = [config]; const spy = spyOn( - flowsService, - 'processSilentRenewCodeFlowCallback' - ).and.returnValue(of({} as CallbackContext)); - const authStateServiceSpy = spyOn( - authStateService, - 'updateAndPublishAuthState' - ); - const resetAuthorizationDataSpy = spyOn( - resetAuthDataService, - 'resetAuthorizationData' - ); - const setNonceSpy = spyOn(flowsDataService, 'setNonce'); - const stopPeriodicTokenCheckSpy = spyOn( - intervalService, - 'stopPeriodicTokenCheck' - ); const url = 'url-part-1'; - const urlParts = 'error=some_error'; - - silentRenewService - .codeFlowCallbackSilentRenewIframe([url, urlParts], config, allConfigs) - .subscribe({ - error: (error) => { - expect(error).toEqual(new Error('some_error')); - expect(spy).not.toHaveBeenCalled(); - expect(authStateServiceSpy).toHaveBeenCalledOnceWith({ - isAuthenticated: false, - validationResult: ValidationResult.LoginRequired, - isRenewProcess: true, - }); - expect(resetAuthorizationDataSpy).toHaveBeenCalledOnceWith( - config, - allConfigs - ); - expect(setNonceSpy).toHaveBeenCalledOnceWith('', config); - expect(stopPeriodicTokenCheckSpy).toHaveBeenCalledTimes(1); - }, - }); - })); - }); - - describe('silentRenewEventHandler', () => { - it('returns if no details is given', fakeAsync(() => { - const isCurrentFlowCodeFlowSpy = spyOn( - flowHelper, - 'isCurrentFlowCodeFlow' - ).and.returnValue(false); - - spyOn( - implicitFlowCallbackService, - 'authenticatedImplicitFlowCallback' - ).and.returnValue(of({} as CallbackContext)); - const eventData = { detail: null } as CustomEvent; - const allConfigs = [{ configId: 'configId1' }]; - - silentRenewService.silentRenewEventHandler( - eventData, - allConfigs[0], - allConfigs - ); - tick(1000); - expect(isCurrentFlowCodeFlowSpy).not.toHaveBeenCalled(); - })); - - it('calls authorizedImplicitFlowCallback if current flow is not code flow', fakeAsync(() => { - const isCurrentFlowCodeFlowSpy = spyOn( - flowHelper, - 'isCurrentFlowCodeFlow' - ).and.returnValue(false); - const authorizedImplicitFlowCallbackSpy = spyOn( - implicitFlowCallbackService, - 'authenticatedImplicitFlowCallback' - ).and.returnValue(of({} as CallbackContext)); - const eventData = { detail: 'detail' } as CustomEvent; - const allConfigs = [{ configId: 'configId1' }]; - - silentRenewService.silentRenewEventHandler( - eventData, - allConfigs[0], - allConfigs - ); - tick(1000); - expect(isCurrentFlowCodeFlowSpy).toHaveBeenCalled(); - expect(authorizedImplicitFlowCallbackSpy).toHaveBeenCalledOnceWith( - allConfigs[0], - allConfigs, - 'detail' - ); - })); - - it('calls codeFlowCallbackSilentRenewIframe if current flow is code flow', fakeAsync(() => { - spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); - const codeFlowCallbackSilentRenewIframe = spyOn( - silentRenewService, - 'codeFlowCallbackSilentRenewIframe' - ).and.returnValue(of({} as CallbackContext)); - const eventData = { detail: 'detail?detail2' } as CustomEvent; - const allConfigs = [{ configId: 'configId1' }]; - - silentRenewService.silentRenewEventHandler( - eventData, - allConfigs[0], - allConfigs - ); - tick(1000); - expect(codeFlowCallbackSilentRenewIframe).toHaveBeenCalledOnceWith( - ['detail', 'detail2'], - allConfigs[0], - allConfigs - ); - })); - - it('calls authorizedImplicitFlowCallback if current flow is not code flow', fakeAsync(() => { - spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); - const codeFlowCallbackSilentRenewIframe = spyOn( - silentRenewService, - 'codeFlowCallbackSilentRenewIframe' - ).and.returnValue(of({} as CallbackContext)); - const eventData = { detail: 'detail?detail2' } as CustomEvent; - const allConfigs = [{ configId: 'configId1' }]; - - silentRenewService.silentRenewEventHandler( - eventData, - allConfigs[0], - allConfigs - ); - tick(1000); - expect(codeFlowCallbackSilentRenewIframe).toHaveBeenCalledOnceWith( - ['detail', 'detail2'], - allConfigs[0], - allConfigs - ); - })); - - it('calls next on refreshSessionWithIFrameCompleted with callbackcontext', fakeAsync(() => { - spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); - spyOn( - silentRenewService, - 'codeFlowCallbackSilentRenewIframe' - ).and.returnValue( - of({ refreshToken: 'callbackContext' } as CallbackContext) - ); - const eventData = { detail: 'detail?detail2' } as CustomEvent; - const allConfigs = [{ configId: 'configId1' }]; - - silentRenewService.refreshSessionWithIFrameCompleted$.subscribe( - (result) => { - expect(result).toEqual({ - refreshToken: 'callbackContext', - } as CallbackContext); - } - ); - - silentRenewService.silentRenewEventHandler( - eventData, - allConfigs[0], - allConfigs - ); - tick(1000); - })); - - it('loggs and calls flowsDataService.resetSilentRenewRunning in case of an error', fakeAsync(() => { - spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); - spyOn( - silentRenewService, - 'codeFlowCallbackSilentRenewIframe' - ).and.returnValue(throwError(() => new Error('ERROR'))); - const resetSilentRenewRunningSpy = spyOn( - flowsDataService, - 'resetSilentRenewRunning' - ); - const logErrorSpy = spyOn(loggerService, 'logError'); - const allConfigs = [{ configId: 'configId1' }]; - const eventData = { detail: 'detail?detail2' } as CustomEvent; - - silentRenewService.silentRenewEventHandler( - eventData, - allConfigs[0], - allConfigs - ); - tick(1000); - expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes(1); - expect(logErrorSpy).toHaveBeenCalledTimes(1); - })); - - it('calls next on refreshSessionWithIFrameCompleted with null in case of error', fakeAsync(() => { - spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); - spyOn( - silentRenewService, - 'codeFlowCallbackSilentRenewIframe' - ).and.returnValue(throwError(() => new Error('ERROR'))); - const eventData = { detail: 'detail?detail2' } as CustomEvent; - const allConfigs = [{ configId: 'configId1' }]; - - silentRenewService.refreshSessionWithIFrameCompleted$.subscribe( - (result) => { - expect(result).toBeNull(); - } - ); - - silentRenewService.silentRenewEventHandler( - eventData, - allConfigs[0], - allConfigs - ); - tick(1000); - })); - }); -}); +import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { Observable, of, throwError } from 'rxjs'; +import { mockProvider } from '../../test/auto-mock'; +import { AuthStateService } from '../auth-state/auth-state.service'; +import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.service'; +import { IntervalService } from '../callback/interval.service'; +import { CallbackContext } from '../flows/callback-context'; +import { FlowsDataService } from '../flows/flows-data.service'; +import { FlowsService } from '../flows/flows.service'; +import { ResetAuthDataService } from '../flows/reset-auth-data.service'; +import { LoggerService } from '../logging/logger.service'; +import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; +import { ValidationResult } from '../validation/validation-result'; +import { IFrameService } from './existing-iframe.service'; +import { SilentRenewService } from './silent-renew.service'; + +describe('SilentRenewService ', () => { + let silentRenewService: SilentRenewService; + let flowHelper: FlowHelper; + let implicitFlowCallbackService: ImplicitFlowCallbackService; + let iFrameService: IFrameService; + let flowsDataService: FlowsDataService; + let loggerService: LoggerService; + let flowsService: FlowsService; + let authStateService: AuthStateService; + let resetAuthDataService: ResetAuthDataService; + let intervalService: IntervalService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + SilentRenewService, + IFrameService, + mockProvider(FlowsService), + mockProvider(ResetAuthDataService), + mockProvider(FlowsDataService), + mockProvider(AuthStateService), + mockProvider(LoggerService), + mockProvider(ImplicitFlowCallbackService), + mockProvider(IntervalService), + FlowHelper, + ], + }); + }); + + beforeEach(() => { + silentRenewService = TestBed.inject(SilentRenewService); + iFrameService = TestBed.inject(IFrameService); + flowHelper = TestBed.inject(FlowHelper); + implicitFlowCallbackService = TestBed.inject(ImplicitFlowCallbackService); + flowsDataService = TestBed.inject(FlowsDataService); + flowsService = TestBed.inject(FlowsService); + loggerService = TestBed.inject(LoggerService); + authStateService = TestBed.inject(AuthStateService); + resetAuthDataService = TestBed.inject(ResetAuthDataService); + intervalService = TestBed.inject(IntervalService); + }); + + it('should create', () => { + expect(silentRenewService).toBeTruthy(); + }); + + describe('refreshSessionWithIFrameCompleted', () => { + it('is of type observable', () => { + expect(silentRenewService.refreshSessionWithIFrameCompleted$).toEqual( + jasmine.any(Observable) + ); + }); + }); + + describe('isSilentRenewConfigured', () => { + it('returns true if refreshToken is configured false and silentRenew is configured true', () => { + const config = { useRefreshToken: false, silentRenew: true }; + const result = silentRenewService.isSilentRenewConfigured(config); + + expect(result).toBe(true); + }); + + it('returns false if refreshToken is configured true and silentRenew is configured true', () => { + const config = { useRefreshToken: true, silentRenew: true }; + const result = silentRenewService.isSilentRenewConfigured(config); + + expect(result).toBe(false); + }); + + it('returns false if refreshToken is configured false and silentRenew is configured false', () => { + const config = { useRefreshToken: false, silentRenew: false }; + const result = silentRenewService.isSilentRenewConfigured(config); + + expect(result).toBe(false); + }); + }); + + describe('getOrCreateIframe', () => { + it('returns iframe if iframe is truthy', () => { + const config = { configId: 'configId1' }; + const mockIframe = { name: 'anything' } as HTMLIFrameElement; + + spyOn(iFrameService, 'getExistingIFrame').and.returnValue(mockIframe); + + const result = silentRenewService.getOrCreateIframe(config); + + expect(result).toEqual(mockIframe); + expect(iFrameService.getExistingIFrame).toHaveBeenCalledOnceWith('myiFrameForSilentRenew_configId1'); + }); + + it('adds iframe to body if existing iframe is falsy', () => { + const config = { configId: 'configId1' }; + + spyOn(iFrameService, 'getExistingIFrame').and.returnValue(null); + + const spy = spyOn(iFrameService, 'addIFrameToWindowBody').and.returnValue( + { name: 'anything' } as HTMLIFrameElement + ); + const result = silentRenewService.getOrCreateIframe(config); + + expect(result).toEqual({ name: 'anything' } as HTMLIFrameElement); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledOnceWith('myiFrameForSilentRenew_configId1', config); + }); + }); + + describe('codeFlowCallbackSilentRenewIframe', () => { + it('calls processSilentRenewCodeFlowCallback with correct arguments', waitForAsync(() => { + const config = { configId: 'configId1' }; + const allConfigs = [config]; + const spy = spyOn( + flowsService, + 'processSilentRenewCodeFlowCallback' + ).and.returnValue(of({} as CallbackContext)); + const expectedContext = { + code: 'some-code', + refreshToken: '', + state: 'some-state', + sessionState: 'some-session-state', + authResult: null, + isRenewProcess: true, + jwtKeys: null, + validationResult: null, + existingIdToken: null, + } as CallbackContext; + const url = 'url-part-1'; + const urlParts = + 'code=some-code&state=some-state&session_state=some-session-state'; + + silentRenewService + .codeFlowCallbackSilentRenewIframe([url, urlParts], config, allConfigs) + .subscribe(() => { + expect(spy).toHaveBeenCalledOnceWith( + expectedContext, + config, + allConfigs + ); + }); + })); + + it('throws error if url has error param and resets everything on error', waitForAsync(() => { + const config = { configId: 'configId1' }; + const allConfigs = [config]; + const spy = spyOn( + flowsService, + 'processSilentRenewCodeFlowCallback' + ).and.returnValue(of({} as CallbackContext)); + const authStateServiceSpy = spyOn( + authStateService, + 'updateAndPublishAuthState' + ); + const resetAuthorizationDataSpy = spyOn( + resetAuthDataService, + 'resetAuthorizationData' + ); + const setNonceSpy = spyOn(flowsDataService, 'setNonce'); + const stopPeriodicTokenCheckSpy = spyOn( + intervalService, + 'stopPeriodicTokenCheck' + ); + const url = 'url-part-1'; + const urlParts = 'error=some_error'; + + silentRenewService + .codeFlowCallbackSilentRenewIframe([url, urlParts], config, allConfigs) + .subscribe({ + error: (error) => { + expect(error).toEqual(new Error('some_error')); + expect(spy).not.toHaveBeenCalled(); + expect(authStateServiceSpy).toHaveBeenCalledOnceWith({ + isAuthenticated: false, + validationResult: ValidationResult.LoginRequired, + isRenewProcess: true, + configId: 'configId1', + }); + expect(resetAuthorizationDataSpy).toHaveBeenCalledOnceWith( + config, + allConfigs + ); + expect(setNonceSpy).toHaveBeenCalledOnceWith('', config); + expect(stopPeriodicTokenCheckSpy).toHaveBeenCalledTimes(1); + }, + }); + })); + }); + + describe('silentRenewEventHandler', () => { + it('returns if no details is given', fakeAsync(() => { + const isCurrentFlowCodeFlowSpy = spyOn( + flowHelper, + 'isCurrentFlowCodeFlow' + ).and.returnValue(false); + + spyOn( + implicitFlowCallbackService, + 'authenticatedImplicitFlowCallback' + ).and.returnValue(of({} as CallbackContext)); + const eventData = { detail: null } as CustomEvent; + const allConfigs = [{ configId: 'configId1' }]; + + silentRenewService.silentRenewEventHandler( + eventData, + allConfigs[0], + allConfigs + ); + tick(1000); + expect(isCurrentFlowCodeFlowSpy).not.toHaveBeenCalled(); + })); + + it('calls authorizedImplicitFlowCallback if current flow is not code flow', fakeAsync(() => { + const isCurrentFlowCodeFlowSpy = spyOn( + flowHelper, + 'isCurrentFlowCodeFlow' + ).and.returnValue(false); + const authorizedImplicitFlowCallbackSpy = spyOn( + implicitFlowCallbackService, + 'authenticatedImplicitFlowCallback' + ).and.returnValue(of({} as CallbackContext)); + const eventData = { detail: 'detail' } as CustomEvent; + const allConfigs = [{ configId: 'configId1' }]; + + silentRenewService.silentRenewEventHandler( + eventData, + allConfigs[0], + allConfigs + ); + tick(1000); + expect(isCurrentFlowCodeFlowSpy).toHaveBeenCalled(); + expect(authorizedImplicitFlowCallbackSpy).toHaveBeenCalledOnceWith( + allConfigs[0], + allConfigs, + 'detail' + ); + })); + + it('calls codeFlowCallbackSilentRenewIframe if current flow is code flow', fakeAsync(() => { + spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); + const codeFlowCallbackSilentRenewIframe = spyOn( + silentRenewService, + 'codeFlowCallbackSilentRenewIframe' + ).and.returnValue(of({} as CallbackContext)); + const eventData = { detail: 'detail?detail2' } as CustomEvent; + const allConfigs = [{ configId: 'configId1' }]; + + silentRenewService.silentRenewEventHandler( + eventData, + allConfigs[0], + allConfigs + ); + tick(1000); + expect(codeFlowCallbackSilentRenewIframe).toHaveBeenCalledOnceWith( + ['detail', 'detail2'], + allConfigs[0], + allConfigs + ); + })); + + it('calls authorizedImplicitFlowCallback if current flow is not code flow', fakeAsync(() => { + spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); + const codeFlowCallbackSilentRenewIframe = spyOn( + silentRenewService, + 'codeFlowCallbackSilentRenewIframe' + ).and.returnValue(of({} as CallbackContext)); + const eventData = { detail: 'detail?detail2' } as CustomEvent; + const allConfigs = [{ configId: 'configId1' }]; + + silentRenewService.silentRenewEventHandler( + eventData, + allConfigs[0], + allConfigs + ); + tick(1000); + expect(codeFlowCallbackSilentRenewIframe).toHaveBeenCalledOnceWith( + ['detail', 'detail2'], + allConfigs[0], + allConfigs + ); + })); + + it('calls next on refreshSessionWithIFrameCompleted with callbackcontext', fakeAsync(() => { + spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); + spyOn( + silentRenewService, + 'codeFlowCallbackSilentRenewIframe' + ).and.returnValue( + of({ + authResult: { id_token: 'test-token', access_token: 'test-access' } + } as CallbackContext) + ); + const eventData = { detail: 'detail?detail2' } as CustomEvent; + const allConfigs = [{ configId: 'configId1' }]; + + silentRenewService.refreshSessionWithIFrameCompleted$.subscribe( + (result) => { + expect(result).toEqual({ + success: true, + authResult: { id_token: 'test-token', access_token: 'test-access' }, + configId: 'configId1', + }); + } + ); + + silentRenewService.silentRenewEventHandler( + eventData, + allConfigs[0], + allConfigs + ); + tick(1000); + })); + + it('loggs and calls flowsDataService.resetSilentRenewRunning in case of an error', fakeAsync(() => { + spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); + spyOn( + silentRenewService, + 'codeFlowCallbackSilentRenewIframe' + ).and.returnValue(throwError(() => new Error('ERROR'))); + const resetSilentRenewRunningSpy = spyOn( + flowsDataService, + 'resetSilentRenewRunning' + ); + const logErrorSpy = spyOn(loggerService, 'logError'); + const allConfigs = [{ configId: 'configId1' }]; + const eventData = { detail: 'detail?detail2' } as CustomEvent; + + silentRenewService.silentRenewEventHandler( + eventData, + allConfigs[0], + allConfigs + ); + tick(1000); + expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes(1); + expect(logErrorSpy).toHaveBeenCalledTimes(1); + })); + + it('calls next on refreshSessionWithIFrameCompleted with null in case of error', fakeAsync(() => { + spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); + spyOn( + silentRenewService, + 'codeFlowCallbackSilentRenewIframe' + ).and.returnValue(throwError(() => new Error('ERROR'))); + const eventData = { detail: 'detail?detail2' } as CustomEvent; + const allConfigs = [{ configId: 'configId1' }]; + + silentRenewService.refreshSessionWithIFrameCompleted$.subscribe( + (result) => { + expect(result).toEqual({ success: false, configId: 'configId1' }); + } + ); + + silentRenewService.silentRenewEventHandler( + eventData, + allConfigs[0], + allConfigs + ); + tick(1000); + })); + }); +}); diff --git a/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.ts b/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.ts index 725ea8483..c080e5657 100644 --- a/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.ts @@ -6,7 +6,7 @@ import { AuthStateService } from '../auth-state/auth-state.service'; import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.service'; import { IntervalService } from '../callback/interval.service'; import { OpenIdConfiguration } from '../config/openid-configuration'; -import { CallbackContext } from '../flows/callback-context'; +import { AuthResult, CallbackContext } from '../flows/callback-context'; import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsService } from '../flows/flows.service'; import { ResetAuthDataService } from '../flows/reset-auth-data.service'; @@ -17,12 +17,15 @@ import { IFrameService } from './existing-iframe.service'; const IFRAME_FOR_SILENT_RENEW_IDENTIFIER = 'myiFrameForSilentRenew'; +export const getFrameId = (configId?: string): string => `${IFRAME_FOR_SILENT_RENEW_IDENTIFIER}_${configId}`; +type RefreshSessionWithIFrameCompleted = + {success: true, authResult: AuthResult | null, configId?: string } | {success: false, configId?: string}; @Injectable({ providedIn: 'root' }) export class SilentRenewService { private readonly refreshSessionWithIFrameCompletedInternal$ = - new Subject(); + new Subject(); - get refreshSessionWithIFrameCompleted$(): Observable { + get refreshSessionWithIFrameCompleted$(): Observable { return this.refreshSessionWithIFrameCompletedInternal$.asObservable(); } @@ -39,15 +42,21 @@ export class SilentRenewService { private readonly intervalService = inject(IntervalService); getOrCreateIframe(config: OpenIdConfiguration): HTMLIFrameElement { - const existingIframe = this.getExistingIframe(); + // Create unique iframe identifier for each configuration + const iframeId = getFrameId(config.configId); + const existingIframe = this.iFrameService.getExistingIFrame(iframeId); if (!existingIframe) { + this.loggerService.logDebug(config, `Creating new iframe: ${iframeId}`); + return this.iFrameService.addIFrameToWindowBody( - IFRAME_FOR_SILENT_RENEW_IDENTIFIER, + iframeId, config ); } + this.loggerService.logDebug(config, `Using existing iframe: ${iframeId}`); + return existingIframe; } @@ -72,6 +81,7 @@ export class SilentRenewService { isAuthenticated: false, validationResult: ValidationResult.LoginRequired, isRenewProcess: true, + configId: config.configId, }); this.resetAuthDataService.resetAuthorizationData(config, allConfigs); this.flowsDataService.setNonce('', config); @@ -138,21 +148,15 @@ export class SilentRenewService { } callback$.subscribe({ - next: (callbackContext) => { - this.refreshSessionWithIFrameCompletedInternal$.next(callbackContext); + next: ({authResult}) => { + this.refreshSessionWithIFrameCompletedInternal$.next({authResult, configId: config.configId, success: true}); this.flowsDataService.resetSilentRenewRunning(config); }, error: (err: unknown) => { this.loggerService.logError(config, 'Error: ' + err); - this.refreshSessionWithIFrameCompletedInternal$.next(null); + this.refreshSessionWithIFrameCompletedInternal$.next({configId: config.configId, success: false}); this.flowsDataService.resetSilentRenewRunning(config); }, }); } - - private getExistingIframe(): HTMLIFrameElement | null { - return this.iFrameService.getExistingIFrame( - IFRAME_FOR_SILENT_RENEW_IDENTIFIER - ); - } } diff --git a/projects/angular-auth-oidc-client/src/lib/utils/tokenHelper/token-helper.service.ts b/projects/angular-auth-oidc-client/src/lib/utils/tokenHelper/token-helper.service.ts index 2eb4e04f5..9b6e9bd3b 100644 --- a/projects/angular-auth-oidc-client/src/lib/utils/tokenHelper/token-helper.service.ts +++ b/projects/angular-auth-oidc-client/src/lib/utils/tokenHelper/token-helper.service.ts @@ -132,7 +132,7 @@ export class TokenHelperService { ) .join('') ); - } catch (err) { + } catch (_err) { return decoded; } } diff --git a/projects/angular-auth-oidc-client/src/test/auto-mock.ts b/projects/angular-auth-oidc-client/src/test/auto-mock.ts index db2ffe13b..ea44d1417 100644 --- a/projects/angular-auth-oidc-client/src/test/auto-mock.ts +++ b/projects/angular-auth-oidc-client/src/test/auto-mock.ts @@ -5,7 +5,7 @@ export function mockClass(obj: new (...args: any[]) => T): any { const allMethods = keys.filter((key) => { try { return typeof obj.prototype[key] === 'function'; - } catch (error) { + } catch (_error) { return false; } }); diff --git a/projects/integration-tests/karma.conf.js b/projects/integration-tests/karma.conf.js new file mode 100644 index 000000000..0b40eda6b --- /dev/null +++ b/projects/integration-tests/karma.conf.js @@ -0,0 +1,56 @@ +// Karma configuration file +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/integration-tests'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + files: [ + // Include test files + { pattern: 'src/tests/**/*.spec.ts', included: true }, + // Serve assets (like silent-renew.html) but don't include them + { pattern: 'src/assets/**/*', included: false, served: true, watched: false } + ], + proxies: { + // Make assets available at the expected path + '/assets/': '/base/src/assets/', + // Handle silent-renew.html with query parameters + '/base/src/assets/': '/base/src/assets/' + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome', 'ChromeHeadless'], + restartOnFileChange: true, + singleRun: false, + + // Increase timeouts for integration tests + browserNoActivityTimeout: 60000, + browserDisconnectTimeout: 20000, + browserDisconnectTolerance: 3, + captureTimeout: 60000 + }); +}; \ No newline at end of file diff --git a/projects/integration-tests/src/assets/silent-renew.html b/projects/integration-tests/src/assets/silent-renew.html new file mode 100644 index 000000000..f2ed93597 --- /dev/null +++ b/projects/integration-tests/src/assets/silent-renew.html @@ -0,0 +1,27 @@ + + + + + + + silent-renew + + + + + + \ No newline at end of file diff --git a/projects/integration-tests/src/tests/token-refresh.spec.ts b/projects/integration-tests/src/tests/token-refresh.spec.ts new file mode 100644 index 000000000..07c71adf3 --- /dev/null +++ b/projects/integration-tests/src/tests/token-refresh.spec.ts @@ -0,0 +1,281 @@ +import {TestBed} from '@angular/core/testing'; +import {provideHttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {firstValueFrom, of} from 'rxjs'; +import {take, filter, toArray} from 'rxjs/operators'; +import {AuthModule, OidcSecurityService, StsConfigLoader, EventTypes, PublicEventsService} from 'angular-auth-oidc-client'; +// Note: Test IDP server should be running on port 8081 before running this test + +/** + * E2E Test: Force Refresh Session with Automatic Token Renewal + * + * Test Flow: + * 1. Initial auth check (should be false) + * 2. Force refresh session to authenticate via silent renew + * 3. Wait for automatic token refresh before expiration + * 4. Verify tokens are refreshed correctly + * + * This test validates: + * - Force refresh session works correctly + * - Automatic token renewal works correctly before expiration + * - Token refresh maintains authentication state + * - All configured IDPs are refreshed + * + * Note: This test uses 5-minute token expiration (300 seconds) with + * renewal 290 seconds before expiration (10 seconds after issue). + * This means tokens are only kept for 10 seconds before being refreshed. + */ +const idp_host = "http://localhost:8081" +const renewSecBeforeExp = 290 +const configIdIdp1 = "idp1" +const configIdIdp2 = "idp2" +const configIdIdp3 = "idp3" + +@Injectable() +class TestStsConfigLoaderWithAutoRefresh extends StsConfigLoader { + private readonly silentRenewUrl = `${window.location.origin}/assets/silent-renew.html`; + constructor() { + super(); + console.log('Constructed silent renew URL:', this.silentRenewUrl); + } + override loadConfigs() { + + const baseConfig = { + redirectUrl: `${idp_host}/callback`, + silentRenewUrl: this.silentRenewUrl, + postLogoutRedirectUri: `${idp_host}/`, + responseType: 'code', + // Enable automatic silent renewal + silentRenew: true, + // Renew 290 seconds before token expires (for 300 second/5 minute tokens) + // This means tokens are only kept for 10 seconds before refresh + renewTimeBeforeTokenExpiresInSeconds: renewSecBeforeExp, + renewUserInfoAfterTokenRenew: true, + // Enable periodic token checks every 2 seconds for faster detection + tokenRefreshInSeconds: 2, + // Enable automatic refresh when ID token is about to expire + triggerRefreshWhenIdTokenExpired: true, + useRefreshToken: false + }; + + return of([ + { + ...baseConfig, + configId: configIdIdp1, + authority: `${idp_host}/idp1`, + clientId: 'client-idp1', + scope: 'openid profile email' + }, + { + ...baseConfig, + configId: configIdIdp2, + authority: `${idp_host}/idp2`, + clientId: 'client-idp2', + scope: 'openid profile' + }, + { + ...baseConfig, + configId: configIdIdp3, + authority: `${idp_host}/idp3`, + clientId: 'client-idp3', + scope: 'openid email' + } + ]); + } +} + + +describe('Force Refresh Session with Automatic Token Renewal', () => { + let oidcSecurityService: OidcSecurityService; + let publicEventsService: PublicEventsService; + + beforeAll(async () => { + // Check that IDP server is running + console.log(`Checking IDP server health at ${idp_host}/health...`); + + try { + const healthResponse = await fetch(`${idp_host}/health`); + + expect(healthResponse.ok).withContext( + `IDP server health check failed. Status: ${healthResponse.status}. ` + + `Make sure the test IDP server is running at ${idp_host}. ` + + `Start it with: cd projects/integration-tests/test-idp-server && ./start.sh` + ).toBe(true); + + const healthData = await healthResponse.json(); + expect(healthData.status).withContext( + `IDP server returned unhealthy status: ${JSON.stringify(healthData)}` + ).toBe('ok'); + + console.log('✅ IDP server is healthy:', healthData); + } catch (error) { + fail( + `Failed to connect to IDP server at ${idp_host}. ` + + `Error: ${error instanceof Error ? error.message : error}. ` + + `\n\nMake sure the test IDP server is running:\n` + + ` cd projects/integration-tests/test-idp-server && ./start.sh\n\n` + + `The server should be accessible at ${idp_host}` + ); + } + }); + + beforeEach(() => { + localStorage.clear(); + TestBed.configureTestingModule({ + imports: [ + AuthModule.forRoot({ + loader: { + provide: StsConfigLoader, + useClass: TestStsConfigLoaderWithAutoRefresh + } + }) + ], + providers: [ + provideHttpClient() + ] + }); + + oidcSecurityService = TestBed.inject(OidcSecurityService); + publicEventsService = TestBed.inject(PublicEventsService); + }); + + afterEach(() => { + oidcSecurityService.logoffLocalMultiple(); + }); + + it('should force refresh session and automatically renew tokens before expiration', async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; + + console.log('=== TEST FLOW: Automatic Token Refresh ==='); + + console.log('STEP 1: Initial authentication check'); + + const beforeInitRefreshAuthState = await firstValueFrom(oidcSecurityService.checkAuthMultiple()); + + console.log('Initial auth check results:', beforeInitRefreshAuthState.map(r => ({ + configId: r.configId, + isAuthenticated: r.isAuthenticated + }))); + + expect(beforeInitRefreshAuthState.every(r => !r.isAuthenticated)).toBe(true); + + console.log('Force refresh session for all configs'); + const forceRefreshPromises = beforeInitRefreshAuthState.map(config => { + console.log(`Preparing force refresh for config: ${config.configId}`); + return firstValueFrom(oidcSecurityService.forceRefreshSession(undefined, config.configId)); + }); + + try { + // Wait parallel refresh operations + await Promise.all(forceRefreshPromises); + console.log('All force refresh sessions completed successfully'); + } catch (error: any) { + console.error('Failed to force refresh one or more configs:', error); + + // Check if iframe was created + const iframes = document.querySelectorAll('iframe'); + console.error('Number of iframes:', iframes.length); + iframes.forEach((iframe, i) => { + console.error(`Iframe ${i} src:`, iframe.src); + }); + + throw error; + } + + const afterInitRefreshAuthState = await firstValueFrom(oidcSecurityService.checkAuthMultiple()); + + expect(afterInitRefreshAuthState.every(r => r.isAuthenticated)).toBe(true); + + const initRefreshIdp1Claims = await getTokenClaims(oidcSecurityService, configIdIdp1); + const initRefreshIdp2Claims = await getTokenClaims(oidcSecurityService, configIdIdp2); + const initRefreshIdp3Claims = await getTokenClaims(oidcSecurityService, configIdIdp3); + + console.log('Initial token IATs:'); + console.log(`IDP1: ${new Date(initRefreshIdp1Claims.iat * 1000).toISOString()} (${initRefreshIdp1Claims.iat})`); + console.log(`IDP2: ${new Date(initRefreshIdp2Claims.iat * 1000).toISOString()} (${initRefreshIdp2Claims.iat})`); + console.log(`IDP3: ${new Date(initRefreshIdp3Claims.iat * 1000).toISOString()} (${initRefreshIdp3Claims.iat})`); + + const uniqueInitialIats = new Set([ + initRefreshIdp1Claims.iat, + initRefreshIdp2Claims.iat, + initRefreshIdp3Claims.iat + ]); + console.log(`Unique initial IAT timestamps: ${uniqueInitialIats.size} (should be 3 for independent refreshes)`); + expect(uniqueInitialIats.size).toBe(3); + + const now = Date.now(); + const timeUntilRefreshIdp1 = ((initRefreshIdp1Claims.exp * 1000) - (renewSecBeforeExp * 1000)) - now; + const timeUntilRefreshIdp2 = ((initRefreshIdp2Claims.exp * 1000) - (renewSecBeforeExp * 1000)) - now; + const timeUntilRefreshIdp3 = ((initRefreshIdp3Claims.exp * 1000) - (renewSecBeforeExp * 1000)) - now; + + console.log('Time until next refresh for each IDP:'); + console.log(`IDP1: ${timeUntilRefreshIdp1 / 1000} seconds`); + console.log(`IDP2: ${timeUntilRefreshIdp2 / 1000} seconds`); + console.log(`IDP3: ${timeUntilRefreshIdp3 / 1000} seconds`); + + expect(timeUntilRefreshIdp1).toBeGreaterThan(1000); + expect(timeUntilRefreshIdp2).toBeGreaterThan(1000); + expect(timeUntilRefreshIdp3).toBeGreaterThan(1000); + + console.log('STEP 2: Waiting for automatic token refresh...'); + + // Wait for all 3 token renewals + await waitForTokenRenewals(publicEventsService, 3); + + const autoRefreshIdp1Claims = await getTokenClaims(oidcSecurityService, configIdIdp1); + const autoRefreshIdp2Claims = await getTokenClaims(oidcSecurityService, configIdIdp2); + const autoRefreshIdp3Claims = await getTokenClaims(oidcSecurityService, configIdIdp3); + + const uniqueIats = new Set([ + autoRefreshIdp1Claims.iat, + autoRefreshIdp2Claims.iat, + autoRefreshIdp3Claims.iat + ]); + console.log(`Unique IAT timestamps: ${uniqueIats.size} (should be 3 for independent renewals)`); + + expect(uniqueIats.size).toBe(3); + expect(autoRefreshIdp1Claims.iat).toBeGreaterThan(initRefreshIdp1Claims.iat); + expect(autoRefreshIdp2Claims.iat).toBeGreaterThan(initRefreshIdp2Claims.iat); + expect(autoRefreshIdp3Claims.iat).toBeGreaterThan(initRefreshIdp3Claims.iat); + + console.log('\n✅ TEST COMPLETE: Automatic token refresh works correctly'); + }, 60000); +}); + + +function parseJwt(token: string): any { + try { + const parts = token.split('.'); + if (parts.length !== 3) { + throw new Error('Invalid JWT format'); + } + + const payload = parts[1]; + const base64 = payload.replace(/-/g, '+').replace(/_/g, '/'); + const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '='); + const decoded = atob(padded); + return JSON.parse(decoded); + } catch (error) { + console.error('Failed to parse JWT:', error); + return null; + } +} + +function waitForTokenRenewals(publicEventsService: PublicEventsService, count: number) { + return firstValueFrom( + publicEventsService.registerForEvents() + .pipe( + filter(event => + event.type === EventTypes.NewAuthenticationResult && + event.value?.isRenewProcess === true + ), + take(count), + toArray() + ) + ); +} + +async function getTokenClaims(oidcSecurityService: OidcSecurityService, configId: string): Promise { + const authResult = await firstValueFrom(oidcSecurityService.getAuthenticationResult(configId)); + return parseJwt(authResult!.id_token!); +} diff --git a/projects/integration-tests/test-idp-server/.gitignore b/projects/integration-tests/test-idp-server/.gitignore new file mode 100644 index 000000000..a7c52664e --- /dev/null +++ b/projects/integration-tests/test-idp-server/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.log +.env +tmp diff --git a/projects/integration-tests/test-idp-server/README.md b/projects/integration-tests/test-idp-server/README.md new file mode 100644 index 000000000..83136ff8f --- /dev/null +++ b/projects/integration-tests/test-idp-server/README.md @@ -0,0 +1,33 @@ +# Test IDP Server + +## Usage + +### Server Management Scripts + +The server now includes shell scripts for managing the server lifecycle: + +```bash +# Start the server (finds available port, tracks PID) +./start.sh + +# Stop the server +./stop.sh + +# Restart the server +./restart.sh + +``` + +## Endpoints + +Management endpoints: +- `/health` - Token endpoint + +Each realm provides the following endpoints: + +- `/{realm}/.well-known/openid-configuration` - OIDC discovery +- `/{realm}/authorize` - Authorization endpoint +- `/{realm}/token` - Token endpoint +- `/{realm}/jwks` - JWKS endpoint +- `/{realm}/userinfo` - UserInfo endpoint +- `/{realm}/logout` - Logout endpoint diff --git a/projects/integration-tests/test-idp-server/package-lock.json b/projects/integration-tests/test-idp-server/package-lock.json new file mode 100644 index 000000000..7300a7b04 --- /dev/null +++ b/projects/integration-tests/test-idp-server/package-lock.json @@ -0,0 +1,1606 @@ +{ + "name": "test-idp-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test-idp-server", + "version": "1.0.0", + "dependencies": { + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "express": "^4.18.2", + "jose": "^5.2.0" + }, + "devDependencies": { + "@types/cookie-parser": "^1.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^20.10.0", + "nodemon": "^3.0.2", + "ts-node": "^10.9.2", + "typescript": "^5.3.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.3.tgz", + "integrity": "sha512-AiR5uKpFxP3PjO4R19kQGIMwxyRyPuXmKEEy301V1C0+1rVjS94EZQXf1QKZYN8Q0YM+estSPhmx5JwNftv6nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "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/cookie-parser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "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/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "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/node": { + "version": "20.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.2.tgz", + "integrity": "sha512-9pLGGwdzOUBDYi0GNjM97FIA+f92fqSke6joWeBjWXllfNxZBs7qeMF7tvtOIsbY45xkWkxrdwUfUf3MnQa9gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "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.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "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/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "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/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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "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==", + "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/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "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/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "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/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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==", + "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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/projects/integration-tests/test-idp-server/package.json b/projects/integration-tests/test-idp-server/package.json new file mode 100644 index 000000000..ff8c45cd0 --- /dev/null +++ b/projects/integration-tests/test-idp-server/package.json @@ -0,0 +1,24 @@ +{ + "name": "test-idp-server", + "version": "1.0.0", + "description": "Test OIDC Identity Provider server", + "scripts": { + "start": "ts-node src/index.ts", + "dev": "export PORT=8081; nodemon --exec ts-node src/index.ts" + }, + "dependencies": { + "express": "^4.18.2", + "jose": "^5.2.0", + "cors": "^2.8.5", + "cookie-parser": "^1.4.6" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.10.0", + "@types/cors": "^2.8.17", + "@types/cookie-parser": "^1.4.6", + "typescript": "^5.3.0", + "ts-node": "^10.9.2", + "nodemon": "^3.0.2" + } +} diff --git a/projects/integration-tests/test-idp-server/restart.sh b/projects/integration-tests/test-idp-server/restart.sh new file mode 100755 index 000000000..08f166367 --- /dev/null +++ b/projects/integration-tests/test-idp-server/restart.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Test IDP Server Restart Script + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "Restarting Test IDP Server..." + +# Stop the server if running +"$SCRIPT_DIR/stop.sh" + +# Wait a moment for cleanup +sleep 2 + +# Start the server +"$SCRIPT_DIR/start.sh" \ No newline at end of file diff --git a/projects/integration-tests/test-idp-server/src/TestIdpServer.ts b/projects/integration-tests/test-idp-server/src/TestIdpServer.ts new file mode 100644 index 000000000..90f5d9bff --- /dev/null +++ b/projects/integration-tests/test-idp-server/src/TestIdpServer.ts @@ -0,0 +1,499 @@ +import express, { Application, Request, Response, NextFunction } from 'express'; +import { Server } from 'http'; +import cors from 'cors'; +import cookieParser from 'cookie-parser'; +import { SignJWT, exportJWK, importPKCS8, KeyLike } from 'jose'; +import crypto from 'crypto'; + +export interface TestIdpServerOptions { + port?: number; + realms?: string[]; +} + +interface KeyPair { + privateKey: KeyLike; + publicKey: string; + publicJWK: any; + kid: string; +} + +interface Session { + realm: string; + state: string; + nonce?: string; + clientId?: string; + scope?: string; + codeChallenge?: string; + codeChallengeMethod?: string; +} + +export class TestIdpServer { + private app: Application; + private server: Server | null = null; + private port: number; + private realms: string[]; + private keyPairs: Record = {}; + private activeSessions: Map = new Map(); + private loggedOutSessions: Set = new Set(); + + constructor(options: TestIdpServerOptions = {}) { + this.port = options.port || 8080; + this.realms = options.realms || ['idp1', 'idp2', 'idp3']; + this.app = express(); + this.setupMiddleware(); + this.setupRoutes(); + } + + private setupMiddleware(): void { + this.app.use(express.urlencoded({ extended: true })); + this.app.use(express.json()); + this.app.use(cookieParser()); + this.app.use(cors({ + origin: true, + credentials: true + })); + this.app.use((req: Request, res: Response, next: NextFunction) => { + console.log(`Test IDP: ${req.method} ${req.url}`); + next(); + }); + } + + private setupRoutes(): void { + this.app.get('/health', (req: Request, res: Response) => { + res.json({ status: 'ok', realms: this.realms }); + }); + + this.realms.forEach(realm => { + this.setupRealmRoutes(realm); + }); + } + + private setupRealmRoutes(realm: string): void { + const realmPath = `/${realm}`; + + // OIDC Discovery endpoint + this.app.get(`${realmPath}/.well-known/openid-configuration`, (req: Request, res: Response) => { + res.json({ + issuer: `http://localhost:${this.port}${realmPath}`, + authorization_endpoint: `http://localhost:${this.port}${realmPath}/authorize`, + token_endpoint: `http://localhost:${this.port}${realmPath}/token`, + userinfo_endpoint: `http://localhost:${this.port}${realmPath}/userinfo`, + end_session_endpoint: `http://localhost:${this.port}${realmPath}/logout`, + jwks_uri: `http://localhost:${this.port}${realmPath}/jwks`, + scopes_supported: this.getSupportedScopes(realm), + response_types_supported: ['code'], + grant_types_supported: ['authorization_code'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + code_challenge_methods_supported: ['S256', 'plain'] + }); + }); + + // Authorization endpoint + this.app.get(`${realmPath}/authorize`, (req: Request, res: Response) => { + const { response_type, client_id, redirect_uri, scope, state, prompt, code_challenge, code_challenge_method } = req.query; + + console.log(`Test IDP: ${realm} authorize request:`, { + response_type, + client_id, + redirect_uri, + prompt, + code_challenge: code_challenge ? 'present' : 'missing', + code_challenge_method, + cookies: req.cookies + }); + + if (response_type !== 'code') { + return res.status(400).json({ error: 'unsupported_response_type' }); + } + + // Generate authorization code + const code = `${realm}-auth-code-${Date.now()}`; + + // Store session for this authorization including all parameters + this.activeSessions.set(code, { + realm, + state: state as string, + nonce: req.query.nonce as string, + clientId: client_id as string, + scope: scope as string, + codeChallenge: code_challenge as string, + codeChallengeMethod: code_challenge_method as string + }); + + // For silent authentication (prompt=none) or SSO scenario + // We simulate that the user has an active session + if (prompt === 'none' || req.cookies?.SSO_SESSION === 'active') { + // Redirect back with authorization code + const redirectUrl = new URL(redirect_uri as string); + redirectUrl.searchParams.set('code', code); + redirectUrl.searchParams.set('state', state as string); + + console.log(`Test IDP: ${realm} silent auth success, redirecting to:`, redirectUrl.toString()); + return res.redirect(redirectUrl.toString()); + } + + // For regular authentication, we would show a login page + // For testing, we'll just redirect with the code + const redirectUrl = new URL(redirect_uri as string); + redirectUrl.searchParams.set('code', code); + redirectUrl.searchParams.set('state', state as string); + + // Set SSO cookie for subsequent requests + res.cookie('SSO_SESSION', 'active', { + httpOnly: true, + sameSite: 'lax', + maxAge: 3600000 // 1 hour + }); + res.redirect(redirectUrl.toString()); + }); + + // Token endpoint + this.app.post(`${realmPath}/token`, async (req: Request, res: Response) => { + const { grant_type, code, redirect_uri, client_id, code_verifier } = req.body; + + console.log(`Test IDP: ${realm} token request:`, { + grant_type, + code, + code_verifier: code_verifier ? 'present' : 'missing' + }); + + if (grant_type !== 'authorization_code') { + return res.status(400).json({ error: 'unsupported_grant_type' }); + } + + // Verify the authorization code + const session = this.activeSessions.get(code); + if (!session || session.realm !== realm) { + return res.status(400).json({ error: 'invalid_grant' }); + } + + // Verify PKCE if code_challenge was provided + if (session.codeChallenge) { + if (!code_verifier) { + console.log(`Test IDP: ${realm} missing code_verifier for PKCE flow`); + return res.status(400).json({ error: 'invalid_request', error_description: 'code_verifier required' }); + } + + // Verify the code_verifier matches the code_challenge + const verifierBuffer = Buffer.from(code_verifier, 'utf-8'); + const challenge = crypto.createHash('sha256').update(verifierBuffer).digest('base64url'); + + if (challenge !== session.codeChallenge) { + console.log(`Test IDP: ${realm} code_verifier validation failed`); + return res.status(400).json({ error: 'invalid_grant', error_description: 'code_verifier validation failed' }); + } + + console.log(`Test IDP: ${realm} PKCE validation successful`); + } + + // Remove used authorization code + this.activeSessions.delete(code); + + // Get the appropriate test token for this realm with session data + const tokenData = await this.getTokenForRealm(realm, session); + + res.json({ + access_token: `${realm}-access-token-${Date.now()}`, + id_token: tokenData, + token_type: 'Bearer', + expires_in: 300, // 5 minutes to match ID token + scope: session.scope || this.getScopeString(realm) + }); + return; + }); + + // JWKS endpoint + this.app.get(`${realmPath}/jwks`, (req: Request, res: Response) => { + const keyPair = this.getKeyPairForRealm(realm); + res.json({ + keys: [keyPair.publicJWK] + }); + }); + + // UserInfo endpoint + this.app.get(`${realmPath}/userinfo`, (req: Request, res: Response): void => { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.includes(`${realm}-access-token`)) { + res.status(401).json({ error: 'unauthorized' }); + return; + } + + res.json(this.getUserInfoForRealm(realm)); + }); + + // Logout endpoint (OIDC end_session_endpoint) + this.app.get(`${realmPath}/logout`, async (req: Request, res: Response) => { + const { id_token_hint, post_logout_redirect_uri, state } = req.query; + + console.log(`Test IDP: ${realm} logout request:`, { + id_token_hint: id_token_hint ? 'present' : 'missing', + post_logout_redirect_uri, + state + }); + + // In a real IDP, you would validate the id_token_hint + // For testing, we'll just check if it's present + if (id_token_hint) { + try { + // Decode the token to verify it's from this realm + const tokenParts = (id_token_hint as string).split('.'); + if (tokenParts.length === 3) { + const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString()); + console.log(`Test IDP: ${realm} logout for subject:`, payload.sub); + } + } catch (error) { + console.log(`Test IDP: ${realm} invalid id_token_hint`); + } + } + + // In a real OIDC logout, we end the IDP session but the SSO cookie remains + // This allows other applications to continue using SSO + // We'll track logged out sessions in memory for testing + if (!this.loggedOutSessions) { + this.loggedOutSessions = new Set(); + } + this.loggedOutSessions.add(`${realm}-${id_token_hint}`); + + // Note: We do NOT clear the SSO_SESSION cookie + // res.clearCookie('SSO_SESSION'); + + // Log the logout for testing purposes + console.log(`Test IDP: ${realm} logout completed, redirecting to:`, post_logout_redirect_uri); + + if (post_logout_redirect_uri) { + // Build redirect URL with state if provided + let redirectUrl = post_logout_redirect_uri as string; + if (state) { + const separator = redirectUrl.includes('?') ? '&' : '?'; + redirectUrl = `${redirectUrl}${separator}state=${state}`; + } + res.redirect(redirectUrl); + } else { + res.send(`Logged out from ${realm}`); + } + }); + } + + private getSupportedScopes(realm: string): string[] { + switch (realm) { + case 'master-idp': + return ['openid', 'profile', 'email']; + case 'secondary-idp-1': + return ['openid', 'profile']; + case 'secondary-idp-2': + return ['openid', 'email']; + case 'idp1': + return ['openid', 'profile', 'email']; + case 'idp2': + return ['openid', 'profile']; + case 'idp3': + return ['openid', 'email']; + default: + return ['openid']; + } + } + + private getScopeString(realm: string): string { + return this.getSupportedScopes(realm).join(' '); + } + + private async getTokenForRealm(realm: string, session?: Session): Promise { + const keyPair = this.getKeyPairForRealm(realm); + const claims = this.getClaimsForRealm(realm, session); + + // Add a unique timestamp with millisecond precision to ensure different IATs + // Add light jitter (0-10ms) based on realm to ensure different IATs + const jitter = this.getJitterForRealm(realm); + const now = Date.now() + jitter; + const iatWithMillis = Math.floor(now / 1000) + (now % 1000) / 1000000; + + const jwt = await new SignJWT(claims) + .setProtectedHeader({ alg: 'RS256', kid: keyPair.kid }) + .setIssuer(`http://localhost:${this.port}/${realm}`) + .setAudience(session?.clientId || this.getClientIdForRealm(realm)) + .setExpirationTime('5m') + .setIssuedAt(iatWithMillis) + .sign(keyPair.privateKey); + + return jwt; + } + + private getKeyPairForRealm(realm: string): KeyPair { + const realmKey = this.mapRealmToKey(realm); + const keyPair = this.keyPairs[realmKey]; + if (!keyPair) { + throw new Error(`No key pair found for realm: ${realm}`); + } + return keyPair; + } + + private mapRealmToKey(realm: string): string { + switch (realm) { + case 'master-idp': + return 'master'; + case 'secondary-idp-1': + return 'secondary1'; + case 'secondary-idp-2': + return 'secondary2'; + case 'idp1': + return 'idp1'; + case 'idp2': + return 'idp2'; + case 'idp3': + return 'idp3'; + default: + return realm; + } + } + + private getJitterForRealm(realm: string): number { + // Add deterministic jitter based on realm to ensure different IATs + switch (realm) { + case 'idp1': + return 0; // No jitter + case 'idp2': + return 5; // 5ms jitter + case 'idp3': + return 10; // 10ms jitter + default: + return Math.floor(Math.random() * 10); // Random 0-10ms for other realms + } + } + + private getClientIdForRealm(realm: string): string { + switch (realm) { + case 'idp1': + return 'client-idp1'; + case 'idp2': + return 'client-idp2'; + case 'idp3': + return 'client-idp3'; + default: + return 'unknown-client'; + } + } + + private getClaimsForRealm(realm: string, session?: Session): any { + const userInfo = this.getUserInfoForRealm(realm); + const scope = session?.scope || this.getScopeString(realm); + + // Parse the scope to bp: prefixed roles + const scopes = scope.split(' '); + const bpRoles = scopes + .filter(s => s.startsWith('bp:')) + .map(s => s.substring(3)); + + return { + ...userInfo, + nonce: session?.nonce || 'test-nonce', + azp: session?.clientId || this.getClientIdForRealm(realm), + scope: scope + }; + } + + private getUserInfoForRealm(realm: string): any { + switch (realm) { + case 'master-idp': + return { + sub: 'master-user-123', + name: 'Test User', + email: 'test@example.com', + preferred_username: 'testuser' + }; + case 'secondary-idp-1': + return { + sub: 'secondary1-user-123', + name: 'Test User', + preferred_username: 'testuser' + }; + case 'secondary-idp-2': + return { + sub: 'secondary2-user-123', + email: 'test@example.com', + preferred_username: 'testuser' + }; + case 'idp1': + return { + sub: 'idp1-user-123', + name: 'Test User IDP1', + email: 'test@example.com', + preferred_username: 'testuser1' + }; + case 'idp2': + return { + sub: 'idp2-user-123', + name: 'Test User IDP2', + preferred_username: 'testuser2' + }; + case 'idp3': + return { + sub: 'idp3-user-123', + email: 'test@example.com', + preferred_username: 'testuser3' + }; + default: + return { sub: 'unknown-user' }; + } + } + + private async generateTestData(): Promise { + // Generate key pairs for each realm + const keyRealms = ['master', 'secondary1', 'secondary2', 'idp1', 'idp2', 'idp3']; + for (const realm of keyRealms) { + const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }); + + const kid = `${realm}-key-${Date.now()}-${Math.random().toString(36).substring(7)}`; + const privateKeyJose = await importPKCS8(privateKey, 'RS256'); + const publicKeyBuffer = crypto.createPublicKey(publicKey); + const publicJWK = await exportJWK(publicKeyBuffer); + publicJWK.kid = kid; + publicJWK.use = 'sig'; + publicJWK.alg = 'RS256'; + + this.keyPairs[realm] = { + privateKey: privateKeyJose, + publicKey, + publicJWK, + kid + }; + } + } + + async start(): Promise { + // Generate test data + await this.generateTestData(); + + return new Promise((resolve, reject) => { + this.server = this.app.listen(this.port, () => { + console.log(`Test IDP server started on http://localhost:${this.port}`); + console.log(`Realms available: ${this.realms.join(', ')}`); + resolve(); + }).on('error', reject); + }); + } + + async stop(): Promise { + return new Promise((resolve) => { + if (this.server) { + this.server.close(() => { + console.log('Test IDP server stopped'); + this.server = null; + resolve(); + }); + } else { + resolve(); + } + }); + } + + getUrl(realm?: string): string { + const baseUrl = `http://localhost:${this.port}`; + return realm ? `${baseUrl}/${realm}` : baseUrl; + } +} diff --git a/projects/integration-tests/test-idp-server/src/index.ts b/projects/integration-tests/test-idp-server/src/index.ts new file mode 100644 index 000000000..6f55822b1 --- /dev/null +++ b/projects/integration-tests/test-idp-server/src/index.ts @@ -0,0 +1,23 @@ +import { TestIdpServer } from './TestIdpServer'; + +// Start the server if run directly +if (require.main === module) { + const port = parseInt(process.env.PORT || '8080', 10); + const server = new TestIdpServer({ + port, + realms: ['idp1', 'idp2', 'idp3'] + }); + + server.start().then(() => { + console.log(`Test IDP server is running on port ${port}`); + console.log('Press Ctrl+C to stop the server'); + + process.on('SIGINT', async () => { + console.log('\nStopping server...'); + await server.stop(); + process.exit(0); + }); + }).catch(console.error); +} + +export { TestIdpServer }; \ No newline at end of file diff --git a/projects/integration-tests/test-idp-server/start.sh b/projects/integration-tests/test-idp-server/start.sh new file mode 100755 index 000000000..b29650bae --- /dev/null +++ b/projects/integration-tests/test-idp-server/start.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +# Test IDP Server Start Script +# Based on wiremock's approach but adapted for Node.js + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TMP_DIR="$SCRIPT_DIR/tmp" +PID_FILE="$TMP_DIR/test-idp-server.pid" +PORT_FILE="$TMP_DIR/test-idp-server.port" +LOG_FILE="$TMP_DIR/test-idp-server.log" +DEFAULT_PORT=8081 + +# Create tmp directory if it doesn't exist +mkdir -p "$TMP_DIR" + +# Check if server is already running +if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p "$PID" > /dev/null 2>&1; then + PORT=$(cat "$PORT_FILE" 2>/dev/null || echo "$DEFAULT_PORT") + echo "Test IDP Server is already running on port $PORT (PID: $PID)" + exit 0 + else + echo "Removing stale PID file..." + rm -f "$PID_FILE" + rm -f "$PORT_FILE" + fi +fi + +# Check if npm is available +if ! command -v npm &> /dev/null; then + echo "npm is not installed. Please install Node.js and npm first." + exit 1 +fi + +# Change to script directory +cd "$SCRIPT_DIR" + +# Install dependencies if needed +if [ ! -d "node_modules" ]; then + echo "Installing dependencies..." + npm install +fi + +# Find available port +PORT=$DEFAULT_PORT +while lsof -i :$PORT >/dev/null 2>&1; do + echo "Port $PORT is in use, trying next port..." + PORT=$((PORT + 1)) +done + +echo "Starting Test IDP Server on port $PORT..." + +# Start the server in background +PORT=$PORT nohup npm run start > "$LOG_FILE" 2>&1 & +PID=$! +disown $PID + +# Store PID and port +echo $PID > "$PID_FILE" +echo $PORT > "$PORT_FILE" + +# Wait for server to start +echo "Waiting for server to start..." +MAX_ATTEMPTS=30 +ATTEMPT=0 + +while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do + if curl -s "http://localhost:$PORT/.well-known/openid-configuration" > /dev/null 2>&1; then + echo "Test IDP Server started successfully on port $PORT (PID: $PID)" + echo "Log file: $LOG_FILE" + exit 0 + fi + + # Check if process is still running + if ! ps -p "$PID" > /dev/null 2>&1; then + echo "Server failed to start. Check the log file: $LOG_FILE" + tail -20 "$LOG_FILE" + rm -f "$PID_FILE" + rm -f "$PORT_FILE" + exit 1 + fi + + sleep 1 + ATTEMPT=$((ATTEMPT + 1)) +done + +echo "Server failed to start within 30 seconds. Check the log file: $LOG_FILE" +tail -20 "$LOG_FILE" +exit 1 diff --git a/projects/integration-tests/test-idp-server/stop.sh b/projects/integration-tests/test-idp-server/stop.sh new file mode 100755 index 000000000..e42cd32f7 --- /dev/null +++ b/projects/integration-tests/test-idp-server/stop.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Test IDP Server Stop Script + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TMP_DIR="$SCRIPT_DIR/tmp" +PID_FILE="$TMP_DIR/test-idp-server.pid" +PORT_FILE="$TMP_DIR/test-idp-server.port" + +# Check if PID file exists +if [ ! -f "$PID_FILE" ]; then + echo "Test IDP Server is not running (no PID file found)" + exit 0 +fi + +# Read PID +PID=$(cat "$PID_FILE") + +# Check if process is running +if ! ps -p "$PID" > /dev/null 2>&1; then + echo "Test IDP Server is not running (process $PID not found)" + rm -f "$PID_FILE" + rm -f "$PORT_FILE" + exit 0 +fi + +# Get port for display +PORT=$(cat "$PORT_FILE" 2>/dev/null || echo "unknown") + +echo "Stopping Test IDP Server on port $PORT (PID: $PID)..." + +# Send SIGTERM to gracefully stop the server +kill -TERM "$PID" + +# Wait for process to stop (max 10 seconds) +WAIT_TIME=0 +while ps -p "$PID" > /dev/null 2>&1 && [ $WAIT_TIME -lt 10 ]; do + sleep 1 + WAIT_TIME=$((WAIT_TIME + 1)) +done + +# Force kill if still running +if ps -p "$PID" > /dev/null 2>&1; then + echo "Server didn't stop gracefully, forcing shutdown..." + kill -9 "$PID" +fi + +# Clean up files +rm -f "$PID_FILE" +rm -f "$PORT_FILE" + +echo "Test IDP Server stopped" \ No newline at end of file diff --git a/projects/integration-tests/test-idp-server/tsconfig.json b/projects/integration-tests/test-idp-server/tsconfig.json new file mode 100644 index 000000000..63c481204 --- /dev/null +++ b/projects/integration-tests/test-idp-server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/projects/integration-tests/tsconfig.app.json b/projects/integration-tests/tsconfig.app.json new file mode 100644 index 000000000..e40712b8d --- /dev/null +++ b/projects/integration-tests/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/projects/integration-tests/tsconfig.json b/projects/integration-tests/tsconfig.json new file mode 100644 index 000000000..1f432e654 --- /dev/null +++ b/projects/integration-tests/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "exclude": [ + "test-idp-server/**/*" + ] +} \ No newline at end of file diff --git a/projects/integration-tests/tsconfig.spec.json b/projects/integration-tests/tsconfig.spec.json new file mode 100644 index 000000000..0370688a4 --- /dev/null +++ b/projects/integration-tests/tsconfig.spec.json @@ -0,0 +1,21 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts", + "src/tests/**/*.spec.ts", + "src/tests/**/*.ts" + ], + "exclude": [ + "test-idp-server/**/*" + ] +} diff --git a/projects/sample-code-flow-multi-iframe/src/silent-renew.html b/projects/sample-code-flow-multi-iframe/src/silent-renew.html index da73284b7..f03217c60 100644 --- a/projects/sample-code-flow-multi-iframe/src/silent-renew.html +++ b/projects/sample-code-flow-multi-iframe/src/silent-renew.html @@ -12,9 +12,13 @@ window.onload = function () { /* The parent window hosts the Angular application */ var parent = window.parent; + /* Send the id_token information to the oidc message handler */ var event = new CustomEvent('oidc-silent-renew-message', { - detail: window.location, + detail: { + url: window.location, + srcFrameId: window.frameElement?.id + } }); parent.dispatchEvent(event); }; diff --git a/projects/schematics/src/ng-add/actions/add-module-import.ts b/projects/schematics/src/ng-add/actions/add-module-import.ts index ace70e76a..d7a1e2b41 100644 --- a/projects/schematics/src/ng-add/actions/add-module-import.ts +++ b/projects/schematics/src/ng-add/actions/add-module-import.ts @@ -10,9 +10,19 @@ export function addModuleToImports(options: NgAddOptions): Rule { const { moduleFileName, moduleName } = options.moduleInfo!; + // Try to find the app module file with different naming conventions + const appModulePath = findAppModulePath(host, project.sourceRoot); + + if (!appModulePath) { + throw new Error( + 'Could not find app module file. Tried: app.module.ts, app-module.ts. ' + + 'Please ensure your app module exists in the src/app directory.' + ); + } + const modulesToImport = [ { - target: `${project.sourceRoot}/app/app.module.ts`, + target: appModulePath, moduleName, modulePath: `./auth/${moduleFileName}`, }, @@ -28,6 +38,22 @@ export function addModuleToImports(options: NgAddOptions): Rule { }; } +function findAppModulePath(host: Tree, sourceRoot: string): string | null { + // Try common naming conventions for app module + const possiblePaths = [ + `${sourceRoot}/app/app.module.ts`, // Traditional Angular CLI naming + `${sourceRoot}/app/app-module.ts`, // Newer Angular CLI naming convention + ]; + + for (const path of possiblePaths) { + if (host.exists(path)) { + return path; + } + } + + return null; +} + function addImport(host: Tree, context: SchematicContext, moduleName: string, source: string, target: string) { const sourcefile = readIntoSourceFile(host, target); const importChanges = addImportToModule(sourcefile, source, moduleName, source) as InsertChange[]; diff --git a/projects/schematics/src/ng-add/files/silent-renew/silent-renew.html b/projects/schematics/src/ng-add/files/silent-renew/silent-renew.html index 7aab22f4b..4815235ed 100644 --- a/projects/schematics/src/ng-add/files/silent-renew/silent-renew.html +++ b/projects/schematics/src/ng-add/files/silent-renew/silent-renew.html @@ -12,8 +12,14 @@ window.onload = function () { /* The parent window hosts the Angular application */ var parent = window.parent; + /* Send the id_token information to the oidc message handler */ - var event = new CustomEvent('oidc-silent-renew-message', { detail: window.location }); + var event = new CustomEvent('oidc-silent-renew-message', { + detail: { + url: window.location, + srcFrameId: window.frameElement?.id + } + }); parent.dispatchEvent(event); };