diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index 9175db9..0000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = {
- extends: [
- '@nextcloud',
- ],
- rules: {
- 'jsdoc/require-jsdoc': 'off',
- 'vue/first-attribute-linebreak': 'off',
- },
-}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 852b265..6dd9544 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -16,22 +16,6 @@ updates:
time: "03:00"
timezone: Europe/Paris
open-pull-requests-limit: 10
- - package-ecosystem: composer
- directory: "/vendor-bin/openapi-extractor"
- schedule:
- interval: weekly
- day: saturday
- time: "03:00"
- timezone: Europe/Paris
- open-pull-requests-limit: 10
- - package-ecosystem: composer
- directory: "/vendor-bin/phpunit"
- schedule:
- interval: weekly
- day: saturday
- time: "03:00"
- timezone: Europe/Paris
- open-pull-requests-limit: 10
- package-ecosystem: composer
directory: "/vendor-bin/psalm"
schedule:
diff --git a/.github/workflows/lint-eslint.yml b/.github/workflows/lint-eslint.yml
deleted file mode 100644
index d0a8a2f..0000000
--- a/.github/workflows/lint-eslint.yml
+++ /dev/null
@@ -1,95 +0,0 @@
-# This workflow is provided via the organization template repository
-#
-# https://github.com/nextcloud/.github
-# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
-#
-# Use lint-eslint together with lint-eslint-when-unrelated to make eslint a required check for GitHub actions
-# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
-
-name: Lint eslint
-
-on: pull_request
-
-permissions:
- contents: read
-
-concurrency:
- group: lint-eslint-${{ github.head_ref || github.run_id }}
- cancel-in-progress: true
-
-jobs:
- changes:
- runs-on: ubuntu-latest-low
-
- outputs:
- src: ${{ steps.changes.outputs.src}}
-
- steps:
- - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- id: changes
- continue-on-error: true
- with:
- filters: |
- src:
- - '.github/workflows/**'
- - 'src/**'
- - 'appinfo/info.xml'
- - 'package.json'
- - 'package-lock.json'
- - 'tsconfig.json'
- - '.eslintrc.*'
- - '.eslintignore'
- - '**.js'
- - '**.ts'
- - '**.vue'
-
- lint:
- runs-on: ubuntu-latest
-
- needs: changes
- if: needs.changes.outputs.src != 'false'
-
- name: NPM lint
-
- steps:
- - name: Checkout
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
-
- - name: Read package.json node and npm engines version
- uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
- id: versions
- with:
- fallbackNode: '^20'
- fallbackNpm: '^10'
-
- - name: Set up node ${{ steps.versions.outputs.nodeVersion }}
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v3
- with:
- node-version: ${{ steps.versions.outputs.nodeVersion }}
-
- - name: Set up npm ${{ steps.versions.outputs.npmVersion }}
- run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
-
- - name: Install dependencies
- env:
- CYPRESS_INSTALL_BINARY: 0
- PUPPETEER_SKIP_DOWNLOAD: true
- run: npm ci
-
- - name: Lint
- run: npm run lint
-
- summary:
- permissions:
- contents: none
- runs-on: ubuntu-latest-low
- needs: [changes, lint]
-
- if: always()
-
- # This is the summary, we just avoid to rename it so that branch protection rules still match
- name: eslint
-
- steps:
- - name: Summary status
- run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi
diff --git a/.github/workflows/lint-stylelint.yml b/.github/workflows/lint-stylelint.yml
deleted file mode 100644
index 4effd42..0000000
--- a/.github/workflows/lint-stylelint.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-# This workflow is provided via the organization template repository
-#
-# https://github.com/nextcloud/.github
-# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
-
-name: Lint stylelint
-
-on: pull_request
-
-permissions:
- contents: read
-
-concurrency:
- group: lint-stylelint-${{ github.head_ref || github.run_id }}
- cancel-in-progress: true
-
-jobs:
- lint:
- runs-on: ubuntu-latest
-
- name: stylelint
-
- steps:
- - name: Checkout
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
-
- - name: Read package.json node and npm engines version
- uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
- id: versions
- with:
- fallbackNode: '^20'
- fallbackNpm: '^10'
-
- - name: Set up node ${{ steps.versions.outputs.nodeVersion }}
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v3
- with:
- node-version: ${{ steps.versions.outputs.nodeVersion }}
-
- - name: Set up npm ${{ steps.versions.outputs.npmVersion }}
- run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
-
- - name: Install dependencies
- env:
- CYPRESS_INSTALL_BINARY: 0
- run: npm ci
-
- - name: Lint
- run: npm run stylelint
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
deleted file mode 100644
index b0c3ce2..0000000
--- a/.github/workflows/openapi.yml
+++ /dev/null
@@ -1,85 +0,0 @@
-name: OpenAPI
-
-on: pull_request
-
-permissions:
- contents: read
-
-concurrency:
- group: openapi-${{ github.head_ref || github.run_id }}
- cancel-in-progress: true
-
-jobs:
- openapi:
- runs-on: ubuntu-latest
-
- if: ${{ github.repository_owner != 'nextcloud-gmbh' }}
-
- steps:
- - name: Checkout
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
-
- - name: Get php version
- id: php_versions
- uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
-
- - name: Set up php
- uses: shivammathur/setup-php@a4e22b60bbb9c1021113f2860347b0759f66fe5d # v2
- with:
- php-version: ${{ steps.php_versions.outputs.php-available }}
- extensions: xml
- coverage: none
- ini-file: development
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Check Typescript OpenApi types
- id: check_typescript_openapi
- uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0
- with:
- files: "src/types/openapi/openapi*.ts"
-
- - name: Read package.json node and npm engines version
- if: steps.check_typescript_openapi.outputs.files_exists == 'true'
- uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
- id: node_versions
- # Continue if no package.json
- continue-on-error: true
- with:
- fallbackNode: '^20'
- fallbackNpm: '^10'
-
- - name: Set up node ${{ steps.node_versions.outputs.nodeVersion }}
- if: ${{ steps.node_versions.outputs.nodeVersion }}
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- with:
- node-version: ${{ steps.node_versions.outputs.nodeVersion }}
-
- - name: Set up npm ${{ steps.node_versions.outputs.npmVersion }}
- if: ${{ steps.node_versions.outputs.nodeVersion }}
- run: npm i -g npm@"${{ steps.node_versions.outputs.npmVersion }}"
-
- - name: Install dependencies & build
- if: ${{ steps.node_versions.outputs.nodeVersion }}
- env:
- CYPRESS_INSTALL_BINARY: 0
- PUPPETEER_SKIP_DOWNLOAD: true
- run: |
- npm ci
-
- - name: Set up dependencies
- run: composer i
-
- - name: Regenerate OpenAPI
- run: composer run openapi
-
- - name: Check openapi*.json and typescript changes
- run: |
- bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please run \"composer run openapi\" and commit the openapi*.json files and (if applicable) src/types/openapi/openapi*.ts, see the section \"Show changes on failure\" for details' && exit 1)"
-
- - name: Show changes on failure
- if: failure()
- run: |
- git status
- git --no-pager diff
- exit 1 # make it red to grab attention
diff --git a/.github/workflows/psalm-matrix.yml b/.github/workflows/psalm-matrix.yml
deleted file mode 100644
index 8e3d42f..0000000
--- a/.github/workflows/psalm-matrix.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-# This workflow is provided via the organization template repository
-#
-# https://github.com/nextcloud/.github
-# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
-
-name: Static analysis
-
-on: pull_request
-
-concurrency:
- group: psalm-${{ github.head_ref || github.run_id }}
- cancel-in-progress: true
-
-jobs:
- matrix:
- runs-on: ubuntu-latest-low
- outputs:
- ocp-matrix: ${{ steps.versions.outputs.ocp-matrix }}
- steps:
- - name: Checkout app
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- - name: Get version matrix
- id: versions
- uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
-
- static-analysis:
- runs-on: ubuntu-latest
- needs: matrix
- strategy:
- # do not stop on another job's failure
- fail-fast: false
- matrix: ${{ fromJson(needs.matrix.outputs.ocp-matrix) }}
-
- name: static-psalm-analysis ${{ matrix.ocp-version }}
- steps:
- - name: Checkout
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
-
- - name: Set up php${{ matrix.php-versions }}
- uses: shivammathur/setup-php@a4e22b60bbb9c1021113f2860347b0759f66fe5d # v2
- with:
- php-version: ${{ matrix.php-versions }}
- extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
- coverage: none
- ini-file: development
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Install dependencies
- run: composer i
-
- - name: Install dependencies
- run: composer require --dev nextcloud/ocp:${{ matrix.ocp-version }} --ignore-platform-reqs --with-dependencies
-
- - name: Run coding standards check
- run: composer run psalm
-
- summary:
- runs-on: ubuntu-latest-low
- needs: static-analysis
-
- if: always()
-
- name: static-psalm-analysis-summary
-
- steps:
- - name: Summary status
- run: if ${{ needs.static-analysis.result != 'success' }}; then exit 1; fi
diff --git a/.github/workflows/update-nextcloud-ocp-approve-merge.yml b/.github/workflows/update-nextcloud-ocp-approve-merge.yml
deleted file mode 100644
index 9d24f73..0000000
--- a/.github/workflows/update-nextcloud-ocp-approve-merge.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-# This workflow is provided via the organization template repository
-#
-# https://github.com/nextcloud/.github
-# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
-
-name: Auto approve nextcloud/ocp
-
-on:
- pull_request_target:
- branches:
- - main
- - master
- - stable*
-
-permissions:
- contents: read
-
-concurrency:
- group: update-nextcloud-ocp-approve-merge-${{ github.head_ref || github.run_id }}
- cancel-in-progress: true
-
-jobs:
- auto-approve-merge:
- if: github.actor == 'nextcloud-command'
- runs-on: ubuntu-latest-low
- permissions:
- # for hmarr/auto-approve-action to approve PRs
- pull-requests: write
- # for alexwilson/enable-github-automerge-action to approve PRs
- contents: write
-
- steps:
- - uses: mdecoleman/pr-branch-name@bab4c71506bcd299fb350af63bb8e53f2940a599 # v2.0.0
- id: branchname
- with:
- repo-token: ${{ secrets.GITHUB_TOKEN }}
-
- # GitHub actions bot approve
- - uses: hmarr/auto-approve-action@b40d6c9ed2fa10c9a2749eca7eb004418a705501 # v2
- if: startsWith(steps.branchname.outputs.branch, 'automated/noid/') && endsWith(steps.branchname.outputs.branch, 'update-nextcloud-ocp')
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
-
- # Enable GitHub auto merge
- - name: Auto merge
- uses: alexwilson/enable-github-automerge-action@56e3117d1ae1540309dc8f7a9f2825bc3c5f06ff # main
- if: startsWith(steps.branchname.outputs.branch, 'automated/noid/') && endsWith(steps.branchname.outputs.branch, 'update-nextcloud-ocp')
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/update-nextcloud-ocp-matrix.yml b/.github/workflows/update-nextcloud-ocp-matrix.yml
deleted file mode 100644
index e1fa4d9..0000000
--- a/.github/workflows/update-nextcloud-ocp-matrix.yml
+++ /dev/null
@@ -1,94 +0,0 @@
-# This workflow is provided via the organization template repository
-#
-# https://github.com/nextcloud/.github
-# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
-
-name: Update nextcloud/ocp
-
-on:
- workflow_dispatch:
- schedule:
- - cron: '5 2 * * 0'
-
-jobs:
- update-nextcloud-ocp:
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- branches: ['main']
- target: ['stable29']
-
- name: update-nextcloud-ocp-${{ matrix.branches }}
-
- steps:
- - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- with:
- ref: ${{ matrix.branches }}
- submodules: true
-
- - name: Set up php8.2
- uses: shivammathur/setup-php@a4e22b60bbb9c1021113f2860347b0759f66fe5d # v2
- with:
- php-version: 8.2
- # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation
- extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
- coverage: none
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Read codeowners
- id: codeowners
- run: |
- grep '/appinfo/info.xml' .github/CODEOWNERS | cut -f 2- -d ' ' | xargs | awk '{ print "codeowners="$0 }' >> $GITHUB_OUTPUT
- continue-on-error: true
-
- - name: Composer install
- run: composer install
-
- - name: Composer update nextcloud/ocp
- id: update_branch
- run: composer require --dev nextcloud/ocp:dev-${{ matrix.target }}
-
- - name: Raise on issue on failure
- uses: dacbd/create-issue-action@cdb57ab6ff8862aa09fee2be6ba77a59581921c2 # v2.0.0
- if: ${{ failure() && steps.update_branch.conclusion == 'failure' }}
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
- title: Failed to update nextcloud/ocp package}
- body: Please check the output of the GitHub action and manually resolve the issues
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
${{ steps.codeowners.outputs.codeowners }}
-
- - name: Reset checkout 3rdparty
- run: |
- git clean -f 3rdparty
- git checkout 3rdparty
- continue-on-error: true
-
- - name: Reset checkout vendor
- run: |
- git clean -f vendor
- git checkout vendor
- continue-on-error: true
-
- - name: Reset checkout vendor-bin
- run: |
- git clean -f vendor-bin
- git checkout vendor-bin
- continue-on-error: true
-
- - name: Create Pull Request
- uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # v6.0.1
- with:
- token: ${{ secrets.COMMAND_BOT_PAT }}
- commit-message: "chore(dev-deps): Bump nextcloud/ocp package"
- committer: GitHub
- author: nextcloud-command
- signoff: true
- branch: automated/noid/${{ matrix.branches }}-update-nextcloud-ocp
- title: "[${{ matrix.branches }}] Update nextcloud/ocp dependency"
- body: |
- Auto-generated update of [nextcloud/ocp](https://github.com/nextcloud-deps/ocp/) dependency
- labels: |
- dependencies
- 3. to review
diff --git a/.gitignore b/.gitignore
index ad47d83..fc95ac8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@
/node_modules/
*.lock
+/package-lock.json
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index 91fc1d9..240047d 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -3,17 +3,18 @@
declare(strict_types=1);
require_once './vendor-bin/cs-fixer/vendor/autoload.php';
+require_once './src/CodingStandard/Config.php';
-use Nextcloud\CodingStandard\Config;
+use OCA\PrivacyIDEA\CodingStandard\Config;
$config = new Config();
-$config
- ->getFinder()
- ->notPath('build')
- ->notPath('l10n')
- ->notPath('node_modules')
- ->notPath('src')
- ->notPath('vendor')
- ->in(__DIR__);
+
+$config->getFinder()
+ ->notPath('build')
+ ->notPath('l10n')
+ ->notPath('node_modules')
+ ->notPath('src')
+ ->notPath('vendor')
+ ->in(__DIR__);
return $config;
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 589ee07..b7d651b 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -1,15 +1,15 @@
[
- [
- 'name' => 'Settings#setValue',
- 'url' => '/setValue',
- 'verb' => 'POST'
- ],
- [
- 'name' => 'Settings#getValue',
- 'url' => '/getValue',
- 'verb' => 'GET'
- ],
- ]);
\ No newline at end of file
+return [
+ 'routes' => [
+ [
+ 'name' => 'Settings#setValue',
+ 'url' => '/setValue',
+ 'verb' => 'POST'
+ ],
+ [
+ 'name' => 'Settings#getValue',
+ 'url' => '/getValue',
+ 'verb' => 'GET'
+ ],
+ ]];
diff --git a/composer.json b/composer.json
index a074ebe..068a59c 100644
--- a/composer.json
+++ b/composer.json
@@ -22,10 +22,7 @@
],
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './vendor-bin/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
- "cs:fix": "php-cs-fixer fix",
- "psalm": "psalm --threads=1 --no-cache",
- "test:unit": "phpunit tests -c tests/phpunit.xml --colors=always --fail-on-warning --fail-on-risky",
- "openapi": "generate-spec"
+ "cs:fix": "php-cs-fixer fix"
},
"require": {
"bamarni/composer-bin-plugin": "^1.8",
diff --git a/js/main.js b/js/main.js
index 6bc409d..827141e 100644
--- a/js/main.js
+++ b/js/main.js
@@ -80,7 +80,7 @@ function processWebauthn()
+ window.location.hostname
+ (window.location.port ? ':' + window.location.port : '');
}
- piSetValue("origin", window.origin); // todo check if this is correct (window.location.origin)
+ piSetValue("origin", window.origin);
try
{
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 64ce1d6..9cb3402 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -11,15 +11,19 @@
class Application extends App implements IBootstrap
{
- public const APP_ID = 'privacyidea';
+ public const APP_ID = 'privacyidea';
- /** @psalm-suppress PossiblyUnusedMethod */
- public function __construct()
- {
- parent::__construct(self::APP_ID);
- }
+ /** @psalm-suppress PossiblyUnusedMethod */
+ public function __construct()
+ {
+ parent::__construct(self::APP_ID);
+ }
- public function register(IRegistrationContext $context): void {}
+ public function register(IRegistrationContext $context): void
+ {
+ }
- public function boot(IBootContext $context): void {}
-}
\ No newline at end of file
+ public function boot(IBootContext $context): void
+ {
+ }
+}
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index e073127..04d00ae 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -12,50 +12,50 @@
class SettingsController extends Controller
{
- /** @var IL10N Translation */
- private $trans;
- /** @var IConfig Configuration object */
- private IConfig $config;
- private IAppConfig $appConfig;
+ /** @var IL10N Translation */
+ private $trans;
+ /** @var IConfig Configuration object */
+ private IConfig $config;
+ private IAppConfig $appConfig;
- /**
- * @param string $appName
- * @param IRequest $request
- * @param IL10N $trans
- * @param IConfig $config
- * @param IAppConfig $appConfig
- */
- public function __construct(string $appName,
- IRequest $request,
- IL10N $trans,
- IConfig $config,
- IAppConfig $appConfig)
- {
- parent::__construct($appName, $request);
- $this->trans = $trans;
- $this->config = $config;
- $this->appConfig = $appConfig;
- }
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param IL10N $trans
+ * @param IConfig $config
+ * @param IAppConfig $appConfig
+ */
+ public function __construct(string $appName,
+ IRequest $request,
+ IL10N $trans,
+ IConfig $config,
+ IAppConfig $appConfig)
+ {
+ parent::__construct($appName, $request);
+ $this->trans = $trans;
+ $this->config = $config;
+ $this->appConfig = $appConfig;
+ }
- /**
- * Set a configuration value in the privacyIDEA app config.
- *
- * @param string $key configuration key
- * @param string $value configuration value
- */
- public function setValue(string $key, string $value): void
- {
- $this->appConfig->setValueString("privacyidea", $key, $value);
- }
+ /**
+ * Set a configuration value in the privacyIDEA app config.
+ *
+ * @param string $key configuration key
+ * @param string $value configuration value
+ */
+ public function setValue(string $key, string $value): void
+ {
+ $this->appConfig->setValueString('privacyidea', $key, $value);
+ }
- /**
- * Retrieve a configuration from the privacyIDEA app config.
- *
- * @param string $key configuration key
- * @return string
- */
- public function getValue(string $key): string
- {
- return $this->appConfig->getValueString("privacyidea", $key);
- }
-}
\ No newline at end of file
+ /**
+ * Retrieve a configuration from the privacyIDEA app config.
+ *
+ * @param string $key configuration key
+ * @return string
+ */
+ public function getValue(string $key): string
+ {
+ return $this->appConfig->getValueString('privacyidea', $key);
+ }
+}
diff --git a/lib/PIClient/AuthenticationStatus.php b/lib/PIClient/AuthenticationStatus.php
index 1acd469..e207a0e 100644
--- a/lib/PIClient/AuthenticationStatus.php
+++ b/lib/PIClient/AuthenticationStatus.php
@@ -16,8 +16,8 @@
abstract class AuthenticationStatus
{
- const CHALLENGE = "CHALLENGE";
- const ACCEPT = "ACCEPT";
- const REJECT = "REJECT";
- const NONE = "NONE";
-}
\ No newline at end of file
+ public const CHALLENGE = 'CHALLENGE';
+ public const ACCEPT = 'ACCEPT';
+ public const REJECT = 'REJECT';
+ public const NONE = 'NONE';
+}
diff --git a/lib/PIClient/PIBadRequestException.php b/lib/PIClient/PIBadRequestException.php
index 4acab6e..f61d7b5 100644
--- a/lib/PIClient/PIBadRequestException.php
+++ b/lib/PIClient/PIBadRequestException.php
@@ -18,4 +18,4 @@
class PIBadRequestException extends Exception
{
-}
\ No newline at end of file
+}
diff --git a/lib/PIClient/PIChallenge.php b/lib/PIClient/PIChallenge.php
index c3cb5c4..94f0d94 100644
--- a/lib/PIClient/PIChallenge.php
+++ b/lib/PIClient/PIChallenge.php
@@ -16,27 +16,27 @@
class PIChallenge
{
- /* @var string Type of token this challenge is for. */
- public string $type = "";
+ /* @var string Type of token this challenge is for. */
+ public string $type = '';
- /* @var string Message extracted from this challenge. */
- public string $message = "";
+ /* @var string Message extracted from this challenge. */
+ public string $message = '';
- /* @var string Image data extracted from this challenge. */
- public string $image = "";
+ /* @var string Image data extracted from this challenge. */
+ public string $image = '';
- /* @var string TransactionId to reference this challenge in later requests. */
- public string $transactionID = "";
+ /* @var string TransactionId to reference this challenge in later requests. */
+ public string $transactionID = '';
- /* @var string Client mode in which the challenge should be processed. */
- public string $clientMode = "";
+ /* @var string Client mode in which the challenge should be processed. */
+ public string $clientMode = '';
- /* @var string Serial of token this challenge is for. */
- public string $serial = "";
+ /* @var string Serial of token this challenge is for. */
+ public string $serial = '';
- /* @var array Arbitrary attributes that can be appended to the challenge by the server. */
- public array $attributes = array();
+ /* @var array Arbitrary attributes that can be appended to the challenge by the server. */
+ public array $attributes = [];
- /* @var string WebAuthn sign request in JSON format */
- public string $webAuthnSignRequest = "";
-}
\ No newline at end of file
+ /* @var string WebAuthn sign request in JSON format */
+ public string $webAuthnSignRequest = '';
+}
diff --git a/lib/PIClient/PIResponse.php b/lib/PIClient/PIResponse.php
index 96f7c2d..a1a6668 100644
--- a/lib/PIClient/PIResponse.php
+++ b/lib/PIClient/PIResponse.php
@@ -16,344 +16,305 @@
class PIResponse
{
- /* @var string Combined messages of all triggered token. */
- private string $messages = "";
-
- /* @var string Message from the response. Should be shown to the user. */
- private string $message = "";
-
- /* @var string Transaction ID is used to reference the challenges contained in this response in later requests. */
- private string $transactionID = "";
-
- /* @var string Preferred mode in which client should work after triggering challenges. */
- private string $preferredClientMode = "";
-
- /* @var string Raw response in JSON format. */
- private string $raw = "";
-
- /* @var array Array of PIChallenge objects representing the triggered token challenges. */
- private array $multiChallenge = array();
-
- /* @var bool Status indicates if the request was processed successfully by the server. */
- private bool $status = false;
-
- /* @var bool Value is true if the authentication was successful. */
- private bool $value = false;
-
- /* @var string Authentication Status. */
- private string $authenticationStatus = "";
-
- /* @var string If an error occurred, the error code will be set here. */
- private string $errorCode = "";
-
- /* @var string If an error occurred, the error message will be set here. */
- private string $errorMessage = "";
-
- /**
- * Create a PIResponse object from the JSON response of the server.
- *
- * @param string $json Server response in JSON format.
- * @param PrivacyIDEA $privacyIDEA PrivacyIDEA object.
- * @return PIResponse|null Returns the PIResponse object or null if the response of the server is empty or malformed.
- */
- public static function fromJSON(string $json, PrivacyIDEA $privacyIDEA): ?PIResponse
- {
- assert('string' === gettype($json));
-
- if ($json == null || $json == "")
- {
- $privacyIDEA->log("error", "Response from the server is empty.");
- return null;
- }
-
- $ret = new PIResponse();
- $map = json_decode($json, true);
- if ($map == null)
- {
- $privacyIDEA->log("error", "Response from the server is malformed:\n" . $json);
- return null;
- }
- $ret->raw = $json;
-
- // If value is not present, an error occurred
- if (!isset($map['result']['value']))
- {
- $ret->errorCode = $map['result']['error']['code'];
- $ret->errorMessage = $map['result']['error']['message'];
- return $ret;
- }
-
- if (isset($map['detail']['messages']))
- {
- $ret->messages = implode(", ", array_unique($map['detail']['messages'])) ?: "";
- }
- if (isset($map['detail']['message']))
- {
- $ret->message = $map['detail']['message'];
- }
- if (isset($map['detail']['transaction_id']))
- {
- $ret->transactionID = $map['detail']['transaction_id'];
- }
- if (isset($map['detail']['preferred_client_mode']))
- {
- $pref = $map['detail']['preferred_client_mode'];
- if ($pref === "poll")
- {
- $ret->preferredClientMode = "push";
- }
- elseif ($pref === "interactive")
- {
- $ret->preferredClientMode = "otp";
- }
- else
- {
- $ret->preferredClientMode = $map['detail']['preferred_client_mode'];
- }
- }
-
- // Check if the authentication status is legit
- $r = null;
- if (!empty($map['result']['authentication']))
- {
- $r = $map['result']['authentication'];
- }
- if ($r === AuthenticationStatus::CHALLENGE)
- {
- $ret->authenticationStatus = AuthenticationStatus::CHALLENGE;
- }
- elseif ($r === AuthenticationStatus::ACCEPT)
- {
- $ret->authenticationStatus = AuthenticationStatus::ACCEPT;
- }
- elseif ($r === AuthenticationStatus::REJECT)
- {
- $ret->authenticationStatus = AuthenticationStatus::REJECT;
- }
- else
- {
- $privacyIDEA->log("debug", "Unknown authentication status.");
- $ret->authenticationStatus = AuthenticationStatus::NONE;
- }
- $ret->status = $map['result']['status'] ?: false;
- $ret->value = $map['result']['value'] ?: false;
-
- // Add any challenges to multiChallenge
- if (isset($map['detail']['multi_challenge']))
- {
- $mc = $map['detail']['multi_challenge'];
- foreach ($mc as $challenge)
- {
- $tmp = new PIChallenge();
- $tmp->transactionID = $challenge['transaction_id'];
- $tmp->message = $challenge['message'];
- $tmp->serial = $challenge['serial'];
- $tmp->type = $challenge['type'];
- if (isset($challenge['image']))
- {
- $tmp->image = $challenge['image'];
- }
- if (isset($challenge['attributes']))
- {
- $tmp->attributes = $challenge['attributes'];
- }
- if (isset($challenge['client_mode']))
- {
- $tmp->clientMode = $challenge['client_mode'];
- }
-
- if ($tmp->type === "webauthn")
- {
- $t = $challenge['attributes']['webAuthnSignRequest'];
- $tmp->webAuthnSignRequest = json_encode($t);
- }
-
- $ret->multiChallenge[] = $tmp;
- }
- }
- return $ret;
- }
-
- // Getters
-
- /**
- * Get an array with all triggered token types.
- * @return array
- */
- public function getTriggeredTokenTypes(): array
- {
- $ret = array();
- foreach ($this->multiChallenge as $challenge)
- {
- $ret[] = $challenge->type;
- }
- return array_unique($ret);
- }
-
- /**
- * Get the message of any token that is not Push or WebAuthn. Those are OTP tokens requiring an input field.
- * @return string
- */
- public function getOtpMessage(): string
- {
- foreach ($this->multiChallenge as $challenge)
- {
- if ($challenge->type !== "push" && $challenge->type !== "webauthn")
- {
- return $challenge->message;
- }
- }
- return "";
- }
-
- /**
- * Get the Push token message if any were triggered.
- * @return string
- */
- public function getPushMessage(): string
- {
- foreach ($this->multiChallenge as $challenge)
- {
- if ($challenge->type === "push")
- {
- return $challenge->message;
- }
- }
- return "";
- }
-
- /**
- * @return string Combined messages of all triggered token.
- */
- public function getMessages(): string
- {
- return $this->messages;
- }
-
- /**
- * @return string Message from the response. Should be shown to the user.
- */
- public function getMessage(): string
- {
- return $this->message;
- }
-
- /**
- * @return string Transaction ID is used to reference the challenges contained in this response in later requests.
- */
- public function getTransactionID(): string
- {
- return $this->transactionID;
- }
-
- /**
- * @return string Preferred mode in which client should work after triggering challenges.
- */
- public function getPreferredClientMode(): string
- {
- return $this->preferredClientMode;
- }
-
- /**
- * @return string Raw response in JSON format.
- */
- public function getRawResponse(): string
- {
- return $this->raw;
- }
-
- /**
- * Get the WebAuthnSignRequest for any triggered WebAuthn token.
- * @return string WebAuthnSignRequest or empty string if no WebAuthn token was triggered.
- */
- public function getWebauthnSignRequest(): string
- {
- $arr = [];
- $webauthn = "";
- foreach ($this->multiChallenge as $challenge)
- {
- if ($challenge->type === "webauthn")
- {
- $t = json_decode($challenge->webAuthnSignRequest);
- if (empty($webauthn))
- {
- $webauthn = $t;
- }
- $arr[] = $challenge->attributes['webAuthnSignRequest']['allowCredentials'][0];
- }
- }
- if (empty($webauthn))
- {
- return "";
- }
- else
- {
- $webauthn->allowCredentials = $arr;
- return json_encode($webauthn);
- }
- }
-
- /**
- * Get the WebAuthn token message if any were triggered.
- * @return string
- */
- public function getWebauthnMessage(): string
- {
- foreach ($this->multiChallenge as $challenge)
- {
- if ($challenge->type === "webauthn")
- {
- return $challenge->message;
- }
- }
- return "";
- }
-
- /**
- * @return array Array of PIChallenge objects representing the triggered token challenges.
- */
- public function getMultiChallenge(): array
- {
- return $this->multiChallenge;
- }
-
- /**
- * @return bool Status indicates if the request was processed successfully by the server.
- */
- public function getStatus(): bool
- {
- return $this->status;
- }
-
- /**
- * @return bool Value is true if the authentication was successful.
- */
- public function getValue(): bool
- {
- return $this->value;
- }
-
- /**
- * @return string Authentication Status.
- */
- public function getAuthenticationStatus(): string
- {
- return $this->authenticationStatus;
- }
-
- /**
- * @return string If an error occurred, the error code will be set here.
- */
- public function getErrorCode(): string
- {
- return $this->errorCode;
- }
-
- /**
- * @return string If an error occurred, the error message will be set here.
- */
- public function getErrorMessage(): string
- {
- return $this->errorMessage;
- }
-}
\ No newline at end of file
+ /* @var string Combined messages of all triggered token. */
+ private string $messages = '';
+
+ /* @var string Message from the response. Should be shown to the user. */
+ private string $message = '';
+
+ /* @var string Transaction ID is used to reference the challenges contained in this response in later requests. */
+ private string $transactionID = '';
+
+ /* @var string Preferred mode in which client should work after triggering challenges. */
+ private string $preferredClientMode = '';
+
+ /* @var string Raw response in JSON format. */
+ private string $raw = '';
+
+ /* @var array Array of PIChallenge objects representing the triggered token challenges. */
+ private array $multiChallenge = [];
+
+ /* @var bool Status indicates if the request was processed successfully by the server. */
+ private bool $status = false;
+
+ /* @var bool Value is true if the authentication was successful. */
+ private bool $value = false;
+
+ /* @var string Authentication Status. */
+ private string $authenticationStatus = '';
+
+ /* @var string If an error occurred, the error code will be set here. */
+ private string $errorCode = '';
+
+ /* @var string If an error occurred, the error message will be set here. */
+ private string $errorMessage = '';
+
+ /**
+ * Create a PIResponse object from the JSON response of the server.
+ *
+ * @param string $json Server response in JSON format.
+ * @param PrivacyIDEA $privacyIDEA PrivacyIDEA object.
+ * @return PIResponse|null Returns the PIResponse object or null if the response of the server is empty or malformed.
+ */
+ public static function fromJSON(string $json, PrivacyIDEA $privacyIDEA): ?PIResponse
+ {
+ assert(gettype($json) === 'string');
+
+ if ($json == null || $json == '') {
+ $privacyIDEA->log('error', 'Response from the server is empty.');
+ return null;
+ }
+
+ $ret = new PIResponse();
+ $map = json_decode($json, true);
+ if ($map == null) {
+ $privacyIDEA->log('error', "Response from the server is malformed:\n" . $json);
+ return null;
+ }
+ $ret->raw = $json;
+
+ // If value is not present, an error occurred
+ if (!isset($map['result']['value'])) {
+ $ret->errorCode = $map['result']['error']['code'];
+ $ret->errorMessage = $map['result']['error']['message'];
+ return $ret;
+ }
+
+ if (isset($map['detail']['messages'])) {
+ $ret->messages = implode(', ', array_unique($map['detail']['messages'])) ?: '';
+ }
+ if (isset($map['detail']['message'])) {
+ $ret->message = $map['detail']['message'];
+ }
+ if (isset($map['detail']['transaction_id'])) {
+ $ret->transactionID = $map['detail']['transaction_id'];
+ }
+ if (isset($map['detail']['preferred_client_mode'])) {
+ $pref = $map['detail']['preferred_client_mode'];
+ if ($pref === 'poll') {
+ $ret->preferredClientMode = 'push';
+ } elseif ($pref === 'interactive') {
+ $ret->preferredClientMode = 'otp';
+ } else {
+ $ret->preferredClientMode = $map['detail']['preferred_client_mode'];
+ }
+ }
+
+ // Check if the authentication status is legit
+ $r = null;
+ if (!empty($map['result']['authentication'])) {
+ $r = $map['result']['authentication'];
+ }
+ if ($r === AuthenticationStatus::CHALLENGE) {
+ $ret->authenticationStatus = AuthenticationStatus::CHALLENGE;
+ } elseif ($r === AuthenticationStatus::ACCEPT) {
+ $ret->authenticationStatus = AuthenticationStatus::ACCEPT;
+ } elseif ($r === AuthenticationStatus::REJECT) {
+ $ret->authenticationStatus = AuthenticationStatus::REJECT;
+ } else {
+ $privacyIDEA->log('debug', 'Unknown authentication status.');
+ $ret->authenticationStatus = AuthenticationStatus::NONE;
+ }
+ $ret->status = $map['result']['status'] ?: false;
+ $ret->value = $map['result']['value'] ?: false;
+
+ // Add any challenges to multiChallenge
+ if (isset($map['detail']['multi_challenge'])) {
+ $mc = $map['detail']['multi_challenge'];
+ foreach ($mc as $challenge) {
+ $tmp = new PIChallenge();
+ $tmp->transactionID = $challenge['transaction_id'];
+ $tmp->message = $challenge['message'];
+ $tmp->serial = $challenge['serial'];
+ $tmp->type = $challenge['type'];
+ if (isset($challenge['image'])) {
+ $tmp->image = $challenge['image'];
+ }
+ if (isset($challenge['attributes'])) {
+ $tmp->attributes = $challenge['attributes'];
+ }
+ if (isset($challenge['client_mode'])) {
+ $tmp->clientMode = $challenge['client_mode'];
+ }
+
+ if ($tmp->type === 'webauthn') {
+ $t = $challenge['attributes']['webAuthnSignRequest'];
+ $tmp->webAuthnSignRequest = json_encode($t);
+ }
+
+ $ret->multiChallenge[] = $tmp;
+ }
+ }
+ return $ret;
+ }
+
+ // Getters
+
+ /**
+ * Get an array with all triggered token types.
+ * @return array
+ */
+ public function getTriggeredTokenTypes(): array
+ {
+ $ret = [];
+ foreach ($this->multiChallenge as $challenge) {
+ $ret[] = $challenge->type;
+ }
+ return array_unique($ret);
+ }
+
+ /**
+ * Get the message of any token that is not Push or WebAuthn. Those are OTP tokens requiring an input field.
+ * @return string
+ */
+ public function getOtpMessage(): string
+ {
+ foreach ($this->multiChallenge as $challenge) {
+ if ($challenge->type !== 'push' && $challenge->type !== 'webauthn') {
+ return $challenge->message;
+ }
+ }
+ return '';
+ }
+
+ /**
+ * Get the Push token message if any were triggered.
+ * @return string
+ */
+ public function getPushMessage(): string
+ {
+ foreach ($this->multiChallenge as $challenge) {
+ if ($challenge->type === 'push') {
+ return $challenge->message;
+ }
+ }
+ return '';
+ }
+
+ /**
+ * @return string Combined messages of all triggered token.
+ */
+ public function getMessages(): string
+ {
+ return $this->messages;
+ }
+
+ /**
+ * @return string Message from the response. Should be shown to the user.
+ */
+ public function getMessage(): string
+ {
+ return $this->message;
+ }
+
+ /**
+ * @return string Transaction ID is used to reference the challenges contained in this response in later requests.
+ */
+ public function getTransactionID(): string
+ {
+ return $this->transactionID;
+ }
+
+ /**
+ * @return string Preferred mode in which client should work after triggering challenges.
+ */
+ public function getPreferredClientMode(): string
+ {
+ return $this->preferredClientMode;
+ }
+
+ /**
+ * @return string Raw response in JSON format.
+ */
+ public function getRawResponse(): string
+ {
+ return $this->raw;
+ }
+
+ /**
+ * Get the WebAuthnSignRequest for any triggered WebAuthn token.
+ * @return string WebAuthnSignRequest or empty string if no WebAuthn token was triggered.
+ */
+ public function getWebauthnSignRequest(): string
+ {
+ $arr = [];
+ $webauthn = '';
+ foreach ($this->multiChallenge as $challenge) {
+ if ($challenge->type === 'webauthn') {
+ $t = json_decode($challenge->webAuthnSignRequest);
+ if (empty($webauthn)) {
+ $webauthn = $t;
+ }
+ $arr[] = $challenge->attributes['webAuthnSignRequest']['allowCredentials'][0];
+ }
+ }
+ if (empty($webauthn)) {
+ return '';
+ } else {
+ $webauthn->allowCredentials = $arr;
+ return json_encode($webauthn);
+ }
+ }
+
+ /**
+ * Get the WebAuthn token message if any were triggered.
+ * @return string
+ */
+ public function getWebauthnMessage(): string
+ {
+ foreach ($this->multiChallenge as $challenge) {
+ if ($challenge->type === 'webauthn') {
+ return $challenge->message;
+ }
+ }
+ return '';
+ }
+
+ /**
+ * @return array Array of PIChallenge objects representing the triggered token challenges.
+ */
+ public function getMultiChallenge(): array
+ {
+ return $this->multiChallenge;
+ }
+
+ /**
+ * @return bool Status indicates if the request was processed successfully by the server.
+ */
+ public function getStatus(): bool
+ {
+ return $this->status;
+ }
+
+ /**
+ * @return bool Value is true if the authentication was successful.
+ */
+ public function getValue(): bool
+ {
+ return $this->value;
+ }
+
+ /**
+ * @return string Authentication Status.
+ */
+ public function getAuthenticationStatus(): string
+ {
+ return $this->authenticationStatus;
+ }
+
+ /**
+ * @return string If an error occurred, the error code will be set here.
+ */
+ public function getErrorCode(): string
+ {
+ return $this->errorCode;
+ }
+
+ /**
+ * @return string If an error occurred, the error message will be set here.
+ */
+ public function getErrorMessage(): string
+ {
+ return $this->errorMessage;
+ }
+}
diff --git a/lib/PIClient/PrivacyIDEA.php b/lib/PIClient/PrivacyIDEA.php
index 8f6bc87..3f4e740 100644
--- a/lib/PIClient/PrivacyIDEA.php
+++ b/lib/PIClient/PrivacyIDEA.php
@@ -17,12 +17,12 @@
use RecursiveArrayIterator;
use RecursiveIteratorIterator;
-const AUTHENTICATORDATA = "authenticatordata";
-const CLIENTDATA = "clientdata";
-const SIGNATUREDATA = "signaturedata";
-const CREDENTIALID = "credentialid";
-const USERHANDLE = "userhandle";
-const ASSERTIONCLIENTEXTENSIONS = "assertionclientextensions";
+const AUTHENTICATORDATA = 'authenticatordata';
+const CLIENTDATA = 'clientdata';
+const SIGNATUREDATA = 'signaturedata';
+const CREDENTIALID = 'credentialid';
+const USERHANDLE = 'userhandle';
+const ASSERTIONCLIENTEXTENSIONS = 'assertionclientextensions';
/**
* PHP client to aid develop plugins for the privacyIDEA authentication server.
@@ -32,551 +32,485 @@
*/
class PrivacyIDEA
{
- /* @var string User agent name which should be forwarded to the privacyIDEA server. */
- private string $userAgent;
-
- /* @var string URL of the privacyIDEA server. */
- private string $serverURL;
-
- /* @var string User's realm. */
- private string $realm = "";
-
- /* @var bool Disable host verification for SSL. */
- private bool $sslVerifyHost = true;
-
- /* @var bool Disable peer verification for SSL. */
- private bool $sslVerifyPeer = true;
-
- /* @var string Account name for privacyIDEA service account. Required to use the /validate/triggerchallenge endpoint. */
- private string $serviceAccountName = "";
-
- /* @var string Password for privacyIDEA service account. Required to use the /validate/triggerchallenge endpoint. */
- private string $serviceAccountPass = "";
-
- /* @var string Realm for privacyIDEA service account. Optional to use the /validate/triggerchallenge endpoint. */
- private string $serviceAccountRealm = "";
-
- /* @var bool Send the "client" parameter to allow using the original IP address in the privacyIDEA policies. */
- private bool $forwardClientIP = false;
-
- /* @var string Timeout for the request. */
- private string $timeout = "5";
-
- /* @var bool Ignore the system-wide proxy settings and send the authentication requests directly to privacyIDEA. */
- private bool $noProxy = false;
-
- /* @var object|null Implementation of the PILog interface. */
- private ?object $logger = null;
-
- /**
- * PrivacyIDEA constructor.
- * @param $userAgent string User agent.
- * @param $serverURL string privacyIDEA server URL.
- */
- public function __construct(string $userAgent, string $serverURL)
- {
- $this->userAgent = $userAgent;
- $this->serverURL = $serverURL;
- }
-
- /**
- * Try to authenticate the user by the /validate/check endpoint.
- *
- * @param string $username Username to authenticate.
- * @param string $pass This can be the OTP, but also the PIN to trigger a token or PIN+OTP depending on the configuration of the server.
- * @param string|null $transactionID Optional transaction ID. Used to reference a challenge that was triggered beforehand.
- * @param array|null $headers Optional headers to forward to the server.
- * @return PIResponse|null Returns PIResponse object or null if response was empty or malformed, or some parameter is missing.
- * @throws PIBadRequestException If an error occurs during the request.
- */
- public function validateCheck(string $username, string $pass, string $transactionID = null, array $headers = null): ?PIResponse
- {
- assert('string' === gettype($username));
- assert('string' === gettype($pass));
-
- if (!empty($username))
- {
- $params["user"] = $username;
- $params["pass"] = $pass;
- if (!empty($transactionID))
- {
- // Add transaction ID in case of challenge response
- $params["transaction_id"] = $transactionID;
- }
- if (empty($headers))
- {
- $headers = array('');
- }
- if (!empty($this->realm))
- {
- $params["realm"] = $this->realm;
- }
-
- $response = $this->sendRequest($params, $headers, 'POST', '/validate/check');
-
- $ret = PIResponse::fromJSON($response, $this);
- if ($ret == null)
- {
- $this->log("debug", "Server did not respond.");
- }
- return $ret;
- }
- else
- {
- $this->log("debug", "Missing username for /validate/check.");
- }
- return null;
- }
-
- /**
- * Trigger all challenges for the given username.
- * This function requires a service account to be set.
- *
- * @param string $username Username for which the challenges should be triggered.
- * @param array|null $headers Optional headers to forward to the server.
- * @return PIResponse|null Returns PIResponse object or null if response was empty or malformed, or some parameter is missing.
- * @throws PIBadRequestException If an error occurs during the request.
- */
- public function triggerChallenge(string $username, array $headers = null): ?PIResponse
- {
- assert('string' === gettype($username));
-
- if ($username)
- {
- $authToken = $this->getAuthToken();
- $authTokenHeader = array("authorization:" . $authToken);
-
- $params = array("user" => $username);
-
- if (!empty($this->realm))
- {
- $params["realm"] = $this->realm;
- }
-
- if (!empty($headers))
- {
- $headers = array_merge($headers, $authTokenHeader);
- }
- else
- {
- $headers = $authTokenHeader;
- }
-
- $response = $this->sendRequest($params, $headers, 'POST', '/validate/triggerchallenge');
-
- return PIResponse::fromJSON($response, $this);
- }
- else
- {
- $this->log("debug", "Username missing!");
- }
- return null;
- }
-
- /**
- * Poll for the transaction status.
- *
- * @param $transactionID string Transaction ID of the triggered challenge.
- * @param array|null $headers Optional headers to forward to the server.
- * @return bool True if the push request has been accepted, false otherwise.
- * @throws PIBadRequestException If an error occurs during the request.
- */
- public function pollTransaction(string $transactionID, array $headers = null): bool
- {
- assert('string' === gettype($transactionID));
-
- if (!empty($transactionID))
- {
- $params = array("transaction_id" => $transactionID);
- if (empty($headers))
- {
- $headers = array('');
- }
-
- $responseJSON = $this->sendRequest($params, $headers, 'GET', '/validate/polltransaction');
- $response = json_decode($responseJSON, true);
- return $response['result']['value'];
- }
- else
- {
- $this->log("debug", "TransactionID missing!");
- }
- return false;
- }
-
- /**
- * Send request to /validate/check endpoint with the data required to authenticate using WebAuthn token.
- *
- * @param string $username Username to authenticate.
- * @param string $transactionID Transaction ID of the triggered challenge.
- * @param string $webAuthnSignResponse WebAuthn sign response.
- * @param string $origin Origin required to authenticate using WebAuthn token.
- * @param array|null $headers Optional headers to forward to the server.
- * @return PIResponse|null Returns PIResponse object or null if response was empty or malformed, or some parameter is missing.
- * @throws PIBadRequestException If an error occurs during the request.
- */
- public function validateCheckWebAuthn(string $username, string $transactionID, string $webAuthnSignResponse, string $origin, array $headers = null): ?PIResponse
- {
- assert('string' === gettype($username));
- assert('string' === gettype($transactionID));
- assert('string' === gettype($webAuthnSignResponse));
- assert('string' === gettype($origin));
-
- if (!empty($username) && !empty($transactionID) && !empty($webAuthnSignResponse) && !empty($origin))
- {
- // Compose standard validate/check params
- $params["user"] = $username;
- $params["pass"] = "";
- $params["transaction_id"] = $transactionID;
-
- if (!empty($this->realm))
- {
- $params["realm"] = $this->realm;
- }
-
- // Additional WebAuthn params
- $tmp = json_decode($webAuthnSignResponse, true);
-
- $params[CREDENTIALID] = $tmp[CREDENTIALID];
- $params[CLIENTDATA] = $tmp[CLIENTDATA];
- $params[SIGNATUREDATA] = $tmp[SIGNATUREDATA];
- $params[AUTHENTICATORDATA] = $tmp[AUTHENTICATORDATA];
-
- if (!empty($tmp[USERHANDLE]))
- {
- $params[USERHANDLE] = $tmp[USERHANDLE];
- }
- if (!empty($tmp[ASSERTIONCLIENTEXTENSIONS]))
- {
- $params[ASSERTIONCLIENTEXTENSIONS] = $tmp[ASSERTIONCLIENTEXTENSIONS];
- }
-
- $originHeader = array("Origin:" . $origin);
- if (!empty($headers))
- {
- $headers = array_merge($headers, $originHeader);
- }
- else
- {
- $headers = $originHeader;
- }
-
- $response = $this->sendRequest($params, $headers, 'POST', '/validate/check');
-
- return PIResponse::fromJSON($response, $this);
- }
- else
- {
- // Handle debug message if $username is empty
- $this->log("debug", "validateCheckWebAuthn: parameters are incomplete!");
- }
- return null;
- }
-
- /**
- * Check if name and pass of service account are set.
- * @return bool
- */
- public function serviceAccountAvailable(): bool
- {
- return (!empty($this->serviceAccountName) && !empty($this->serviceAccountPass));
- }
-
- /**
- * Retrieves the auth token from the server using the service account. An auth token is required for some requests to the privacyIDEA.
- *
- * @return string Auth token or empty string if the response did not contain a token or no service account is configured.
- * @throws PIBadRequestException If an error occurs during the request.
- */
- public function getAuthToken(): string
- {
- if (!$this->serviceAccountAvailable())
- {
- $this->log("error", "Cannot retrieve auth token without service account!");
- return "";
- }
-
- $params = array("username" => $this->serviceAccountName, "password" => $this->serviceAccountPass);
- if ($this->serviceAccountRealm != null && $this->serviceAccountRealm != "")
- {
- $params["realm"] = $this->serviceAccountRealm;
- }
-
- $response = json_decode($this->sendRequest($params, array(''), 'POST', '/auth'), true);
-
- if (!empty($response['result']['value']))
- {
- // Ensure an admin account
- if (!empty($response['result']['value']['token']))
- {
- if ($this->findRecursive($response, 'role') != 'admin')
- {
- $this->log("debug", "Auth token was of a user without admin role.");
- return "";
- }
- return $response['result']['value']['token'];
- }
- }
- $this->log("debug", "/auth response did not contain the auth token.");
- return "";
- }
-
- /**
- * Find key recursively in array.
- *
- * @param array $haystack The array which will be searched.
- * @param string $needle Search string.
- * @return mixed Result of key search.
- */
- public function findRecursive(array $haystack, string $needle): mixed
- {
- assert(is_array($haystack));
- assert(is_string($needle));
-
- $iterator = new RecursiveArrayIterator($haystack);
- $recursive = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST);
-
- foreach ($recursive as $key => $value)
- {
- if ($key === $needle)
- {
- return $value;
- }
- }
- return false;
- }
-
- /**
- * Send requests to the endpoint with specified parameters and headers.
- *
- * @param $params array Request parameters.
- * @param $headers array Headers to forward.
- * @param $httpMethod string GET or POST.
- * @param $endpoint string Endpoint of the privacyIDEA API (e.g. /validate/check).
- * @return string Returns a string with the server response.
- * @throws PIBadRequestException If an error occurs.
- */
- private function sendRequest(array $params, array $headers, string $httpMethod, string $endpoint): string
- {
- assert('array' === gettype($params));
- assert('array' === gettype($headers));
- assert('string' === gettype($httpMethod));
- assert('string' === gettype($endpoint));
-
- // Add the client parameter if wished.
- if ($this->forwardClientIP === true)
- {
- $serverHeaders = $_SERVER;
- foreach (array("X-Forwarded-For", "HTTP_X_FORWARDED_FOR", "REMOTE_ADDR") as $clientKey)
- {
- if (array_key_exists($clientKey, $serverHeaders))
- {
- $clientIP = $serverHeaders[$clientKey];
- $this->log("debug", "Forwarding Client IP: " . $clientKey . ": " . $clientIP);
- $params['client'] = $clientIP;
- break;
- }
- }
- }
-
- // Ignore proxy settings if wished.
- if ($this->noProxy === true)
- {
- $this->log("debug", "Ignoring proxy settings.");
- $params["proxy"] = ["https" => "", "http" => ""];
- }
-
- // Set request's timeout
- $params["timeout"] = $this->timeout;
-
- $this->log("debug", "Sending " . http_build_query($params, '', ', ') . " to " . $endpoint);
-
- $completeUrl = $this->serverURL . $endpoint;
-
- $curlInstance = curl_init();
- curl_setopt($curlInstance, CURLOPT_URL, $completeUrl);
- curl_setopt($curlInstance, CURLOPT_HEADER, true);
- if ($headers)
- {
- curl_setopt($curlInstance, CURLOPT_HTTPHEADER, $headers);
- }
- curl_setopt($curlInstance, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($curlInstance, CURLOPT_USERAGENT, $this->userAgent);
- if ($httpMethod === "POST")
- {
- curl_setopt($curlInstance, CURLOPT_POST, true);
- curl_setopt($curlInstance, CURLOPT_POSTFIELDS, $params);
- }
- elseif ($httpMethod === "PUT")
- {
- curl_setopt($curlInstance, CURLOPT_CUSTOMREQUEST, "PUT");
- curl_setopt($curlInstance, CURLOPT_POSTFIELDS, $params);
- }
- elseif ($httpMethod === "DELETE")
- {
- curl_setopt($curlInstance, CURLOPT_CUSTOMREQUEST, "DELETE");
- curl_setopt($curlInstance, CURLOPT_POSTFIELDS, $params);
- }
- elseif ($httpMethod === "GET")
- {
- $paramsStr = '?';
- if (!empty($params))
- {
- foreach ($params as $key => $value)
- {
- $paramsStr .= $key . "=" . $value . "&";
- }
- }
- curl_setopt($curlInstance, CURLOPT_URL, $completeUrl . $paramsStr);
- }
-
- // Disable host and/or peer verification for SSL if configured.
- if ($this->sslVerifyHost === true)
- {
- curl_setopt($curlInstance, CURLOPT_SSL_VERIFYHOST, 2);
- }
- else
- {
- curl_setopt($curlInstance, CURLOPT_SSL_VERIFYHOST, 0);
- }
-
- if ($this->sslVerifyPeer === true)
- {
- curl_setopt($curlInstance, CURLOPT_SSL_VERIFYPEER, 2);
- }
- else
- {
- curl_setopt($curlInstance, CURLOPT_SSL_VERIFYPEER, 0);
- }
-
- $response = curl_exec($curlInstance);
-
- if (!$response)
- {
- // Handle the error
- $curlErrno = curl_errno($curlInstance);
- $this->log("error", "Bad request: " . curl_error($curlInstance) . " errno: " . $curlErrno);
- throw new PIBadRequestException("Unable to reach the authentication server (" . $curlErrno . ")");
- }
-
- $headerSize = curl_getinfo($curlInstance, CURLINFO_HEADER_SIZE);
- $ret = substr($response, $headerSize);
- curl_close($curlInstance);
-
- // Log the response
- if ($endpoint != "/auth" && $this->logger != null)
- {
- $retJson = json_decode($ret, true);
- $this->log("debug", $endpoint . " returned " . json_encode($retJson, JSON_PRETTY_PRINT));
- }
-
- // Return decoded response
- return $ret;
- }
-
- /**
- * Log a message with the given log level.
- *
- * @param $level
- * @param $message
- */
- function log($level, $message): void
- {
- $context = ["app" => "privacyIDEA"];
- if ($level === 'debug')
- {
- $this->logger->debug($message, $context);
- }
- if ($level === 'info')
- {
- $this->logger->info($message, $context);
- }
- if ($level === 'error')
- {
- $this->logger->error($message, $context);
- }
- }
-
- // Setters
-
- /**
- * @param string $realm User's realm.
- * @return void
- */
- public function setRealm(string $realm): void
- {
- $this->realm = $realm;
- }
-
- /**
- * @param bool $sslVerifyHost Disable host verification for SSL.
- * @return void
- */
- public function setSSLVerifyHost(bool $sslVerifyHost): void
- {
- $this->sslVerifyHost = $sslVerifyHost;
- }
-
- /**
- * @param bool $sslVerifyPeer Disable peer verification for SSL.
- * @return void
- */
- public function setSSLVerifyPeer(bool $sslVerifyPeer): void
- {
- $this->sslVerifyPeer = $sslVerifyPeer;
- }
-
- /**
- * @param string $serviceAccountName Account name for privacyIDEA service account. Required to use the /validate/triggerchallenge endpoint.
- * @return void
- */
- public function setServiceAccountName(string $serviceAccountName): void
- {
- $this->serviceAccountName = $serviceAccountName;
- }
-
- /**
- * @param string $serviceAccountPass Password for privacyIDEA service account. Required to use the /validate/triggerchallenge endpoint.
- * @return void
- */
- public function setServiceAccountPass(string $serviceAccountPass): void
- {
- $this->serviceAccountPass = $serviceAccountPass;
- }
-
- /**
- * @param string $serviceAccountRealm Realm for privacyIDEA service account. Optional to use the /validate/triggerchallenge endpoint.
- * @return void
- */
- public function setServiceAccountRealm(string $serviceAccountRealm): void
- {
- $this->serviceAccountRealm = $serviceAccountRealm;
- }
-
- /**
- * @param bool $forwardClientIP Send the "client" parameter to allow using the original IP address in the privacyIDEA policies.
- * @return void
- */
- public function setForwardClientIP(bool $forwardClientIP): void
- {
- $this->forwardClientIP = $forwardClientIP;
- }
-
- /**
- * @param bool $noProxy Ignore the system-wide proxy settings and send the authentication requests directly to privacyIDEA.
- * @return void
- */
- public function setNoProxy(bool $noProxy): void
- {
- $this->$noProxy = $noProxy;
- }
-
- /**
- * @param object|null $logger Implementation of the PILog interface.
- * @return void
- */
- public function setLogger(?object $logger): void
- {
- $this->logger = $logger;
- }
-}
\ No newline at end of file
+ /* @var string User agent name which should be forwarded to the privacyIDEA server. */
+ private string $userAgent;
+
+ /* @var string URL of the privacyIDEA server. */
+ private string $serverURL;
+
+ /* @var string User's realm. */
+ private string $realm = '';
+
+ /* @var bool Disable host verification for SSL. */
+ private bool $sslVerifyHost = true;
+
+ /* @var bool Disable peer verification for SSL. */
+ private bool $sslVerifyPeer = true;
+
+ /* @var string Account name for privacyIDEA service account. Required to use the /validate/triggerchallenge endpoint. */
+ private string $serviceAccountName = '';
+
+ /* @var string Password for privacyIDEA service account. Required to use the /validate/triggerchallenge endpoint. */
+ private string $serviceAccountPass = '';
+
+ /* @var string Realm for privacyIDEA service account. Optional to use the /validate/triggerchallenge endpoint. */
+ private string $serviceAccountRealm = '';
+
+ /* @var string Send the "client" parameter to allow using the original IP address in the privacyIDEA policies. */
+ private string $forwardClientIP = '';
+
+ /* @var string Timeout for the request. */
+ private string $timeout = '5';
+
+ /* @var bool Ignore the system-wide proxy settings and send the authentication requests directly to privacyIDEA. */
+ private bool $noProxy = false;
+
+ /* @var object|null Implementation of the PILog interface. */
+ private ?object $logger = null;
+
+ /**
+ * PrivacyIDEA constructor.
+ * @param $userAgent string User agent.
+ * @param $serverURL string privacyIDEA server URL.
+ */
+ public function __construct(string $userAgent, string $serverURL)
+ {
+ $this->userAgent = $userAgent;
+ $this->serverURL = $serverURL;
+ }
+
+ /**
+ * Try to authenticate the user by the /validate/check endpoint.
+ *
+ * @param string $username Username to authenticate.
+ * @param string $pass This can be the OTP, but also the PIN to trigger a token or PIN+OTP depending on the configuration of the server.
+ * @param string|null $transactionID Optional transaction ID. Used to reference a challenge that was triggered beforehand.
+ * @param array|null $headers Optional headers to forward to the server.
+ * @return PIResponse|null Returns PIResponse object or null if response was empty or malformed, or some parameter is missing.
+ * @throws PIBadRequestException If an error occurs during the request.
+ */
+ public function validateCheck(string $username, string $pass, ?string $transactionID = null, ?array $headers = null): ?PIResponse
+ {
+ assert(gettype($username) === 'string');
+ assert(gettype($pass) === 'string');
+
+ if (!empty($username)) {
+ $params['user'] = $username;
+ $params['pass'] = $pass;
+ if (!empty($transactionID)) {
+ // Add transaction ID in case of challenge response
+ $params['transaction_id'] = $transactionID;
+ }
+ if (empty($headers)) {
+ $headers = [''];
+ }
+ if (!empty($this->realm)) {
+ $params['realm'] = $this->realm;
+ }
+
+ $response = $this->sendRequest($params, $headers, 'POST', '/validate/check');
+
+ $ret = PIResponse::fromJSON($response, $this);
+ if ($ret == null) {
+ $this->log('debug', 'Server did not respond.');
+ }
+ return $ret;
+ } else {
+ $this->log('debug', 'Missing username for /validate/check.');
+ }
+ return null;
+ }
+
+ /**
+ * Trigger all challenges for the given username.
+ * This function requires a service account to be set.
+ *
+ * @param string $username Username for which the challenges should be triggered.
+ * @param array|null $headers Optional headers to forward to the server.
+ * @return PIResponse|null Returns PIResponse object or null if response was empty or malformed, or some parameter is missing.
+ * @throws PIBadRequestException If an error occurs during the request.
+ */
+ public function triggerChallenge(string $username, ?array $headers = null): ?PIResponse
+ {
+ assert(gettype($username) === 'string');
+
+ if ($username) {
+ $authToken = $this->getAuthToken();
+ $authTokenHeader = ['authorization:' . $authToken];
+
+ $params = ['user' => $username];
+
+ if (!empty($this->realm)) {
+ $params['realm'] = $this->realm;
+ }
+
+ if (!empty($headers)) {
+ $headers = array_merge($headers, $authTokenHeader);
+ } else {
+ $headers = $authTokenHeader;
+ }
+
+ $response = $this->sendRequest($params, $headers, 'POST', '/validate/triggerchallenge');
+
+ return PIResponse::fromJSON($response, $this);
+ } else {
+ $this->log('debug', 'Username missing!');
+ }
+ return null;
+ }
+
+ /**
+ * Poll for the transaction status.
+ *
+ * @param $transactionID string Transaction ID of the triggered challenge.
+ * @param array|null $headers Optional headers to forward to the server.
+ * @return bool True if the push request has been accepted, false otherwise.
+ * @throws PIBadRequestException If an error occurs during the request.
+ */
+ public function pollTransaction(string $transactionID, ?array $headers = null): bool
+ {
+ assert(gettype($transactionID) === 'string');
+
+ if (!empty($transactionID)) {
+ $params = ['transaction_id' => $transactionID];
+ if (empty($headers)) {
+ $headers = [''];
+ }
+
+ $responseJSON = $this->sendRequest($params, $headers, 'GET', '/validate/polltransaction');
+ $response = json_decode($responseJSON, true);
+ return $response['result']['value'];
+ } else {
+ $this->log('debug', 'TransactionID missing!');
+ }
+ return false;
+ }
+
+ /**
+ * Send request to /validate/check endpoint with the data required to authenticate using WebAuthn token.
+ *
+ * @param string $username Username to authenticate.
+ * @param string $transactionID Transaction ID of the triggered challenge.
+ * @param string $webAuthnSignResponse WebAuthn sign response.
+ * @param string $origin Origin required to authenticate using WebAuthn token.
+ * @param array|null $headers Optional headers to forward to the server.
+ * @return PIResponse|null Returns PIResponse object or null if response was empty or malformed, or some parameter is missing.
+ * @throws PIBadRequestException If an error occurs during the request.
+ */
+ public function validateCheckWebAuthn(string $username, string $transactionID, string $webAuthnSignResponse, string $origin, ?array $headers = null): ?PIResponse
+ {
+ assert(gettype($username) === 'string');
+ assert(gettype($transactionID) === 'string');
+ assert(gettype($webAuthnSignResponse) === 'string');
+ assert(gettype($origin) === 'string');
+
+ if (!empty($username) && !empty($transactionID) && !empty($webAuthnSignResponse) && !empty($origin)) {
+ // Compose standard validate/check params
+ $params['user'] = $username;
+ $params['pass'] = '';
+ $params['transaction_id'] = $transactionID;
+
+ if (!empty($this->realm)) {
+ $params['realm'] = $this->realm;
+ }
+
+ // Additional WebAuthn params
+ $tmp = json_decode($webAuthnSignResponse, true);
+
+ $params[CREDENTIALID] = $tmp[CREDENTIALID];
+ $params[CLIENTDATA] = $tmp[CLIENTDATA];
+ $params[SIGNATUREDATA] = $tmp[SIGNATUREDATA];
+ $params[AUTHENTICATORDATA] = $tmp[AUTHENTICATORDATA];
+
+ if (!empty($tmp[USERHANDLE])) {
+ $params[USERHANDLE] = $tmp[USERHANDLE];
+ }
+ if (!empty($tmp[ASSERTIONCLIENTEXTENSIONS])) {
+ $params[ASSERTIONCLIENTEXTENSIONS] = $tmp[ASSERTIONCLIENTEXTENSIONS];
+ }
+
+ $originHeader = ['Origin:' . $origin];
+ if (!empty($headers)) {
+ $headers = array_merge($headers, $originHeader);
+ } else {
+ $headers = $originHeader;
+ }
+
+ $response = $this->sendRequest($params, $headers, 'POST', '/validate/check');
+
+ return PIResponse::fromJSON($response, $this);
+ } else {
+ // Handle debug message if $username is empty
+ $this->log('debug', 'validateCheckWebAuthn: parameters are incomplete!');
+ }
+ return null;
+ }
+
+ /**
+ * Check if name and pass of service account are set.
+ * @return bool
+ */
+ public function serviceAccountAvailable(): bool
+ {
+ return (!empty($this->serviceAccountName) && !empty($this->serviceAccountPass));
+ }
+
+ /**
+ * Retrieves the auth token from the server using the service account. An auth token is required for some requests to the privacyIDEA.
+ *
+ * @return string Auth token or empty string if the response did not contain a token or no service account is configured.
+ * @throws PIBadRequestException If an error occurs during the request.
+ */
+ public function getAuthToken(): string
+ {
+ if (!$this->serviceAccountAvailable()) {
+ $this->log('error', 'Cannot retrieve auth token without service account!');
+ return '';
+ }
+
+ $params = ['username' => $this->serviceAccountName, 'password' => $this->serviceAccountPass];
+ if ($this->serviceAccountRealm != null && $this->serviceAccountRealm != '') {
+ $params['realm'] = $this->serviceAccountRealm;
+ }
+
+ $response = json_decode($this->sendRequest($params, [''], 'POST', '/auth'), true);
+
+ if (!empty($response['result']['value'])) {
+ // Ensure an admin account
+ if (!empty($response['result']['value']['token'])) {
+ if ($this->findRecursive($response, 'role') != 'admin') {
+ $this->log('debug', 'Auth token was of a user without admin role.');
+ return '';
+ }
+ return $response['result']['value']['token'];
+ }
+ }
+ $this->log('debug', '/auth response did not contain the auth token.');
+ return '';
+ }
+
+ /**
+ * Find key recursively in array.
+ *
+ * @param array $haystack The array which will be searched.
+ * @param string $needle Search string.
+ * @return mixed Result of key search.
+ */
+ public function findRecursive(array $haystack, string $needle): mixed
+ {
+ assert(is_array($haystack));
+ assert(is_string($needle));
+
+ $iterator = new RecursiveArrayIterator($haystack);
+ $recursive = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST);
+
+ foreach ($recursive as $key => $value) {
+ if ($key === $needle) {
+ return $value;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Send requests to the endpoint with specified parameters and headers.
+ *
+ * @param $params array Request parameters.
+ * @param $headers array Headers to forward.
+ * @param $httpMethod string GET or POST.
+ * @param $endpoint string Endpoint of the privacyIDEA API (e.g. /validate/check).
+ * @return string Returns a string with the server response.
+ * @throws PIBadRequestException If an error occurs.
+ */
+ private function sendRequest(array $params, array $headers, string $httpMethod, string $endpoint): string
+ {
+ assert(gettype($params) === 'array');
+ assert(gettype($headers) === 'array');
+ assert(gettype($httpMethod) === 'string');
+ assert(gettype($endpoint) === 'string');
+
+ // Add the client parameter if forwarded.
+ if (!empty($this->forwardClientIP)) {
+ $params['client'] = $this->forwardClientIP;
+ $this->log('debug', 'Forwarding Client IP: ' . $this->forwardClientIP);
+ }
+
+ // Ignore proxy settings if wished.
+ if ($this->noProxy === true) {
+ $this->log('debug', 'Ignoring proxy settings.');
+ $params['proxy'] = ['https' => '', 'http' => ''];
+ }
+
+ // Set request's timeout
+ $params['timeout'] = $this->timeout;
+
+ $this->log('debug', 'Sending ' . http_build_query($params, '', ', ') . ' to ' . $endpoint);
+
+ $completeUrl = $this->serverURL . $endpoint;
+
+ $curlInstance = curl_init();
+ curl_setopt($curlInstance, CURLOPT_URL, $completeUrl);
+ curl_setopt($curlInstance, CURLOPT_HEADER, true);
+ if ($headers) {
+ curl_setopt($curlInstance, CURLOPT_HTTPHEADER, $headers);
+ }
+ curl_setopt($curlInstance, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($curlInstance, CURLOPT_USERAGENT, $this->userAgent);
+ if ($httpMethod === 'POST') {
+ curl_setopt($curlInstance, CURLOPT_POST, true);
+ curl_setopt($curlInstance, CURLOPT_POSTFIELDS, $params);
+ } elseif ($httpMethod === 'PUT') {
+ curl_setopt($curlInstance, CURLOPT_CUSTOMREQUEST, 'PUT');
+ curl_setopt($curlInstance, CURLOPT_POSTFIELDS, $params);
+ } elseif ($httpMethod === 'DELETE') {
+ curl_setopt($curlInstance, CURLOPT_CUSTOMREQUEST, 'DELETE');
+ curl_setopt($curlInstance, CURLOPT_POSTFIELDS, $params);
+ } elseif ($httpMethod === 'GET') {
+ $paramsStr = '?';
+ if (!empty($params)) {
+ foreach ($params as $key => $value) {
+ $paramsStr .= $key . '=' . $value . '&';
+ }
+ }
+ curl_setopt($curlInstance, CURLOPT_URL, $completeUrl . $paramsStr);
+ }
+
+ // Disable host and/or peer verification for SSL if configured.
+ if ($this->sslVerifyHost === true) {
+ curl_setopt($curlInstance, CURLOPT_SSL_VERIFYHOST, 2);
+ } else {
+ curl_setopt($curlInstance, CURLOPT_SSL_VERIFYHOST, 0);
+ }
+
+ if ($this->sslVerifyPeer === true) {
+ curl_setopt($curlInstance, CURLOPT_SSL_VERIFYPEER, 2);
+ } else {
+ curl_setopt($curlInstance, CURLOPT_SSL_VERIFYPEER, 0);
+ }
+
+ $response = curl_exec($curlInstance);
+
+ if (!$response) {
+ // Handle the error
+ $curlErrno = curl_errno($curlInstance);
+ $this->log('error', 'Bad request: ' . curl_error($curlInstance) . ' errno: ' . $curlErrno);
+ throw new PIBadRequestException('Unable to reach the authentication server (' . $curlErrno . ')');
+ }
+
+ $headerSize = curl_getinfo($curlInstance, CURLINFO_HEADER_SIZE);
+ $ret = substr($response, $headerSize);
+ curl_close($curlInstance);
+
+ // Log the response
+ if ($endpoint != '/auth' && $this->logger != null) {
+ $retJson = json_decode($ret, true);
+ $this->log('debug', $endpoint . ' returned ' . json_encode($retJson, JSON_PRETTY_PRINT));
+ }
+
+ // Return decoded response
+ return $ret;
+ }
+
+ /**
+ * Log a message with the given log level.
+ *
+ * @param $level
+ * @param $message
+ */
+ public function log($level, $message): void
+ {
+ $context = ['app' => 'privacyIDEA'];
+ if ($level === 'debug') {
+ $this->logger->debug($message, $context);
+ }
+ if ($level === 'info') {
+ $this->logger->info($message, $context);
+ }
+ if ($level === 'error') {
+ $this->logger->error($message, $context);
+ }
+ }
+
+ // Setters
+
+ /**
+ * @param string $realm User's realm.
+ * @return void
+ */
+ public function setRealm(string $realm): void
+ {
+ $this->realm = $realm;
+ }
+
+ /**
+ * @param bool $sslVerifyHost Disable host verification for SSL.
+ * @return void
+ */
+ public function setSSLVerifyHost(bool $sslVerifyHost): void
+ {
+ $this->sslVerifyHost = $sslVerifyHost;
+ }
+
+ /**
+ * @param bool $sslVerifyPeer Disable peer verification for SSL.
+ * @return void
+ */
+ public function setSSLVerifyPeer(bool $sslVerifyPeer): void
+ {
+ $this->sslVerifyPeer = $sslVerifyPeer;
+ }
+
+ /**
+ * @param string $serviceAccountName Account name for privacyIDEA service account. Required to use the /validate/triggerchallenge endpoint.
+ * @return void
+ */
+ public function setServiceAccountName(string $serviceAccountName): void
+ {
+ $this->serviceAccountName = $serviceAccountName;
+ }
+
+ /**
+ * @param string $serviceAccountPass Password for privacyIDEA service account. Required to use the /validate/triggerchallenge endpoint.
+ * @return void
+ */
+ public function setServiceAccountPass(string $serviceAccountPass): void
+ {
+ $this->serviceAccountPass = $serviceAccountPass;
+ }
+
+ /**
+ * @param string $serviceAccountRealm Realm for privacyIDEA service account. Optional to use the /validate/triggerchallenge endpoint.
+ * @return void
+ */
+ public function setServiceAccountRealm(string $serviceAccountRealm): void
+ {
+ $this->serviceAccountRealm = $serviceAccountRealm;
+ }
+
+ /**
+ * @param bool $clientIP Send the "client" parameter to allow using the original IP address in the privacyIDEA policies.
+ * @return void
+ */
+ public function setForwardClientIP(bool $clientIP): void
+ {
+ $this->forwardClientIP = $clientIP;
+ }
+
+ /**
+ * @param bool $noProxy Ignore the system-wide proxy settings and send the authentication requests directly to privacyIDEA.
+ * @return void
+ */
+ public function setNoProxy(bool $noProxy): void
+ {
+ $this->$noProxy = $noProxy;
+ }
+
+ /**
+ * @param object|null $logger Implementation of the PILog interface.
+ * @return void
+ */
+ public function setLogger(?object $logger): void
+ {
+ $this->logger = $logger;
+ }
+}
diff --git a/lib/Provider/PrivacyIDEAProvider.php b/lib/Provider/PrivacyIDEAProvider.php
index 49240e6..aa03d53 100644
--- a/lib/Provider/PrivacyIDEAProvider.php
+++ b/lib/Provider/PrivacyIDEAProvider.php
@@ -20,671 +20,541 @@
class PrivacyIDEAProvider implements IProvider
{
- /** @var IAppConfig */
- private IAppConfig $appConfig;
- /** @var LoggerInterface */
- private LoggerInterface $logger;
- /** @var IRequest */
- private IRequest $request;
- /** @var IGroupManager */
- private IGroupManager $groupManager;
- /** @var IL10N */
- private IL10N $trans;
- /** @var ISession */
- private ISession $session;
- /** @var PrivacyIDEA */
- private PrivacyIDEA $pi;
-
- /**
- * PrivacyIDEAProvider constructor.
- *
- * @param IAppConfig $appConfig
- * @param LoggerInterface $logger
- * @param IRequest $request
- * @param IGroupManager $groupManager
- * @param IL10N $trans
- * @param ISession $session
- */
- public function __construct(IAppConfig $appConfig, LoggerInterface $logger, IRequest $request, IGroupManager $groupManager, IL10N $trans, ISession $session)
- {
- $this->appConfig = $appConfig;
- $this->logger = $logger;
- $this->request = $request;
- $this->groupManager = $groupManager;
- $this->trans = $trans;
- $this->session = $session;
- if ($this->session->get("piAllowCreatingPIInstance") === true)
- {
- $this->pi = $this->createPrivacyIDEAInstance();
- }
- }
-
- /**
- * Get the template for rending the 2FA provider view.
- *
- * @param IUser $user
- * @return Template
- * @throws TwoFactorException
- * @throws PIBadRequestException
- */
- public function getTemplate(IUser $user): Template
- {
- if ($this->isTwoFactorAuthEnabledForUser($user) === false)
- {
- $this->session->set("piNoAuthRequired", true);
- $this->verifyChallenge($user, "");
- }
- else
- {
- $this->session->set("piAllowCreatingPIInstance", true);
- $this->pi = $this->createPrivacyIDEAInstance();
- }
- $authenticationFlow = $this->getAppValue("piSelectedAuthFlow", "piAuthFlowDefault");
- $this->log("debug", "Selected authentication flow: " . $authenticationFlow);
- $username = $user->getUID();
- $headers = array();
- $headersFromConfig = $this->getAppValue("piForwardHeaders", "");
- if (!empty($headersFromConfig))
- {
- $headers = $this->getHeadersToForward($headersFromConfig);
- }
-
- if ($authenticationFlow === "piAuthFlowTriggerChallenge")
- {
- if (!empty($this->pi))
- {
- if (!$this->pi->serviceAccountAvailable())
- {
- $this->log("error", "Service account name or password is not set in config. Cannot trigger the challenges.");
- }
- else
- {
- if ($this->session->get("piTriggerChallengeDone") !== true)
- {
- try
- {
- $response = $this->pi->triggerChallenge($username, $headers);
- $this->session->set("piTriggerChallengeDone", true);
- if ($response !== null)
- {
- $this->processPIResponse($response);
- }
- else
- {
- $this->log("error", "No response from privacyIDEA server for triggerchallenge.");
- }
- }
- catch (PIBadRequestException $e)
- {
- $this->handlePIException($e);
- }
- }
- }
- }
- }
- elseif ($authenticationFlow === "piAuthFlowSendStaticPass")
- {
- // Call /validate/check with a static pass from the configuration
- // This could already end up the authentication if the "passOnNoToken" policy is set.
- // Otherwise, it triggers the challenges.
- if ($this->session->get("piStaticPassDone") !== true)
- {
- $response = $this->pi->validateCheck($username, $this->getAppValue("piStaticPass", ""), "", $headers);
- $this->session->set("piStaticPassDone", true);
- if ($response->getAuthenticationStatus() === AuthenticationStatus::ACCEPT)
- {
- // Complete the authentication
- $this->session->set("piSuccess", true);
- $this->verifyChallenge($user, "");
- }
- else
- {
- $this->processPIResponse($response);
- }
- }
- }
- elseif ($authenticationFlow === "piAuthFlowSeparateOTP")
- {
- $this->session->set("piSeparateOTP", true);
- }
- elseif ($authenticationFlow !== "piAuthFlowDefault")
- {
- $this->log("error", "Unknown authentication flow: " . $authenticationFlow . ". Fallback to default.");
- }
-
- // Set options, tokens and load counter to the template
- $template = new Template("privacyidea", "main");
-
- if (!empty($this->session->get("piMessage")))
- {
- $template->assign("message", $this->session->get("piMessage"));
- }
- else
- {
- $template->assign("message", $this->getAppValue("piDefaultMessage", "Please enter the OTP!"));
- }
- if ($this->session->get("piMode") !== null)
- {
- $template->assign("mode", $this->session->get("piMode"));
- }
- if ($this->session->get("piWebAuthnSignRequest") !== null)
- {
- $template->assign("webAuthnSignRequest", $this->session->get("piWebAuthnSignRequest"));
- }
- if ($this->session->get("piPushAvailable"))
- {
- $template->assign("pushAvailable", $this->session->get("piPushAvailable"));
- }
- if ($this->session->get("piOTPAvailable"))
- {
- $template->assign("otpAvailable", $this->session->get("piOTPAvailable"));
- }
- if ($this->session->get("piImgWebauthn") !== null)
- {
- $template->assign("imgWebauthn", $this->session->get("piImgWebauthn"));
- }
- if ($this->session->get("piImgPush") !== null)
- {
- $template->assign("imgPush", $this->session->get("piImgPush"));
- }
- if ($this->session->get("piImgOTP") !== null)
- {
- $template->assign("imgOTP", $this->session->get("piImgOTP"));
- }
- $template->assign("activateAutoSubmitOtpLength", $this->getAppValue("piActivateAutoSubmitOtpLength", "0"));
- $template->assign("autoSubmitOtpLength", $this->getAppValue("piAutoSubmitOtpLength", "6"));
- $template->assign("pollInBrowser", $this->getAppValue("piPollInBrowser", "0"));
- $template->assign("pollInBrowserUrl", $this->getAppValue("piPollInBrowserURL", ""));
- if ($this->session->get("piTransactionID") !== null)
- {
- $template->assign("transactionID", $this->session->get("piTransactionID"));
- }
- if ($this->session->get("piSeparateOTP") !== null && $this->session->get("piSeparateOTP") === true)
- {
- $template->assign("separateOTP", $this->session->get("piSeparateOTP"));
- }
- if ($this->session->get("piPollInBrowserFailed") !== null && $this->session->get("piPollInBrowserFailed") === true)
- {
- $template->assign("pollInBrowserFailed", $this->session->get("piPollInBrowserFailed"));
- }
- if ($this->session->get("piErrorMessage") !== null)
- {
- $template->assign("errorMessage", $this->session->get("piErrorMessage"));
- }
- if ($this->session->get("piAutoSubmit") !== null && $this->session->get("piAutoSubmit") === true)
- {
- $template->assign("autoSubmit", $this->session->get("piAutoSubmit"));
- }
-
- $loads = 1;
- if ($this->session->get("piLoadCounter") !== null)
- {
- $loads = $this->session->get("piLoadCounter");
- }
- $template->assign("loadCounter", $loads);
-
- // Add translations
- $template->assign("verify", $this->trans->t("Verify"));
- $template->assign("alternateLoginOptions", $this->trans->t("Alternate Login Options"));
-
- return $template;
- }
-
- /**
- * Verify the given challenge.
- *
- * @param IUser $user
- * @param string $challenge
- * @return Bool True in case of success. In case of failure, this raises
- * a TwoFactorException with a descriptive error message.
- * @throws TwoFactorException|Exception
- */
- public function verifyChallenge(IUser $user, string $challenge): bool
- {
- if ($this->session->get("piNoAuthRequired") || $this->session->get("piSuccess"))
- {
- $this->session->set("piAutoSubmit", true);
- return true;
- }
-
- if (!empty($this->request->getParam("passField")))
- {
- $password = $this->request->getParam("passField") . $challenge;
- }
- else
- {
- $password = $challenge;
- }
- $username = $user->getUID();
- $transactionID = $this->session->get("piTransactionID");
- $mode = $this->request->getParam("mode");
- $this->session->set("piMode", $mode);
-
- $piResponse = null;
- if ($this->request->getParam("modeChanged") === "1")
- {
- throw new TwoFactorException(" ");
- }
-
- if ($mode === "push")
- {
- $this->log("debug", "Processing PUSH response...");
-
- if ($this->pi->pollTransaction($transactionID))
- {
- // The challenge has been answered. Now we need to verify it.
- $piResponse = $this->pi->validateCheck($username, "", $transactionID);
- $this->processPIResponse($piResponse);
- }
- else
- {
- $this->log("debug", "PUSH not confirmed yet...");
- }
-
- // Increase load counter
- if ($this->request->getParam("loadCounter"))
- {
- $counter = $this->request->getParam("loadCounter");
- $this->session->set("piLoadCounter", $counter + 1);
- }
- }
- elseif ($mode === "webauthn")
- {
- $webAuthnSignResponse = json_decode($this->request->getParam("webAuthnSignResponse"), true);
- $origin = $this->request->getParam("origin");
-
- if (empty($webAuthnSignResponse))
- {
- $this->log("error", "Incomplete data for WebAuthn authentication: WebAuthn sign response is missing!");
- }
- else
- {
- $piResponse = $this->pi->validateCheckWebAuthn($username, $transactionID, json_encode($webAuthnSignResponse), $origin);
- $this->processPIResponse($piResponse);
- }
- }
- else
- {
- if (!empty($transactionID))
- {
- $this->log("debug", "Transaction ID: " . $transactionID);
- $piResponse = $this->pi->validateCheck($username, $password, $transactionID);
- }
- else
- {
- $piResponse = $this->pi->validateCheck($username, $password);
- }
- $this->processPIResponse($piResponse);
- }
-
- if ($piResponse !== null)
- {
- if (!empty($piResponse->getErrorMessage()))
- {
- throw new TwoFactorException($piResponse->getErrorMessage());
- }
- else
- {
- if ($piResponse->getStatus() === true)
- {
- if ($piResponse->getAuthenticationStatus() === AuthenticationStatus::ACCEPT)
- {
- $this->log("debug", "User authenticated successfully!");
- return true;
- }
- else
- {
- if (!empty($piResponse->getMessages()))
- {
- $this->session->set("piMessage", $piResponse->getMessages());
- $this->log("debug", $piResponse->getMessages());
- }
- else
- {
- $this->session->set("piMessage", $piResponse->getMessage());
- $this->log("debug", $piResponse->getMessage());
- }
- }
- }
- elseif ($mode === "push")
- {
- $this->log("debug", "PUSH not confirmed yet...");
- }
- else
- {
- // status === false
- $this->log("error", "privacyIDEA error code: " . $piResponse->getErrorCode());
- $this->log("error", "privacyIDEA error message: " . $piResponse->getErrorMessage());
- throw new TwoFactorException($this->trans->t("Failed to authenticate.") . " " . $piResponse->getErrorMessage());
- }
- }
- }
- throw new TwoFactorException(" ");
- }
-
- /**
- * Create a new privacyIDEA object with the given configuration.
- *
- * @return PrivacyIDEA|null privacyIDEA object or null on error.
- */
- private function createPrivacyIDEAInstance(): ?PrivacyIDEA
- {
- $this->log("info", "Creating privacyIDEA instance...");
- if (!empty($this->getAppValue("piURL", "")))
- {
- $pi = new PrivacyIDEA("privacyidea-nextcloud/1.0.0", $this->getAppValue("piURL", ""));
- $pi->setLogger($this->logger);
- $pi->setSSLVerifyHost($this->getAppValue("piSSLVerify", "true"));
- $pi->setSSLVerifyPeer($this->getAppValue("piSSLVerify", "true"));
- $pi->setServiceAccountName($this->getAppValue("piServiceName", ""));
- $pi->setServiceAccountPass($this->getAppValue("piServicePass", ""));
- $pi->setServiceAccountRealm($this->getAppValue("piServiceRealm", ""));
- $pi->setRealm($this->getAppValue("piRealm", ""));
- $pi->setForwardClientIP($this->getAppValue("piForwardClientIP", false));
- $pi->setNoProxy($this->getAppValue("piNoProxy", false));
- return $pi;
- }
- else
- {
- $this->log("error", "Cannot create privacyIDEA instance: Server URL missing in configuration!");
- }
- return null;
- }
-
- /**
- * Process the response from privacyIDEA and write information to session.
- *
- * @param PIResponse $response
- * @return void
- */
- private function processPIResponse(PIResponse $response): void
- {
- $this->log("info", "Processing server response...");
- $this->session->set("piMode", "otp");
- $this->log("info", "Authentication status: " . $response->getAuthenticationStatus());
- if (!empty($response->getMultiChallenge()))
- {
- $triggeredTokens = $response->getTriggeredTokenTypes();
- if (!empty($response->getPreferredClientMode()))
- {
- if ($response->getPreferredClientMode() === "interactive")
- {
- $this->session->set("piMode", "otp");
- }
- elseif ($response->getPreferredClientMode() === "poll")
- {
- $this->session->set("piMode", "push");
- }
- else
- {
- $this->session->set("piMode", $response->getPreferredClientMode());
- }
- $this->log("debug", "Preferred client mode: " . $this->session->get("piMode"));
- }
- $this->session->set("piPushAvailable", in_array("push", $triggeredTokens));
- $this->session->set("piOTPAvailable", true);
- $this->session->set("piMessage", $response->getMessages());
- $this->session->set("piTransactionID", $response->getTransactionID());
- if (in_array("webauthn", $triggeredTokens))
- {
- $this->session->set("piWebAuthnSignRequest", $response->getWebauthnSignRequest());
- }
-
- // Search for the images
- foreach ($response->getMultiChallenge() as $challenge)
- {
- if (!empty($challenge->image))
- {
- if (!empty($challenge->clientMode) && $challenge->clientMode === "interactive")
- {
- $this->session->set("piImageOtp", $challenge->image);
- }
- elseif (!empty($challenge->clientMode) && $challenge->clientMode === "poll")
- {
- $this->session->set("piImagePush", $challenge->image);
- }
- elseif (!empty($challenge->clientMode) && $challenge->clientMode === "webauthn")
- {
- $this->session->set("piImageWebAuthn", $challenge->image);
- }
- }
- }
- }
- elseif (!empty($response->getErrorCode()))
- {
- // privacyIDEA returned an error, prepare it to display.
- $this->log("error", "Error code: " . $response->getErrorCode() . ", Error Message: " . $response->getErrorMessage());
- $this->session->set("piErrorCode", $response->getErrorCode());
- $this->session->set("piErrorMessage", $response->getErrorMessage());
- }
- elseif ($response->getAuthenticationStatus() === AuthenticationStatus::ACCEPT)
- {
- // The user has been authenticated successfully.
- $this->log("info", $response->getMessage());
- }
- else
- {
- // Unexpected response
- $this->log("error", $response->getMessage());
- $this->session->set("piErrorMessage", $response->getMessage());
- }
- }
-
- /**
- * Search for the configured headers in $_SERVER and return all found with their values.
- *
- * @return array Headers to forward with their values.
- */
- private function getHeadersToForward(string $headers): array
- {
- $cleanHeaders = str_replace(' ', '', $headers);
- $arrHeaders = explode(',', $cleanHeaders);
-
- $headersToForward = array();
- foreach ($arrHeaders as $header)
- {
- if (array_key_exists($header, $_SERVER))
- {
- $this->log("debug", "Found matching header: " . $header);
- $value = $_SERVER[$header];
- if (is_array($_SERVER[$header]))
- {
- $value = implode(',', $_SERVER[$header]);
- }
- $header = array($header => $value);
- $headersToForward = array_push($headersToForward, $header);
- }
- else
- {
- $this->log("debug", "No values for header: " . $header . " found.");
- }
- }
- return $headersToForward;
- }
-
- /**
- * Log the exceptions coming from the privacyIDEA.
- * Also set the error code and message in the session.
- *
- * @param PIBadRequestException $e
- * @return void
- */
- private function handlePIException(PIBadRequestException $e): void
- {
- $this->log("error", "Exception: " . $e->getMessage());
- $this->session->set("piErrorCode", $e->getCode());
- $this->session->set("piErrorMessage", $e->getMessage());
- }
-
- /**
- * Check whether 2FA is enabled for the given user.
- *
- * @param IUser $user
- * @return bool
- */
- public function isTwoFactorAuthEnabledForUser(IUser $user): bool
- {
- $piActive = $this->getAppValue('piActivatePI', '0');
- $piExcludeIPs = $this->getAppValue('piExcludeIPs', '');
- $piInExGroups = $this->getAppValue('piInExGroupsField', '');
- $piInOrExSelected = $this->getAppValue('piInOrExSelected', 'exclude');
-
- if ($piActive === "1")
- {
- if ($piExcludeIPs)
- {
- $ipAddresses = explode(",", $piExcludeIPs);
- $clientIP = ip2long($this->getClientIP());
- foreach ($ipAddresses as $address)
- {
- if (str_contains($address, '-'))
- {
- $range = explode('-', $address);
- $startIP = ip2long($range[0]);
- $endIP = ip2long($range[1]);
- if ($clientIP >= $startIP && $clientIP <= $endIP)
- {
- return false;
- }
- }
- else
- {
- if ($clientIP === ip2long($address))
- {
- return false;
- }
- }
- }
- }
- if (!empty($piInExGroups))
- {
- $piInExGroups = str_replace(" ", "", $piInExGroups);
- $groups = explode(",", $piInExGroups);
- $checkEnabled = false;
- foreach ($groups as $group)
- {
- if ($this->groupManager->isInGroup($user->getUID(), trim($group)))
- {
- $this->log("debug", "[isTwoFactorEnabledForUser] The user " . $user->getUID() . " is in group " . $group . ".");
- if ($piInOrExSelected === "exclude")
- {
- $this->log("debug", "[isTwoFactorEnabledForUser] The group " . $group . " is excluded (User does not need MFA).");
- return false;
- }
- if ($piInOrExSelected === "include")
- {
- $this->log("debug", "[isTwoFactorEnabledForUser] The group " . $group . " is included (User needs MFA).");
- return true;
- }
- }
- $this->log("debug", "[isTwoFactorEnabledForUser] The user " . $user->getUID() . " is not in group " . $group . ".");
- if ($piInOrExSelected === "exclude")
- {
- $this->log("debug", "[isTwoFactorEnabledForUser] The group " . $group . " is excluded (User may need MFA).");
- $checkEnabled = true;
- }
- if ($piInOrExSelected === "include")
- {
- $this->log("debug", "[isTwoFactorEnabledForUser] The group " . $group . " is included (User may not need MFA).");
- $checkEnabled = false;
- }
- }
- if (!$checkEnabled)
- {
- return false;
- }
- }
- $this->log("debug", "[isTwoFactorAuthEnabledForUser] User needs MFA.");
- return true;
- }
- $this->log("debug", "[isTwoFactorAuthEnabledForUser] privacyIDEA is not enabled.");
- return false;
- }
-
- /**
- * Retrieve a value from the privacyIDEA app configuration store.
- *
- * @param string $key application config key
- * @param $default
- * @return string
- */
- private function getAppValue(string $key, $default): string
- {
- return $this->appConfig->getValueString('privacyidea', $key, $default);
- }
-
- /**
- * Get the client IP address.
- *
- * @return mixed|string
- */
- public function getClientIP(): mixed
- {
- if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER))
- {
- return $_SERVER["HTTP_X_FORWARDED_FOR"];
- }
- else if (array_key_exists('REMOTE_ADDR', $_SERVER))
- {
- return $_SERVER["REMOTE_ADDR"];
- }
- else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER))
- {
- return $_SERVER["HTTP_CLIENT_IP"];
- }
- return '';
- }
-
- /**
- * Get unique identifier of this 2FA provider.
- *
- * @return string
- */
- public function getId(): string
- {
- return 'privacyidea';
- }
-
- /**
- * Get the display name for selecting the 2FA provider.
- *
- * @return string
- */
- public function getDisplayName(): string
- {
- return 'PrivacyIDEA';
- }
-
- /**
- * Get the description for selecting the 2FA provider.
- *
- * @return string
- */
- public function getDescription(): string
- {
- return 'Use PrivacyIDEA for multi-factor authentication';
- }
-
- /**
- * Log a message with the given log level.
- *
- * @param $level
- * @param $message
- */
- private function log($level, $message): void
- {
- $context = ["app" => "privacyIDEA"];
- if ($level === 'debug')
- {
- $this->logger->debug($message, $context);
- }
- if ($level === 'info')
- {
- $this->logger->info($message, $context);
- }
- if ($level === 'error')
- {
- $this->logger->error($message, $context);
- }
- }
-}
\ No newline at end of file
+ /** @var IAppConfig */
+ private IAppConfig $appConfig;
+ /** @var LoggerInterface */
+ private LoggerInterface $logger;
+ /** @var IRequest */
+ private IRequest $request;
+ /** @var IGroupManager */
+ private IGroupManager $groupManager;
+ /** @var IL10N */
+ private IL10N $trans;
+ /** @var ISession */
+ private ISession $session;
+ /** @var PrivacyIDEA */
+ private PrivacyIDEA $pi;
+
+ /**
+ * PrivacyIDEAProvider constructor.
+ *
+ * @param IAppConfig $appConfig
+ * @param LoggerInterface $logger
+ * @param IRequest $request
+ * @param IGroupManager $groupManager
+ * @param IL10N $trans
+ * @param ISession $session
+ */
+ public function __construct(IAppConfig $appConfig, LoggerInterface $logger, IRequest $request, IGroupManager $groupManager, IL10N $trans, ISession $session)
+ {
+ $this->appConfig = $appConfig;
+ $this->logger = $logger;
+ $this->request = $request;
+ $this->groupManager = $groupManager;
+ $this->trans = $trans;
+ $this->session = $session;
+ if ($this->session->get('piAllowCreatingPIInstance') === true) {
+ $this->pi = $this->createPrivacyIDEAInstance();
+ }
+ }
+
+ /**
+ * Get the template for rending the 2FA provider view.
+ *
+ * @param IUser $user
+ * @return Template
+ * @throws TwoFactorException
+ * @throws PIBadRequestException
+ */
+ public function getTemplate(IUser $user): Template
+ {
+ if ($this->isTwoFactorAuthEnabledForUser($user) === false) {
+ $this->session->set('piNoAuthRequired', true);
+ $this->verifyChallenge($user, '');
+ } else {
+ $this->session->set('piAllowCreatingPIInstance', true);
+ $this->pi = $this->createPrivacyIDEAInstance();
+ }
+ $authenticationFlow = $this->getAppValue('piSelectedAuthFlow', 'piAuthFlowDefault');
+ $this->log('debug', 'Selected authentication flow: ' . $authenticationFlow);
+ $username = $user->getUID();
+ $headers = [];
+ $headersFromConfig = $this->getAppValue('piForwardHeaders', '');
+ if (!empty($headersFromConfig)) {
+ $headers = $this->getHeadersToForward($headersFromConfig);
+ }
+
+ if ($authenticationFlow === 'piAuthFlowTriggerChallenge') {
+ if (!empty($this->pi)) {
+ if (!$this->pi->serviceAccountAvailable()) {
+ $this->log('error', 'Service account name or password is not set in config. Cannot trigger the challenges.');
+ } else {
+ if ($this->session->get('piTriggerChallengeDone') !== true) {
+ try {
+ $response = $this->pi->triggerChallenge($username, $headers);
+ $this->session->set('piTriggerChallengeDone', true);
+ if ($response !== null) {
+ $this->processPIResponse($response);
+ } else {
+ $this->log('error', 'No response from privacyIDEA server for triggerchallenge.');
+ }
+ } catch (PIBadRequestException $e) {
+ $this->handlePIException($e);
+ }
+ }
+ }
+ }
+ } elseif ($authenticationFlow === 'piAuthFlowSendStaticPass') {
+ // Call /validate/check with a static pass from the configuration
+ // This could already end up the authentication if the "passOnNoToken" policy is set.
+ // Otherwise, it triggers the challenges.
+ if ($this->session->get('piStaticPassDone') !== true) {
+ $response = $this->pi->validateCheck($username, $this->getAppValue('piStaticPass', ''), '', $headers);
+ $this->session->set('piStaticPassDone', true);
+ if ($response->getAuthenticationStatus() === AuthenticationStatus::ACCEPT) {
+ // Complete the authentication
+ $this->session->set('piSuccess', true);
+ $this->verifyChallenge($user, '');
+ } else {
+ $this->processPIResponse($response);
+ }
+ }
+ } elseif ($authenticationFlow === 'piAuthFlowSeparateOTP') {
+ $this->session->set('piSeparateOTP', true);
+ } elseif ($authenticationFlow !== 'piAuthFlowDefault') {
+ $this->log('error', 'Unknown authentication flow: ' . $authenticationFlow . '. Fallback to default.');
+ }
+
+ // Set options, tokens and load counter to the template
+ $template = new Template('privacyidea', 'main');
+
+ if (!empty($this->session->get('piMessage'))) {
+ $template->assign('message', $this->session->get('piMessage'));
+ } else {
+ $template->assign('message', $this->getAppValue('piDefaultMessage', 'Please enter the OTP!'));
+ }
+ if ($this->session->get('piMode') !== null) {
+ $template->assign('mode', $this->session->get('piMode'));
+ }
+ if ($this->session->get('piWebAuthnSignRequest') !== null) {
+ $template->assign('webAuthnSignRequest', $this->session->get('piWebAuthnSignRequest'));
+ }
+ if ($this->session->get('piPushAvailable')) {
+ $template->assign('pushAvailable', $this->session->get('piPushAvailable'));
+ }
+ if ($this->session->get('piOTPAvailable')) {
+ $template->assign('otpAvailable', $this->session->get('piOTPAvailable'));
+ }
+ if ($this->session->get('piImgWebauthn') !== null) {
+ $template->assign('imgWebauthn', $this->session->get('piImgWebauthn'));
+ }
+ if ($this->session->get('piImgPush') !== null) {
+ $template->assign('imgPush', $this->session->get('piImgPush'));
+ }
+ if ($this->session->get('piImgOTP') !== null) {
+ $template->assign('imgOTP', $this->session->get('piImgOTP'));
+ }
+ $template->assign('activateAutoSubmitOtpLength', $this->getAppValue('piActivateAutoSubmitOtpLength', '0'));
+ $template->assign('autoSubmitOtpLength', $this->getAppValue('piAutoSubmitOtpLength', '6'));
+ $template->assign('pollInBrowser', $this->getAppValue('piPollInBrowser', '0'));
+ $template->assign('pollInBrowserUrl', $this->getAppValue('piPollInBrowserURL', ''));
+ if ($this->session->get('piTransactionID') !== null) {
+ $template->assign('transactionID', $this->session->get('piTransactionID'));
+ }
+ if ($this->session->get('piSeparateOTP') !== null && $this->session->get('piSeparateOTP') === true) {
+ $template->assign('separateOTP', $this->session->get('piSeparateOTP'));
+ }
+ if ($this->session->get('piPollInBrowserFailed') !== null && $this->session->get('piPollInBrowserFailed') === true) {
+ $template->assign('pollInBrowserFailed', $this->session->get('piPollInBrowserFailed'));
+ }
+ if ($this->session->get('piErrorMessage') !== null) {
+ $template->assign('errorMessage', $this->session->get('piErrorMessage'));
+ }
+ if ($this->session->get('piAutoSubmit') !== null && $this->session->get('piAutoSubmit') === true) {
+ $template->assign('autoSubmit', $this->session->get('piAutoSubmit'));
+ }
+
+ $loads = 1;
+ if ($this->session->get('piLoadCounter') !== null) {
+ $loads = $this->session->get('piLoadCounter');
+ }
+ $template->assign('loadCounter', $loads);
+
+ // Add translations
+ $template->assign('verify', $this->trans->t('Verify'));
+ $template->assign('alternateLoginOptions', $this->trans->t('Alternate Login Options'));
+
+ return $template;
+ }
+
+ /**
+ * Verify the given challenge.
+ *
+ * @param IUser $user
+ * @param string $challenge
+ * @return Bool True in case of success. In case of failure, this raises
+ * a TwoFactorException with a descriptive error message.
+ * @throws TwoFactorException|Exception
+ */
+ public function verifyChallenge(IUser $user, string $challenge): bool
+ {
+ if ($this->session->get('piNoAuthRequired') || $this->session->get('piSuccess')) {
+ $this->session->set('piAutoSubmit', true);
+ return true;
+ }
+
+ if (!empty($this->request->getParam('passField'))) {
+ $password = $this->request->getParam('passField') . $challenge;
+ } else {
+ $password = $challenge;
+ }
+ $username = $user->getUID();
+ $transactionID = $this->session->get('piTransactionID');
+ $mode = $this->request->getParam('mode');
+ $this->session->set('piMode', $mode);
+
+ $piResponse = null;
+ if ($this->request->getParam('modeChanged') === '1') {
+ throw new TwoFactorException(' ');
+ }
+
+ if ($mode === 'push') {
+ $this->log('debug', 'Processing PUSH response...');
+
+ if ($this->pi->pollTransaction($transactionID)) {
+ // The challenge has been answered. Now we need to verify it.
+ $piResponse = $this->pi->validateCheck($username, '', $transactionID);
+ $this->processPIResponse($piResponse);
+ } else {
+ $this->log('debug', 'PUSH not confirmed yet...');
+ }
+
+ // Increase load counter
+ if ($this->request->getParam('loadCounter')) {
+ $counter = $this->request->getParam('loadCounter');
+ $this->session->set('piLoadCounter', $counter + 1);
+ }
+ } elseif ($mode === 'webauthn') {
+ $webAuthnSignResponse = json_decode($this->request->getParam('webAuthnSignResponse'), true);
+ $origin = $this->request->getParam('origin');
+
+ if (empty($webAuthnSignResponse)) {
+ $this->log('error', 'Incomplete data for WebAuthn authentication: WebAuthn sign response is missing!');
+ } else {
+ $piResponse = $this->pi->validateCheckWebAuthn($username, $transactionID, json_encode($webAuthnSignResponse), $origin);
+ $this->processPIResponse($piResponse);
+ }
+ } else {
+ if (!empty($transactionID)) {
+ $this->log('debug', 'Transaction ID: ' . $transactionID);
+ $piResponse = $this->pi->validateCheck($username, $password, $transactionID);
+ } else {
+ $piResponse = $this->pi->validateCheck($username, $password);
+ }
+ $this->processPIResponse($piResponse);
+ }
+
+ if ($piResponse !== null) {
+ if (!empty($piResponse->getErrorMessage())) {
+ throw new TwoFactorException($piResponse->getErrorMessage());
+ } else {
+ if ($piResponse->getStatus() === true) {
+ if ($piResponse->getAuthenticationStatus() === AuthenticationStatus::ACCEPT) {
+ $this->log('debug', 'User authenticated successfully!');
+ return true;
+ } else {
+ if (!empty($piResponse->getMessages())) {
+ $this->session->set('piMessage', $piResponse->getMessages());
+ $this->log('debug', $piResponse->getMessages());
+ } else {
+ $this->session->set('piMessage', $piResponse->getMessage());
+ $this->log('debug', $piResponse->getMessage());
+ }
+ }
+ } elseif ($mode === 'push') {
+ $this->log('debug', 'PUSH not confirmed yet...');
+ } else {
+ // status === false
+ $this->log('error', 'privacyIDEA error code: ' . $piResponse->getErrorCode());
+ $this->log('error', 'privacyIDEA error message: ' . $piResponse->getErrorMessage());
+ throw new TwoFactorException($this->trans->t('Failed to authenticate.') . ' ' . $piResponse->getErrorMessage());
+ }
+ }
+ }
+ throw new TwoFactorException(' ');
+ }
+
+ /**
+ * Create a new privacyIDEA object with the given configuration.
+ *
+ * @return PrivacyIDEA|null privacyIDEA object or null on error.
+ */
+ private function createPrivacyIDEAInstance(): ?PrivacyIDEA
+ {
+ $this->log('info', 'Creating privacyIDEA instance...');
+ if (!empty($this->getAppValue('piURL', ''))) {
+ $pi = new PrivacyIDEA('privacyidea-nextcloud/1.0.0', $this->getAppValue('piURL', ''));
+ $pi->setLogger($this->logger);
+ $pi->setSSLVerifyHost($this->getAppValue('piSSLVerify', true));
+ $pi->setSSLVerifyPeer($this->getAppValue('piSSLVerify', true));
+ $pi->setServiceAccountName($this->getAppValue('piServiceName', ''));
+ $pi->setServiceAccountPass($this->getAppValue('piServicePass', ''));
+ $pi->setServiceAccountRealm($this->getAppValue('piServiceRealm', ''));
+ $pi->setRealm($this->getAppValue('piRealm', ''));
+ $pi->setNoProxy($this->getAppValue('piNoProxy', false));
+ if ($this->getAppValue('piForwardClientIP', false) && !empty($this->getClientIP())) {
+ $pi->setForwardClientIP($this->getClientIP());
+ }
+ return $pi;
+ } else {
+ $this->log('error', 'Cannot create privacyIDEA instance: Server URL missing in configuration!');
+ }
+ return null;
+ }
+
+ /**
+ * Process the response from privacyIDEA and write information to session.
+ *
+ * @param PIResponse $response
+ * @return void
+ */
+ private function processPIResponse(PIResponse $response): void
+ {
+ $this->log('info', 'Processing server response...');
+ $this->session->set('piMode', 'otp');
+ $this->log('info', 'Authentication status: ' . $response->getAuthenticationStatus());
+ if (!empty($response->getMultiChallenge())) {
+ $triggeredTokens = $response->getTriggeredTokenTypes();
+ if (!empty($response->getPreferredClientMode())) {
+ if ($response->getPreferredClientMode() === 'interactive') {
+ $this->session->set('piMode', 'otp');
+ } elseif ($response->getPreferredClientMode() === 'poll') {
+ $this->session->set('piMode', 'push');
+ } else {
+ $this->session->set('piMode', $response->getPreferredClientMode());
+ }
+ $this->log('debug', 'Preferred client mode: ' . $this->session->get('piMode'));
+ }
+ $this->session->set('piPushAvailable', in_array('push', $triggeredTokens));
+ $this->session->set('piOTPAvailable', true);
+ $this->session->set('piMessage', $response->getMessages());
+ $this->session->set('piTransactionID', $response->getTransactionID());
+ if (in_array('webauthn', $triggeredTokens)) {
+ $this->session->set('piWebAuthnSignRequest', $response->getWebauthnSignRequest());
+ }
+
+ // Search for the images
+ foreach ($response->getMultiChallenge() as $challenge) {
+ if (!empty($challenge->image)) {
+ if (!empty($challenge->clientMode) && $challenge->clientMode === 'interactive') {
+ $this->session->set('piImageOtp', $challenge->image);
+ } elseif (!empty($challenge->clientMode) && $challenge->clientMode === 'poll') {
+ $this->session->set('piImagePush', $challenge->image);
+ } elseif (!empty($challenge->clientMode) && $challenge->clientMode === 'webauthn') {
+ $this->session->set('piImageWebAuthn', $challenge->image);
+ }
+ }
+ }
+ } elseif (!empty($response->getErrorCode())) {
+ // privacyIDEA returned an error, prepare it to display.
+ $this->log('error', 'Error code: ' . $response->getErrorCode() . ', Error Message: ' . $response->getErrorMessage());
+ $this->session->set('piErrorCode', $response->getErrorCode());
+ $this->session->set('piErrorMessage', $response->getErrorMessage());
+ } elseif ($response->getAuthenticationStatus() === AuthenticationStatus::ACCEPT) {
+ // The user has been authenticated successfully.
+ $this->log('info', $response->getMessage());
+ } else {
+ // Unexpected response
+ $this->log('error', $response->getMessage());
+ $this->session->set('piErrorMessage', $response->getMessage());
+ }
+ }
+
+ /**
+ * Search for the configured headers in $_SERVER and return all found with their values.
+ *
+ * @return array Headers to forward with their values.
+ */
+ private function getHeadersToForward(string $headers): array
+ {
+ $cleanHeaders = str_replace(' ', '', $headers);
+ $arrHeaders = explode(',', $cleanHeaders);
+
+ $headersToForward = [];
+ foreach ($arrHeaders as $header) {
+ if (array_key_exists($header, $_SERVER)) {
+ $this->log('debug', 'Found matching header: ' . $header);
+ $value = $_SERVER[$header];
+ if (is_array($_SERVER[$header])) {
+ $value = implode(',', $_SERVER[$header]);
+ }
+ $header = [$header => $value];
+ $headersToForward = array_push($headersToForward, $header);
+ } else {
+ $this->log('debug', 'No values for header: ' . $header . ' found.');
+ }
+ }
+ return $headersToForward;
+ }
+
+ /**
+ * Log the exceptions coming from the privacyIDEA.
+ * Also set the error code and message in the session.
+ *
+ * @param PIBadRequestException $e
+ * @return void
+ */
+ private function handlePIException(PIBadRequestException $e): void
+ {
+ $this->log('error', 'Exception: ' . $e->getMessage());
+ $this->session->set('piErrorCode', $e->getCode());
+ $this->session->set('piErrorMessage', $e->getMessage());
+ }
+
+ /**
+ * Check whether 2FA is enabled for the given user.
+ *
+ * @param IUser $user
+ * @return bool
+ */
+ public function isTwoFactorAuthEnabledForUser(IUser $user): bool
+ {
+ $piActive = $this->getAppValue('piActivatePI', '0');
+ $piExcludeIPs = $this->getAppValue('piExcludeIPs', '');
+ $piInExGroups = $this->getAppValue('piInExGroupsField', '');
+ $piInOrExSelected = $this->getAppValue('piInOrExSelected', 'exclude');
+
+ if ($piActive === '1') {
+ if ($piExcludeIPs) {
+ $ipAddresses = explode(',', $piExcludeIPs);
+ $clientIP = ip2long($this->getClientIP());
+ foreach ($ipAddresses as $address) {
+ if (str_contains($address, '-')) {
+ $range = explode('-', $address);
+ $startIP = ip2long($range[0]);
+ $endIP = ip2long($range[1]);
+ if ($clientIP >= $startIP && $clientIP <= $endIP) {
+ return false;
+ }
+ } else {
+ if ($clientIP === ip2long($address)) {
+ return false;
+ }
+ }
+ }
+ }
+ if (!empty($piInExGroups)) {
+ $piInExGroups = str_replace(' ', '', $piInExGroups);
+ $groups = explode(',', $piInExGroups);
+ $checkEnabled = false;
+ foreach ($groups as $group) {
+ if ($this->groupManager->isInGroup($user->getUID(), trim($group))) {
+ $this->log('debug', '[isTwoFactorEnabledForUser] The user ' . $user->getUID() . ' is in group ' . $group . '.');
+ if ($piInOrExSelected === 'exclude') {
+ $this->log('debug', '[isTwoFactorEnabledForUser] The group ' . $group . ' is excluded (User does not need MFA).');
+ return false;
+ }
+ if ($piInOrExSelected === 'include') {
+ $this->log('debug', '[isTwoFactorEnabledForUser] The group ' . $group . ' is included (User needs MFA).');
+ return true;
+ }
+ }
+ $this->log('debug', '[isTwoFactorEnabledForUser] The user ' . $user->getUID() . ' is not in group ' . $group . '.');
+ if ($piInOrExSelected === 'exclude') {
+ $this->log('debug', '[isTwoFactorEnabledForUser] The group ' . $group . ' is excluded (User may need MFA).');
+ $checkEnabled = true;
+ }
+ if ($piInOrExSelected === 'include') {
+ $this->log('debug', '[isTwoFactorEnabledForUser] The group ' . $group . ' is included (User may not need MFA).');
+ $checkEnabled = false;
+ }
+ }
+ if (!$checkEnabled) {
+ return false;
+ }
+ }
+ $this->log('debug', '[isTwoFactorAuthEnabledForUser] User needs MFA.');
+ return true;
+ }
+ $this->log('debug', '[isTwoFactorAuthEnabledForUser] privacyIDEA is not enabled.');
+ return false;
+ }
+
+ /**
+ * Retrieve a value from the privacyIDEA app configuration store.
+ *
+ * @param string $key application config key
+ * @param $default
+ * @return string
+ */
+ private function getAppValue(string $key, $default): string
+ {
+ return $this->appConfig->getValueString('privacyidea', $key, $default);
+ }
+
+ /**
+ * Get the client IP address.
+ *
+ * @return string Client IP address or an empty string.
+ */
+ public function getClientIP(): string
+ {
+ $clientIP = $this->request->getRemoteAddress();
+ if (!empty($clientIP)) {
+ return $clientIP;
+ } else {
+ $this->log('error', 'Cannot get client IP address.');
+ return '';
+ }
+ }
+
+ /**
+ * Get unique identifier of this 2FA provider.
+ *
+ * @return string
+ */
+ public function getId(): string
+ {
+ return 'privacyidea';
+ }
+
+ /**
+ * Get the display name for selecting the 2FA provider.
+ *
+ * @return string
+ */
+ public function getDisplayName(): string
+ {
+ return 'PrivacyIDEA';
+ }
+
+ /**
+ * Get the description for selecting the 2FA provider.
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'Use PrivacyIDEA for multi-factor authentication';
+ }
+
+ /**
+ * Log a message with the given log level.
+ *
+ * @param $level
+ * @param $message
+ */
+ private function log($level, $message): void
+ {
+ $context = ['app' => 'privacyIDEA'];
+ if ($level === 'debug') {
+ $this->logger->debug($message, $context);
+ }
+ if ($level === 'info') {
+ $this->logger->info($message, $context);
+ }
+ if ($level === 'error') {
+ $this->logger->error($message, $context);
+ }
+ }
+}
diff --git a/lib/Sections/PrivacyIDEAAdmin.php b/lib/Sections/PrivacyIDEAAdmin.php
index 0efd369..ee64916 100644
--- a/lib/Sections/PrivacyIDEAAdmin.php
+++ b/lib/Sections/PrivacyIDEAAdmin.php
@@ -8,32 +8,32 @@
class PrivacyIDEAAdmin implements IIconSection
{
- private IL10N $l;
- private IURLGenerator $urlGenerator;
-
- public function __construct(IL10N $l, IURLGenerator $urlGenerator)
- {
- $this->l = $l;
- $this->urlGenerator = $urlGenerator;
- }
-
- public function getIcon(): string
- {
- return $this->urlGenerator->imagePath('privacyidea', 'settings-dark.svg');
- }
-
- public function getID(): string
- {
- return 'privacyidea';
- }
-
- public function getName(): string
- {
- return $this->l->t('privacyIDEA');
- }
-
- public function getPriority(): int
- {
- return 91;
- }
-}
\ No newline at end of file
+ private IL10N $l;
+ private IURLGenerator $urlGenerator;
+
+ public function __construct(IL10N $l, IURLGenerator $urlGenerator)
+ {
+ $this->l = $l;
+ $this->urlGenerator = $urlGenerator;
+ }
+
+ public function getIcon(): string
+ {
+ return $this->urlGenerator->imagePath('privacyidea', 'settings-dark.svg');
+ }
+
+ public function getID(): string
+ {
+ return 'privacyidea';
+ }
+
+ public function getName(): string
+ {
+ return $this->l->t('privacyIDEA');
+ }
+
+ public function getPriority(): int
+ {
+ return 91;
+ }
+}
diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php
index e96a438..cb97b00 100644
--- a/lib/Settings/Admin.php
+++ b/lib/Settings/Admin.php
@@ -3,33 +3,33 @@
namespace OCA\PrivacyIDEA\Settings;
use OCP\AppFramework\Http\TemplateResponse;
-use OCP\Settings\ISettings;
use OCP\IConfig;
+use OCP\Settings\ISettings;
class Admin implements ISettings
{
- private IConfig $config;
+ private IConfig $config;
- public function __construct(IConfig $config)
- {
- $this->config = $config;
- }
+ public function __construct(IConfig $config)
+ {
+ $this->config = $config;
+ }
- /**
- * @return TemplateResponse
- */
- public function getForm(): TemplateResponse
- {
- return new TemplateResponse('privacyidea', 'settings-admin', [], '');
- }
+ /**
+ * @return TemplateResponse
+ */
+ public function getForm(): TemplateResponse
+ {
+ return new TemplateResponse('privacyidea', 'settings-admin', [], '');
+ }
- public function getSection(): string
- {
- return 'privacyidea';
- }
+ public function getSection(): string
+ {
+ return 'privacyidea';
+ }
- public function getPriority(): int
- {
- return 10;
- }
-}
\ No newline at end of file
+ public function getPriority(): int
+ {
+ return 10;
+ }
+}
diff --git a/package.json b/package.json
index ff89edb..cec0e73 100644
--- a/package.json
+++ b/package.json
@@ -19,9 +19,6 @@
"@nextcloud/router": "^3.0.1"
},
"devDependencies": {
- "@nextcloud/browserslist-config": "^3.0.1",
- "@nextcloud/eslint-config": "^8.3.0",
- "@nextcloud/stylelint-config": "^2.4.0",
- "@nextcloud/webpack-vue-config": "^6.0.1"
+ "@nextcloud/browserslist-config": "^3.0.1"
}
}
\ No newline at end of file
diff --git a/src/CodingStandard/Config.php b/src/CodingStandard/Config.php
new file mode 100644
index 0000000..506ccab
--- /dev/null
+++ b/src/CodingStandard/Config.php
@@ -0,0 +1,79 @@
+setIndent("\t");
+ $this->registerCustomFixers(new PhpCsFixerCustomFixers\Fixers());
+ }
+
+ public function getRules() : array {
+ return [
+ '@PSR1' => true,
+ '@PSR2' => true,
+ 'align_multiline_comment' => true,
+ 'array_indentation' => true,
+ 'array_syntax' => true,
+ 'binary_operator_spaces' => [
+ 'default' => 'single_space',
+ ],
+ 'blank_line_after_namespace' => true,
+ 'blank_line_after_opening_tag' => true,
+ 'cast_spaces' => ['space' => 'none'],
+ 'concat_space' => ['spacing' => 'one'],
+ 'elseif' => true,
+ 'encoding' => true,
+ 'full_opening_tag' => true,
+ 'function_declaration' => [
+ 'closure_function_spacing' => 'one',
+ ],
+ 'indentation_type' => true,
+ 'line_ending' => true,
+ 'list_syntax' => true,
+ 'lowercase_cast' => true,
+ 'lowercase_keywords' => true,
+ 'method_argument_space' => [
+ 'on_multiline' => 'ignore',
+ ],
+ 'method_chaining_indentation' => true,
+ 'no_closing_tag' => true,
+ 'no_leading_import_slash' => true,
+ 'no_short_bool_cast' => true,
+ 'no_spaces_after_function_name' => true,
+ 'no_spaces_inside_parenthesis' => true,
+ 'no_trailing_whitespace' => true,
+ 'no_trailing_whitespace_in_comment' => true,
+ 'no_unused_imports' => true,
+ 'nullable_type_declaration_for_default_null_value' => true,
+ 'nullable_type_declaration' => ['syntax' => 'question_mark'],
+ 'ordered_imports' => [
+ 'imports_order' => ['class', 'function', 'const'],
+ 'sort_algorithm' => 'alpha'
+ ],
+ 'phpdoc_align' => ['align' => 'left'],
+ 'phpdoc_single_line_var_spacing' => true,
+ 'phpdoc_var_annotation_correct_order' => true,
+ 'short_scalar_cast' => true,
+ 'single_blank_line_at_eof' => true,
+ 'single_class_element_per_statement' => true,
+ 'single_import_per_statement' => true,
+ 'single_line_after_imports' => true,
+ 'single_quote' => ['strings_containing_single_quote_chars' => false],
+ 'switch_case_space' => true,
+ 'trailing_comma_in_multiline' => ['elements' => ['parameters']],
+ 'types_spaces' => ['space' => 'none', 'space_multiple_catch' => 'none'],
+ 'visibility_required' => [
+ 'elements' => ['property', 'method', 'const']
+ ],
+ 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false],
+ PhpCsFixerCustomFixers\Fixer\MultilinePromotedPropertiesFixer::name() => true,
+ ];
+ }
+}
diff --git a/stylelint.config.js b/stylelint.config.js
deleted file mode 100644
index 3be3a7b..0000000
--- a/stylelint.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- extends: 'stylelint-config-recommended-vue',
-}
diff --git a/templates/main.php b/templates/main.php
index 33f89e5..7bf40fd 100644
--- a/templates/main.php
+++ b/templates/main.php
@@ -19,13 +19,13 @@
-
+
+if (!empty($_['imgPush']) && $_['mode'] === 'push') : ?>
+if (!empty($_['imgOTP']) && $_['mode'] === 'otp') : ?>
@@ -75,13 +75,10 @@
diff --git a/vendor-bin/cs-fixer/composer.json b/vendor-bin/cs-fixer/composer.json
index dc131e7..2414a84 100644
--- a/vendor-bin/cs-fixer/composer.json
+++ b/vendor-bin/cs-fixer/composer.json
@@ -1,7 +1,4 @@
{
- "require-dev": {
- "nextcloud/coding-standard": "^1.2"
- },
"config": {
"platform": {
"php": "8.1"
diff --git a/vendor-bin/openapi-extractor/composer.json b/vendor-bin/openapi-extractor/composer.json
deleted file mode 100644
index dde3ad3..0000000
--- a/vendor-bin/openapi-extractor/composer.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "repositories": [
- {
- "type": "vcs",
- "url": "https://github.com/nextcloud/openapi-extractor"
- }
- ],
- "require-dev": {
- "nextcloud/openapi-extractor": "dev-main"
- },
- "config": {
- "platform": {
- "php": "8.1"
- }
- }
-}
diff --git a/vendor-bin/phpunit/composer.json b/vendor-bin/phpunit/composer.json
deleted file mode 100644
index fe8b171..0000000
--- a/vendor-bin/phpunit/composer.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "require-dev": {
- "phpunit/phpunit": "^10.5"
- },
- "config": {
- "platform": {
- "php": "8.1"
- }
- }
-}