├── .editorconfig ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── appstore-build-publish.yml │ ├── dependabot-approve-merge.yml │ ├── fixup.yml │ ├── integration.yml │ ├── lint.yml │ ├── node.yml │ ├── phpunit.yml │ ├── pr-feedback.yml │ ├── psalm.yml │ └── reuse.yml ├── .gitignore ├── .l10nignore ├── .nextcloudignore ├── .php-cs-fixer.dist.php ├── .tx └── config ├── AUTHORS.md ├── CHANGELOG.md ├── COPYING ├── LICENSES ├── AGPL-3.0-or-later.txt ├── CC0-1.0.txt ├── LicenseRef-OIDFTrademarks.txt └── MIT.txt ├── README.md ├── REUSE.toml ├── appinfo ├── info.xml └── routes.php ├── babel.config.js ├── composer.json ├── composer.lock ├── css └── id4me-login.css ├── docs ├── token_events.md └── token_exchange.md ├── img └── app-dark.svg ├── krankerl.toml ├── l10n ├── .gitkeep ├── ar.js ├── ar.json ├── ast.js ├── ast.json ├── bg.js ├── bg.json ├── ca.js ├── ca.json ├── cs.js ├── cs.json ├── da.js ├── da.json ├── de.js ├── de.json ├── de_DE.js ├── de_DE.json ├── el.js ├── el.json ├── en_GB.js ├── en_GB.json ├── eo.js ├── eo.json ├── es.js ├── es.json ├── es_419.js ├── es_419.json ├── es_AR.js ├── es_AR.json ├── es_CL.js ├── es_CL.json ├── es_CO.js ├── es_CO.json ├── es_CR.js ├── es_CR.json ├── es_DO.js ├── es_DO.json ├── es_EC.js ├── es_EC.json ├── es_GT.js ├── es_GT.json ├── es_HN.js ├── es_HN.json ├── es_MX.js ├── es_MX.json ├── es_NI.js ├── es_NI.json ├── es_PA.js ├── es_PA.json ├── es_PE.js ├── es_PE.json ├── es_PR.js ├── es_PR.json ├── es_PY.js ├── es_PY.json ├── es_SV.js ├── es_SV.json ├── es_UY.js ├── es_UY.json ├── et_EE.js ├── et_EE.json ├── eu.js ├── eu.json ├── fa.js ├── fa.json ├── fi.js ├── fi.json ├── fr.js ├── fr.json ├── ga.js ├── ga.json ├── gl.js ├── gl.json ├── he.js ├── he.json ├── hr.js ├── hr.json ├── hu.js ├── hu.json ├── id.js ├── id.json ├── is.js ├── is.json ├── it.js ├── it.json ├── ja.js ├── ja.json ├── ka.js ├── ka.json ├── ka_GE.js ├── ka_GE.json ├── ko.js ├── ko.json ├── lt_LT.js ├── lt_LT.json ├── lv.js ├── lv.json ├── mk.js ├── mk.json ├── nb.js ├── nb.json ├── nl.js ├── nl.json ├── pl.js ├── pl.json ├── pt_BR.js ├── pt_BR.json ├── pt_PT.js ├── pt_PT.json ├── ro.js ├── ro.json ├── ru.js ├── ru.json ├── sc.js ├── sc.json ├── sk.js ├── sk.json ├── sl.js ├── sl.json ├── sq.js ├── sq.json ├── sr.js ├── sr.json ├── sv.js ├── sv.json ├── th.js ├── th.json ├── tr.js ├── tr.json ├── ug.js ├── ug.json ├── uk.js ├── uk.json ├── vi.js ├── vi.json ├── zh_CN.js ├── zh_CN.json ├── zh_HK.js ├── zh_HK.json ├── zh_TW.js └── zh_TW.json ├── lib ├── AppInfo │ └── Application.php ├── Command │ ├── DeleteProvider.php │ └── UpsertProvider.php ├── Controller │ ├── ApiController.php │ ├── BaseOidcController.php │ ├── Id4meController.php │ ├── LoginController.php │ ├── OcsApiController.php │ ├── SettingsController.php │ └── TimezoneController.php ├── Cron │ └── CleanupSessions.php ├── Db │ ├── Id4Me.php │ ├── Id4MeMapper.php │ ├── Provider.php │ ├── ProviderMapper.php │ ├── Session.php │ ├── SessionMapper.php │ ├── User.php │ └── UserMapper.php ├── Event │ ├── AttributeMappedEvent.php │ ├── ExchangedTokenRequestedEvent.php │ ├── ExternalTokenRequestedEvent.php │ ├── InternalTokenRequestedEvent.php │ ├── TokenObtainedEvent.php │ └── TokenValidatedEvent.php ├── Exception │ ├── GetExternalTokenFailedException.php │ └── TokenExchangeFailedException.php ├── Helper │ └── HttpClientHelper.php ├── Listener │ ├── ExchangedTokenRequestedListener.php │ ├── ExternalTokenRequestedListener.php │ ├── InternalTokenRequestedListener.php │ └── TimezoneHandlingListener.php ├── Migration │ ├── Version00001Date20200322114947.php │ ├── Version00003Date20200420120107.php │ ├── Version00004Date20200428102743.php │ ├── Version00005Date20200428123958.php │ ├── Version00006Date20210630120251.php │ ├── Version00007Date20210730170713.php │ ├── Version01021Date20220719114355.php │ ├── Version01022Date20221202161257.php │ ├── Version010303Date20230602125945.php │ ├── Version010304Date20231121102449.php │ └── Version010304Date20231130104459.php ├── Model │ └── Token.php ├── Service │ ├── DiscoveryService.php │ ├── ID4MeService.php │ ├── LdapService.php │ ├── LocalIdService.php │ ├── OIDCService.php │ ├── ProviderService.php │ ├── ProvisioningService.php │ ├── SettingsService.php │ └── TokenService.php ├── Settings │ ├── AdminSettings.php │ └── Section.php └── User │ ├── Backend.php │ ├── Provisioning │ ├── IProvisioningStrategy.php │ └── SelfEncodedTokenProvisioning.php │ └── Validator │ ├── IBearerTokenValidator.php │ ├── SelfEncodedValidator.php │ └── UserInfoValidator.php ├── package-lock.json ├── package.json ├── psalm.xml ├── src ├── components │ ├── AdminSettings.vue │ └── SettingsForm.vue ├── logger.js ├── main-settings.js └── timezone.js ├── stylelint.config.js ├── templates ├── admin-settings.php └── id4me │ └── login.php ├── tests ├── bootstrap.php ├── docker-compose.integration.yml ├── integration │ ├── Test.php │ └── config │ │ └── realm-export.json ├── phpunit.integration.xml ├── phpunit.xml ├── psalm-baseline.xml ├── stubs │ ├── oc_app.php │ ├── oc_authentication_token_provider.php │ ├── oc_core_command_base.php │ ├── oc_hooks_emitter.php │ ├── oc_user.php │ ├── oc_user_session.php │ ├── oc_util.php │ ├── oca_files_events.php │ ├── oca_oidc_events.php │ └── ocp_imapperexception.php └── unit │ ├── Db │ └── UserMapperTest.php │ └── Service │ ├── DiscoveryServiceTest.php │ ├── IdServiceTest.php │ ├── ProviderServiceTest.php │ └── ProvisioningServiceTest.php ├── vendor-bin └── mozart │ ├── composer.json │ └── composer.lock └── webpack.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = tab 8 | indent_size = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | module.exports = { 7 | globals: { 8 | appVersion: true, 9 | }, 10 | extends: [ 11 | '@nextcloud', 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # App maintainer 4 | /appinfo/info.xml @julien-nc @juliusknorr 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | version: 2 4 | updates: 5 | - package-ecosystem: composer 6 | directory: "/" 7 | schedule: 8 | interval: daily 9 | time: "03:00" 10 | timezone: Europe/Paris 11 | open-pull-requests-limit: 10 12 | ignore: 13 | - dependency-name: coenjacobs/mozart 14 | versions: 15 | - 0.6.0 16 | - 0.7.0 17 | - package-ecosystem: npm 18 | directory: "/" 19 | schedule: 20 | interval: daily 21 | time: "03:00" 22 | timezone: Europe/Paris 23 | open-pull-requests-limit: 10 24 | ignore: 25 | - dependency-name: webpack-cli 26 | versions: 27 | - 4.4.0 28 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-approve-merge.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Dependabot 10 | 11 | on: 12 | pull_request_target: 13 | branches: 14 | - main 15 | - master 16 | - stable* 17 | 18 | permissions: 19 | contents: read 20 | 21 | concurrency: 22 | group: dependabot-approve-merge-${{ github.head_ref || github.run_id }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | auto-approve-merge: 27 | if: github.actor == 'dependabot[bot]' 28 | runs-on: ubuntu-latest-low 29 | permissions: 30 | # for hmarr/auto-approve-action to approve PRs 31 | pull-requests: write 32 | 33 | steps: 34 | - name: Disabled on forks 35 | if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} 36 | run: | 37 | echo 'Can not approve PRs from forks' 38 | exit 1 39 | 40 | # Github actions bot approve 41 | - uses: hmarr/auto-approve-action@b40d6c9ed2fa10c9a2749eca7eb004418a705501 # v2 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | # Nextcloud bot approve and merge request 46 | - uses: ahmadnassri/action-dependabot-auto-merge@45fc124d949b19b6b8bf6645b6c9d55f4f9ac61a # v2 47 | with: 48 | target: minor 49 | github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN }} 50 | -------------------------------------------------------------------------------- /.github/workflows/fixup.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Block fixup and squash commits 10 | 11 | on: 12 | pull_request: 13 | types: [opened, ready_for_review, reopened, synchronize] 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: fixup-${{ github.head_ref || github.run_id }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | commit-message-check: 24 | if: github.event.pull_request.draft == false 25 | 26 | permissions: 27 | pull-requests: write 28 | name: Block fixup and squash commits 29 | 30 | runs-on: ubuntu-latest-low 31 | 32 | steps: 33 | - name: Run check 34 | uses: skjnldsv/block-fixup-merge-action@42d26e1b536ce61e5cf467d65fb76caf4aa85acf # v1 35 | with: 36 | repo-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: MIT 3 | name: Lint 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - main 10 | - master 11 | - stable* 12 | 13 | jobs: 14 | php: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | php-versions: ['8.0', '8.1', '8.2', '8.3'] 20 | 21 | name: php${{ matrix.php-versions }} 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - name: Set up php ${{ matrix.php-versions }} 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php-versions }} 29 | coverage: none 30 | 31 | - name: Lint 32 | run: composer run lint 33 | 34 | php-cs-fixer: 35 | runs-on: ubuntu-latest 36 | 37 | strategy: 38 | matrix: 39 | php-versions: ['8.0'] 40 | 41 | name: cs php${{ matrix.php-versions }} 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v2 45 | 46 | - name: Set up php 47 | uses: shivammathur/setup-php@v2 48 | with: 49 | php-version: ${{ matrix.php-versions }} 50 | coverage: none 51 | 52 | - name: Install dependencies 53 | run: composer i 54 | 55 | - name: Run coding standards check 56 | run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 ) 57 | 58 | node: 59 | runs-on: ubuntu-latest 60 | 61 | name: eslint node 62 | steps: 63 | - uses: actions/checkout@v2 64 | 65 | - name: Set up node 66 | uses: actions/setup-node@v2 67 | with: 68 | node-version: 16 69 | 70 | - name: Set up npm8 71 | run: npm i -g npm@8 72 | 73 | - name: Install dependencies 74 | run: npm ci 75 | 76 | - name: Lint 77 | run: npm run lint 78 | 79 | stylelint: 80 | runs-on: ubuntu-latest 81 | 82 | name: stylelint node 83 | steps: 84 | - uses: actions/checkout@v2 85 | 86 | - name: Set up node 87 | uses: actions/setup-node@v2 88 | with: 89 | node-version: 16 90 | 91 | - name: Set up npm8 92 | run: npm i -g npm@8 93 | 94 | - name: Install dependencies 95 | run: npm ci 96 | 97 | - name: Lint 98 | run: npm run stylelint 99 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Node 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: node-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | changes: 22 | runs-on: ubuntu-latest-low 23 | 24 | outputs: 25 | src: ${{ steps.changes.outputs.src}} 26 | 27 | steps: 28 | - uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0.0 29 | id: changes 30 | continue-on-error: true 31 | with: 32 | filters: | 33 | src: 34 | - '.github/workflows/**' 35 | - 'src/**' 36 | - 'appinfo/info.xml' 37 | - 'package.json' 38 | - 'package-lock.json' 39 | - 'tsconfig.json' 40 | - '**.js' 41 | - '**.ts' 42 | - '**.vue' 43 | 44 | build: 45 | runs-on: ubuntu-latest 46 | 47 | needs: changes 48 | if: needs.changes.outputs.src != 'false' 49 | 50 | name: NPM build 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 54 | 55 | - name: Read package.json node and npm engines version 56 | uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2 57 | id: versions 58 | with: 59 | fallbackNode: '^20' 60 | fallbackNpm: '^10' 61 | 62 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 63 | uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v3 64 | with: 65 | node-version: ${{ steps.versions.outputs.nodeVersion }} 66 | 67 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 68 | run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" 69 | 70 | - name: Install dependencies & build 71 | env: 72 | CYPRESS_INSTALL_BINARY: 0 73 | PUPPETEER_SKIP_DOWNLOAD: true 74 | run: | 75 | npm ci 76 | npm run build --if-present 77 | 78 | - name: Check webpack build changes 79 | run: | 80 | bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please recompile and commit the assets, see the section \"Show changes on failure\" for details' && exit 1)" 81 | 82 | - name: Show changes on failure 83 | if: failure() 84 | run: | 85 | git status 86 | git --no-pager diff 87 | exit 1 # make it red to grab attention 88 | 89 | summary: 90 | permissions: 91 | contents: none 92 | runs-on: ubuntu-latest-low 93 | needs: [changes, build] 94 | 95 | if: always() 96 | 97 | # This is the summary, we just avoid to rename it so that branch protection rules still match 98 | name: node 99 | 100 | steps: 101 | - name: Summary status 102 | run: if ${{ needs.changes.outputs.src != 'false' && needs.build.result != 'success' }}; then exit 1; fi 103 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: MIT 3 | name: PHPUnit 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - main 10 | - master 11 | - stable* 12 | 13 | env: 14 | APP_NAME: user_oidc 15 | 16 | 17 | jobs: 18 | integration: 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | php-versions: ['8.0', '8.1', '8.2'] 25 | databases: ['mysql'] 26 | server-versions: ['stable29', 'stable30', 'stable31', 'master'] 27 | exclude: 28 | - php-versions: 8.0 29 | server-versions: master 30 | - php-versions: 8.0 31 | server-versions: stable31 32 | - php-versions: 8.0 33 | server-versions: stable30 34 | include: 35 | - php-versions: 8.3 36 | databases: mysql 37 | server-versions: stable29 38 | - php-versions: 8.3 39 | databases: mysql 40 | server-versions: stable30 41 | - php-versions: 8.3 42 | databases: mysql 43 | server-versions: stable31 44 | - php-versions: 8.4 45 | databases: mysql 46 | server-versions: stable31 47 | - php-versions: 8.3 48 | databases: mysql 49 | server-versions: master 50 | - php-versions: 8.4 51 | databases: mysql 52 | server-versions: master 53 | 54 | name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }} 55 | 56 | services: 57 | postgres: 58 | image: postgres:14 59 | ports: 60 | - 4445:5432/tcp 61 | env: 62 | POSTGRES_USER: root 63 | POSTGRES_PASSWORD: rootpassword 64 | POSTGRES_DB: nextcloud 65 | options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5 66 | mysql: 67 | image: mariadb:10.5 68 | ports: 69 | - 4444:3306/tcp 70 | env: 71 | MYSQL_ROOT_PASSWORD: rootpassword 72 | options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 5 73 | 74 | steps: 75 | - name: Checkout server 76 | uses: actions/checkout@v2 77 | with: 78 | repository: nextcloud/server 79 | ref: ${{ matrix.server-versions }} 80 | 81 | - name: Checkout submodules 82 | shell: bash 83 | run: | 84 | auth_header="$(git config --local --get http.https://github.com/.extraheader)" 85 | git submodule sync --recursive 86 | git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 87 | 88 | - name: Checkout app 89 | uses: actions/checkout@v2 90 | with: 91 | path: apps/${{ env.APP_NAME }} 92 | 93 | - name: Set up php ${{ matrix.php-versions }} 94 | uses: shivammathur/setup-php@v2 95 | with: 96 | php-version: ${{ matrix.php-versions }} 97 | tools: phpunit 98 | extensions: zip, gd, mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql 99 | coverage: none 100 | 101 | - name: Set up PHPUnit 102 | working-directory: apps/${{ env.APP_NAME }} 103 | run: composer i 104 | 105 | - name: Set up Nextcloud 106 | run: | 107 | if [ "${{ matrix.databases }}" = "mysql" ]; then 108 | export DB_PORT=4444 109 | elif [ "${{ matrix.databases }}" = "pgsql" ]; then 110 | export DB_PORT=4445 111 | fi 112 | mkdir data 113 | ./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin 114 | ./occ app:enable --force ${{ env.APP_NAME }} 115 | php -S localhost:8080 & 116 | 117 | - name: PHPUnit 118 | working-directory: apps/${{ env.APP_NAME }} 119 | run: composer run test:unit 120 | -------------------------------------------------------------------------------- /.github/workflows/pr-feedback.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | # SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-FileCopyrightText: 2023 Marcel Klehr 8 | # SPDX-FileCopyrightText: 2023 Joas Schilling <213943+nickvergessen@users.noreply.github.com> 9 | # SPDX-FileCopyrightText: 2023 Daniel Kesselberg 10 | # SPDX-FileCopyrightText: 2023 Florian Steffens 11 | # SPDX-License-Identifier: MIT 12 | 13 | name: 'Ask for feedback on PRs' 14 | on: 15 | schedule: 16 | - cron: '30 1 * * *' 17 | 18 | jobs: 19 | pr-feedback: 20 | if: ${{ github.repository_owner == 'nextcloud' }} 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: The get-github-handles-from-website action 24 | uses: marcelklehr/get-github-handles-from-website-action@a739600f6b91da4957f51db0792697afbb2f143c # v1.0.0 25 | id: scrape 26 | with: 27 | website: 'https://nextcloud.com/team/' 28 | 29 | - name: Get blocklist 30 | id: blocklist 31 | run: | 32 | blocklist=$(curl https://raw.githubusercontent.com/nextcloud/.github/master/non-community-usernames.txt | paste -s -d, -) 33 | echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT" 34 | 35 | - uses: marcelklehr/pr-feedback-action@1883b38a033fb16f576875e0cf45f98b857655c4 36 | with: 37 | feedback-message: | 38 | Hello there, 39 | Thank you so much for taking the time and effort to create a pull request to our Nextcloud project. 40 | 41 | We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. 42 | 43 | Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6 44 | 45 | Thank you for contributing to Nextcloud and we hope to hear from you soon! 46 | 47 | (If you believe you should not receive this message, you can add yourself to the [blocklist](https://github.com/nextcloud/.github/blob/master/non-community-usernames.txt).) 48 | days-before-feedback: 14 49 | start-date: '2024-04-30' 50 | exempt-authors: '${{ steps.blocklist.outputs.blocklist }},${{ steps.scrape.outputs.users }}' 51 | exempt-bots: true 52 | -------------------------------------------------------------------------------- /.github/workflows/psalm.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Static analysis 10 | 11 | on: pull_request 12 | 13 | concurrency: 14 | group: psalm-${{ github.head_ref || github.run_id }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | static-analysis: 19 | runs-on: ubuntu-latest 20 | 21 | name: static-psalm-analysis 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 25 | 26 | - name: Set up php8.2 27 | uses: shivammathur/setup-php@6d7209f44a25a59e904b1ee9f3b0c33ab2cd888d # v2 28 | with: 29 | php-version: 8.2 30 | extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite 31 | coverage: none 32 | ini-file: development 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Install dependencies 37 | run: composer i 38 | 39 | - name: Run coding standards check 40 | run: composer run psalm 41 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | # SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. 7 | # 8 | # SPDX-License-Identifier: CC0-1.0 9 | 10 | name: REUSE Compliance Check 11 | 12 | on: [pull_request] 13 | 14 | jobs: 15 | reuse-compliance-check: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | with: 21 | persist-credentials: false 22 | 23 | - name: REUSE Compliance Check 24 | uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5.0.0 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | .idea 4 | *.iml 5 | /js 6 | /vendor/ 7 | /build/ 8 | node_modules/ 9 | /.php_cs.cache 10 | /.php-cs-fixer.cache 11 | /lib/Vendor 12 | tests/.phpunit.result.cache 13 | /vendor-bin/mozart/vendor 14 | -------------------------------------------------------------------------------- /.l10nignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # compiled vue templates 4 | js/ 5 | vendor/ 6 | vendor-bin/ 7 | -------------------------------------------------------------------------------- /.nextcloudignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | /AUTHORS.md 4 | /build 5 | /.editorconfig 6 | /.eslintrc.js 7 | /.git 8 | /.github 9 | /.gitignore 10 | /.php* 11 | /.travis.yml 12 | /.tx 13 | /.scrutinizer.yml 14 | /.nextcloudignore 15 | /babel.config.js 16 | /CONTRIBUTING.md 17 | /composer.json 18 | /composer.lock 19 | /composer.phar 20 | /karma.conf.js 21 | /krankerl.toml 22 | /l10n/no-php 23 | /Makefile 24 | /nbproject 25 | /node_modules 26 | /package.json 27 | /package-lock.json 28 | /psalm.xml 29 | /screenshots 30 | /src 31 | /stylelint.config.js 32 | /tests 33 | /vendor/bin 34 | /vendor/**/tests 35 | /webpack.common.js 36 | /webpack.dev.js 37 | /webpack.js 38 | /webpack.prod.js 39 | /vendor/bin 40 | /vendor-bin 41 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | getFinder() 17 | ->notPath('build') 18 | ->notPath('node_modules') 19 | ->notPath('l10n') 20 | ->notPath('src') 21 | ->notPath('vendor') 22 | ->notPath('lib/Vendor') 23 | ->in(__DIR__); 24 | return $config; 25 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | lang_map = hu_HU: hu, nb_NO: nb, sk_SK: sk, th_TH: th, ja_JP: ja, bg_BG: bg, cs_CZ: cs, fi_FI: fi 4 | 5 | [o:nextcloud:p:nextcloud:r:user_oidc] 6 | file_filter = translationfiles//user_oidc.po 7 | source_file = translationfiles/templates/user_oidc.pot 8 | source_lang = en 9 | type = PO 10 | 11 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | 5 | # Authors 6 | 7 | - Aaron Lewandowski 8 | - Ada <29815625+eyduh@users.noreply.github.com> 9 | - Andy Scherzinger 10 | - aro-lew <114079388+aro-lew@users.noreply.github.com> 11 | - Ben Kristinsson 12 | - Bernd.Rederlechner@t-systems.com 13 | - Caleb Maclennan 14 | - Carl Schwan 15 | - Christoph Wurst 16 | - Christoph Wurst 17 | - Clément OUDOT 18 | - Côme Chilliet <91878298+come-nc@users.noreply.github.com> 19 | - Edward Ly 20 | - Florian Klinger 21 | - Joas Schilling 22 | - John Molakvoæ 23 | - joselameira <60699452+joselameira@users.noreply.github.com> 24 | - Julien Veyssier 25 | - Julius Knorr 26 | - Louis Chemineau 27 | - Lukas Reschke 28 | - Marcel Klehr 29 | - Marvin Öhlerking 30 | - msk010 31 | - nc-fkl <149385388+nc-fkl@users.noreply.github.com> 32 | - Nextcloud bot 33 | - Pieter Fiers 34 | - Quentin de Longraye 35 | - Reigo Reinmets 36 | - Rello 37 | - rgfernandes 38 | - Robin Appelman 39 | - Roeland Jago Douma 40 | - Thibault Coupin 41 | - Tobias Wolter 42 | - Vincent Petry 43 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | version = 1 4 | SPDX-PackageName = "user_oidc" 5 | SPDX-PackageSupplier = "Nextcloud " 6 | SPDX-PackageDownloadLocation = "https://github.com/nextcloud/user_oidc" 7 | 8 | [[annotations]] 9 | path = ["l10n/**.js", "l10n/**.json"] 10 | precedence = "aggregate" 11 | SPDX-FileCopyrightText = "2023-2023 Nextcloud translators" 12 | SPDX-License-Identifier = "AGPL-3.0-or-later" 13 | 14 | [[annotations]] 15 | path = [".tx/config", "composer.json", "composer.lock", "package.json", "package-lock.json"] 16 | precedence = "aggregate" 17 | SPDX-FileCopyrightText = "2020 Nextcloud GmbH and Nextcloud contributors" 18 | SPDX-License-Identifier = "AGPL-3.0-or-later" 19 | 20 | [[annotations]] 21 | path = ["tests/integration/config/realm-export.json", "vendor-bin/mozart/composer.json", "vendor-bin/mozart/composer.lock"] 22 | precedence = "aggregate" 23 | SPDX-FileCopyrightText = "2021 Nextcloud GmbH and Nextcloud contributors" 24 | SPDX-License-Identifier = "AGPL-3.0-or-later" 25 | 26 | [[annotations]] 27 | path = ["tests/psalm-baseline.xml"] 28 | precedence = "aggregate" 29 | SPDX-FileCopyrightText = "2024 Nextcloud GmbH and Nextcloud contributors" 30 | SPDX-License-Identifier = "AGPL-3.0-or-later" 31 | 32 | [[annotations]] 33 | path = ["img/app-dark.svg"] 34 | precedence = "aggregate" 35 | SPDX-FileCopyrightText = "2017 OpenID Foundation" 36 | SPDX-License-Identifier = "LicenseRef-OIDFTrademarks" 37 | -------------------------------------------------------------------------------- /appinfo/info.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | user_oidc 8 | OpenID Connect user backend 9 | Use an OpenID Connect backend to login to your Nextcloud 10 | Allows flexible configuration of an OIDC server as Nextcloud login user backend. 11 | 7.2.0 12 | agpl 13 | Roeland Jago Douma 14 | Julius Härtl 15 | Bernd Rederlechner 16 | UserOIDC 17 | 18 | 19 | 20 | integration 21 | social 22 | https://github.com/nextcloud/user_oidc 23 | https://github.com/nextcloud/user_oidc/issues 24 | https://github.com/nextcloud/user_oidc 25 | 26 | 27 | 28 | 29 | OCA\UserOIDC\Settings\AdminSettings 30 | OCA\UserOIDC\Settings\Section 31 | 32 | 33 | 34 | OCA\UserOIDC\Cron\CleanupSessions 35 | 36 | 37 | OCA\UserOIDC\Command\UpsertProvider 38 | OCA\UserOIDC\Command\DeleteProvider 39 | 40 | 41 | -------------------------------------------------------------------------------- /appinfo/routes.php: -------------------------------------------------------------------------------- 1 | '(v1)', 11 | ]; 12 | 13 | return [ 14 | 'routes' => [ 15 | ['name' => 'login#login', 'url' => '/login/{providerId}', 'verb' => 'GET'], 16 | ['name' => 'login#code', 'url' => '/code', 'verb' => 'GET'], 17 | ['name' => 'login#singleLogoutService', 'url' => '/sls', 'verb' => 'GET'], 18 | ['name' => 'login#backChannelLogout', 'url' => '/backchannel-logout/{providerIdentifier}', 'verb' => 'POST'], 19 | 20 | ['name' => 'api#createUser', 'url' => '/user', 'verb' => 'POST'], 21 | ['name' => 'api#deleteUser', 'url' => '/user/{userId}', 'verb' => 'DELETE'], 22 | 23 | ['name' => 'id4me#showLogin', 'url' => '/id4me', 'verb' => 'GET'], 24 | ['name' => 'id4me#login', 'url' => '/id4me', 'verb' => 'POST'], 25 | ['name' => 'id4me#code', 'url' => '/id4me/code', 'verb' => 'GET'], 26 | 27 | ['name' => 'Settings#createProvider', 'url' => '/provider', 'verb' => 'POST'], 28 | ['name' => 'Settings#updateProvider', 'url' => '/provider/{providerId}', 'verb' => 'PUT'], 29 | ['name' => 'Settings#deleteProvider', 'url' => '/provider/{providerId}', 'verb' => 'DELETE'], 30 | ['name' => 'Settings#setID4ME', 'url' => '/provider/id4me', 'verb' => 'POST'], 31 | ['name' => 'Settings#setAdminConfig', 'url' => '/admin-config', 'verb' => 'POST'], 32 | 33 | ['name' => 'Timezone#setTimezone', 'url' => '/config/timezone', 'verb' => 'POST'], 34 | ], 35 | 'ocs' => [ 36 | ['name' => 'ocsApi#createUser', 'url' => '/api/{apiVersion}/user', 'verb' => 'POST', 'requirements' => $requirements], 37 | ['name' => 'ocsApi#deleteUser', 'url' => '/api/{apiVersion}/user/{userId}', 'verb' => 'DELETE', 'requirements' => $requirements], 38 | ], 39 | ]; 40 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | const babelConfig = require('@nextcloud/babel-config') 7 | 8 | module.exports = babelConfig 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextcloud/user_oidc", 3 | "description": "OIDC backend for Nextcloud", 4 | "license": "AGPL3", 5 | "config": { 6 | "optimize-autoloader": true, 7 | "classmap-authoritative": true, 8 | "allow-plugins": { 9 | "bamarni/composer-bin-plugin": true 10 | }, 11 | "platform": { 12 | "php": "8.0" 13 | } 14 | }, 15 | "scripts": { 16 | "cs:fix": "php-cs-fixer fix", 17 | "cs:check": "php-cs-fixer fix --dry-run --diff", 18 | "lint": "find . -name \\*.php ! -path './vendor/*' ! -path './vendor-bin/*' ! -path './lib/Vendor/*' -exec php -l \"{}\" \\;", 19 | "test:unit": "phpunit -c tests/phpunit.xml", 20 | "psalm": "psalm.phar --no-cache", 21 | "psalm:update-baseline": "psalm.phar --threads=1 --update-baseline", 22 | "psalm:update-baseline:force": "psalm.phar --threads=1 --update-baseline --set-baseline=tests/psalm-baseline.xml", 23 | "post-install-cmd": [ 24 | "@composer bin all install --ansi", 25 | "\"vendor/bin/mozart\" compose", 26 | "composer dump-autoload" 27 | ], 28 | "post-update-cmd": [ 29 | "@composer bin all install --ansi", 30 | "\"vendor/bin/mozart\" compose", 31 | "composer dump-autoload" 32 | ] 33 | }, 34 | "require": { 35 | "id4me/id4me-rp": "^1.2", 36 | "firebase/php-jwt": "^6.8.1", 37 | "bamarni/composer-bin-plugin": "^1.4" 38 | }, 39 | "require-dev": { 40 | "nextcloud/coding-standard": "^1.0.0", 41 | "symfony/event-dispatcher": "^4", 42 | "phpunit/phpunit": "^9.5", 43 | "nextcloud/ocp": "dev-stable28", 44 | "psalm/phar": "^5.20" 45 | }, 46 | "extra": { 47 | "mozart": { 48 | "dep_namespace": "OCA\\UserOIDC\\Vendor\\", 49 | "dep_directory": "/lib/Vendor/", 50 | "classmap_directory": "/lib/autoload/", 51 | "classmap_prefix": "NEXTCLOUD_USER_OIDC_", 52 | "packages": [ 53 | "firebase/php-jwt" 54 | ], 55 | "delete_vendor_directories": true 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /css/id4me-login.css: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | #id4me-submit { 7 | position: relative; 8 | right: -6px; 9 | top: 5px; 10 | border: 2px solid var(--color-border-maxcontrast); 11 | border-bottom-right-radius: var(--border-radius-large); 12 | border-top-right-radius: var(--border-radius-large); 13 | } 14 | 15 | #domain { 16 | border-bottom-left-radius: var(--border-radius-large); 17 | border-top-left-radius: var(--border-radius-large); 18 | } 19 | -------------------------------------------------------------------------------- /docs/token_events.md: -------------------------------------------------------------------------------- 1 | 5 | # Token events 6 | 7 | Other apps can ask user_oidc for the login token (the one obtained when logging in Nextcloud using an external Oidc provider) 8 | or a token from the "oidc" app (https://apps.nextcloud.com/apps/oidc). Those tokens might be useful for integration apps 9 | that need to authenticate against another service's API. 10 | 11 | ## Get a token from an external provider 12 | 13 | This is possible to get the current user's login token by emitting an event which will be received by user_oidc. 14 | This is also possible to ask user_oidc to perform a token exchange, see token_exchange.md. 15 | In this paragraph, we only talk about getting the login token (or a refreshed one), meaning it is delivered for the Oidc 16 | client that was used when the user logged in Nextcloud. 17 | 18 | To get the token obtained on login, user_oidc needs to store it and refresh it when needed. This is disabled by default. 19 | You can enable this with: 20 | ``` bash 21 | sudo -u www-data php /var/www/nextcloud/occ config:app:set --value=1 user_oidc store_login_token 22 | ``` 23 | This login token is refreshed by user_oidc when needed. So the token you will get by emitting the event will be valid (not expired). 24 | 25 | Any Nextcloud app can emit the `ExternalTokenRequestedEvent` event: 26 | ```php 27 | if (class_exists(OCA\UserOIDC\Event\ExternalTokenRequestedEvent::class)) { 28 | $event = new OCA\UserOIDC\Event\ExternalTokenRequestedEvent(); 29 | try { 30 | $this->eventDispatcher->dispatchTyped($event); 31 | } catch (OCA\UserOIDC\Exception\GetExternalTokenFailedException $e) { 32 | $this->logger->debug('Failed to get external token: ' . $e->getMessage()); 33 | $error = $e->getError(); 34 | $errorDescription = $e->getErrorDescription(); 35 | if ($error && $errorDescription) { 36 | $this->logger->debug('Token exchange error response from the IdP: ' . $error . ' (' . $errorDescription . ')'); 37 | } 38 | } 39 | $token = $event->getToken(); 40 | if ($token === null) { 41 | $this->logger->debug('There was no token found in the session'); 42 | } else { 43 | $this->logger->debug('Obtained a token that expires in ' . $token->getExpiresInFromNow()); 44 | // use the token 45 | $accessToken = $token->getAccessToken(); 46 | $idToken = $token->getIdToken(); 47 | } 48 | } else { 49 | $this->logger->debug('The user_oidc app is not installed/available'); 50 | } 51 | ``` 52 | 53 | ## Get an internal token 54 | 55 | To get a token from the internal Oidc provider ("oidc" app), the `InternalTokenRequestedEvent` can be emitted. 56 | No need to enable `store_login_token`. 57 | 58 | ```php 59 | if (class_exists(OCA\UserOIDC\Event\InternalTokenRequestedEvent::class)) { 60 | $event = new OCA\UserOIDC\Event\InternalTokenRequestedEvent('my_target_audience'); 61 | $this->eventDispatcher->dispatchTyped($event); 62 | $token = $event->getToken(); 63 | if ($token === null) { 64 | $this->logger->debug('InternalTokenRequestedEvent, no token has been obtained from the oidc app'); 65 | } else { 66 | $this->logger->debug('Obtained a token that expires in ' . $token->getExpiresInFromNow()); 67 | // use the token 68 | $accessToken = $token->getAccessToken(); 69 | $idToken = $token->getIdToken(); 70 | } 71 | } else { 72 | $this->logger->debug('The user_oidc app is not installed/available'); 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/token_exchange.md: -------------------------------------------------------------------------------- 1 | 5 | ### Token exchange 6 | 7 | If your IdP supports token exchange, user_oidc can exchange the login token against another token. 8 | 9 | :warning: The token exchange feature requires to store the login token which is disabled by default. You can enable it with: 10 | ``` bash 11 | sudo -u www-data php /var/www/nextcloud/occ config:app:set --value=1 user_oidc store_login_token 12 | ``` 13 | 14 | Keycloak supports token exchange if its "Preview" mode is enabled. See https://www.keycloak.org/securing-apps/token-exchange . 15 | 16 | :warning: Your IdP need to be configured accordingly. For example, Keycloak requires that token exchange is explicitely 17 | authorized for the target Oidc client. 18 | 19 | The type of token exchange that user_oidc can perform is "Internal token to internal token" 20 | (https://www.keycloak.org/securing-apps/token-exchange#_internal-token-to-internal-token-exchange). 21 | This means you can exchange a token delivered for an audience "A" for a token delivered for an audience "B". 22 | In other words, you can get a token of a different Oidc client than the one you configured in user_oidc. 23 | 24 | In short, you don't need the client ID and client secret of the target audience's client. 25 | Providing a token for the audience "A" (the login token) is enough to obtain a token for the audience "B". 26 | 27 | user_oidc is storing the login token in the user's Nextcloud session and takes care of refreshing it when needed. 28 | When another app wants to exchange the current login token for another one, 29 | it can dispatch the `OCA\UserOIDC\Event\ExchangedTokenRequestedEvent` event. 30 | The exchanged token is immediately stored in the event object itself. 31 | 32 | ```php 33 | if (class_exists(OCA\UserOIDC\Event\ExchangedTokenRequestedEvent:class)) { 34 | $event = new OCA\UserOIDC\Event\ExchangedTokenRequestedEvent('my_target_audience'); 35 | try { 36 | $this->eventDispatcher->dispatchTyped($event); 37 | } catch (OCA\UserOIDC\Exception\TokenExchangeFailedException $e) { 38 | $this->logger->debug('Failed to exchange token: ' . $e->getMessage()); 39 | $error = $e->getError(); 40 | $errorDescription = $e->getErrorDescription(); 41 | if ($error && $errorDescription) { 42 | $this->logger->debug('Token exchange error response from the IdP: ' . $error . ' (' . $errorDescription . ')'); 43 | } 44 | } 45 | $token = $event->getToken(); 46 | if ($token === null) { 47 | $this->logger->debug('ExchangedTokenRequestedEvent event has not been caught by user_oidc'); 48 | } else { 49 | $this->logger->debug('Obtained a token that expires in ' . $token->getExpiresInFromNow()); 50 | // use the token 51 | $accessToken = $token->getAccessToken(); 52 | $idToken = $token->getIdToken(); 53 | } 54 | } else { 55 | $this->logger->debug('The user_oidc app is not installed/available'); 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /img/app-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /krankerl.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | [package] 4 | before_cmds = [ 5 | "composer install --no-dev -o", 6 | "npm ci", 7 | "npm run build", 8 | ] 9 | -------------------------------------------------------------------------------- /l10n/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/user_oidc/900b93fb2396cbbc470425e742314349bc067a1d/l10n/.gitkeep -------------------------------------------------------------------------------- /l10n/ast.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID de veceru", 5 | "Update" : "Anovar", 6 | "Remove" : "Quitar", 7 | "Submit" : "Unviar", 8 | "Client secret" : "Secretu de veceru", 9 | "Scope" : "Ámbitu", 10 | "Attribute mapping" : "Mapéu d'atributos", 11 | "Cancel" : "Encaboxar", 12 | "Domain" : "Dominiu" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/ast.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID de veceru", 3 | "Update" : "Anovar", 4 | "Remove" : "Quitar", 5 | "Submit" : "Unviar", 6 | "Client secret" : "Secretu de veceru", 7 | "Scope" : "Ámbitu", 8 | "Attribute mapping" : "Mapéu d'atributos", 9 | "Cancel" : "Encaboxar", 10 | "Domain" : "Dominiu" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/bg.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Идентификатор на клиент", 5 | "Update" : "Обновяване", 6 | "Remove" : "Премахване", 7 | "Submit" : "Изпращане", 8 | "Client secret" : "Тайна на клиент", 9 | "Scope" : "Обхват", 10 | "Attribute mapping" : "Съпоставяне на атрибут", 11 | "Cancel" : "Отказ", 12 | "Domain" : "Домейн" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/bg.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Идентификатор на клиент", 3 | "Update" : "Обновяване", 4 | "Remove" : "Премахване", 5 | "Submit" : "Изпращане", 6 | "Client secret" : "Тайна на клиент", 7 | "Scope" : "Обхват", 8 | "Attribute mapping" : "Съпоставяне на атрибут", 9 | "Cancel" : "Отказ", 10 | "Domain" : "Домейн" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/ca.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del client", 5 | "Update" : "Actualitza", 6 | "Remove" : "Suprimir", 7 | "Submit" : "Envia", 8 | "Client secret" : "Secret del client", 9 | "Scope" : "Abast", 10 | "Attribute mapping" : "Mapatge d’atributs", 11 | "Cancel" : "Cancel·la", 12 | "Domain" : "Domini" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/ca.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del client", 3 | "Update" : "Actualitza", 4 | "Remove" : "Suprimir", 5 | "Submit" : "Envia", 6 | "Client secret" : "Secret del client", 7 | "Scope" : "Abast", 8 | "Attribute mapping" : "Mapatge d’atributs", 9 | "Cancel" : "Cancel·la", 10 | "Domain" : "Domini" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/eo.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Klientidentigilo", 5 | "Update" : "Ĝisdatigi", 6 | "Submit" : "Sendi", 7 | "Client secret" : "Klientosekreto", 8 | "Scope" : "Amplekso", 9 | "Domain" : "Domajno" 10 | }, 11 | "nplurals=2; plural=(n != 1);"); 12 | -------------------------------------------------------------------------------- /l10n/eo.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Klientidentigilo", 3 | "Update" : "Ĝisdatigi", 4 | "Submit" : "Sendi", 5 | "Client secret" : "Klientosekreto", 6 | "Scope" : "Amplekso", 7 | "Domain" : "Domajno" 8 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 9 | } -------------------------------------------------------------------------------- /l10n/es_419.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_419.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_AR.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Remove" : "Eliminar", 7 | "Submit" : "Enviar", 8 | "Client secret" : "Secreto del cliente", 9 | "Scope" : "Alcance", 10 | "Attribute mapping" : "Mapeo del atributo", 11 | "Domain" : "Dominio" 12 | }, 13 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 14 | -------------------------------------------------------------------------------- /l10n/es_AR.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Remove" : "Eliminar", 5 | "Submit" : "Enviar", 6 | "Client secret" : "Secreto del cliente", 7 | "Scope" : "Alcance", 8 | "Attribute mapping" : "Mapeo del atributo", 9 | "Domain" : "Dominio" 10 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 11 | } -------------------------------------------------------------------------------- /l10n/es_CL.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Remove" : "Remover", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_CL.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Remove" : "Remover", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_CO.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_CO.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_CR.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_CR.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_DO.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_DO.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_EC.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Cancel" : "Cancelar", 11 | "Domain" : "Dominio" 12 | }, 13 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 14 | -------------------------------------------------------------------------------- /l10n/es_EC.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Cancel" : "Cancelar", 9 | "Domain" : "Dominio" 10 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 11 | } -------------------------------------------------------------------------------- /l10n/es_GT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_GT.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_HN.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Submit" : "Enviar", 6 | "Client secret" : "Secreto del cliente", 7 | "Scope" : "Alcance", 8 | "Attribute mapping" : "Mapeo del atributo", 9 | "Domain" : "Dominio" 10 | }, 11 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 12 | -------------------------------------------------------------------------------- /l10n/es_HN.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Submit" : "Enviar", 4 | "Client secret" : "Secreto del cliente", 5 | "Scope" : "Alcance", 6 | "Attribute mapping" : "Mapeo del atributo", 7 | "Domain" : "Dominio" 8 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 9 | } -------------------------------------------------------------------------------- /l10n/es_MX.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Remove" : "Borrar", 7 | "Submit" : "Enviar", 8 | "Client secret" : "Secreto del cliente", 9 | "Scope" : "Ámbito", 10 | "Attribute mapping" : "Mapeo del atributo", 11 | "Cancel" : "Cancelar", 12 | "Domain" : "Dominio" 13 | }, 14 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 15 | -------------------------------------------------------------------------------- /l10n/es_MX.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Remove" : "Borrar", 5 | "Submit" : "Enviar", 6 | "Client secret" : "Secreto del cliente", 7 | "Scope" : "Ámbito", 8 | "Attribute mapping" : "Mapeo del atributo", 9 | "Cancel" : "Cancelar", 10 | "Domain" : "Dominio" 11 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 12 | } -------------------------------------------------------------------------------- /l10n/es_NI.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_NI.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_PA.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_PA.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_PE.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_PE.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_PR.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_PR.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_PY.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_PY.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_SV.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_SV.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/es_UY.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID del cliente", 5 | "Update" : "Actualizar", 6 | "Submit" : "Enviar", 7 | "Client secret" : "Secreto del cliente", 8 | "Scope" : "Alcance", 9 | "Attribute mapping" : "Mapeo del atributo", 10 | "Domain" : "Dominio" 11 | }, 12 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 13 | -------------------------------------------------------------------------------- /l10n/es_UY.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID del cliente", 3 | "Update" : "Actualizar", 4 | "Submit" : "Enviar", 5 | "Client secret" : "Secreto del cliente", 6 | "Scope" : "Alcance", 7 | "Attribute mapping" : "Mapeo del atributo", 8 | "Domain" : "Dominio" 9 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 10 | } -------------------------------------------------------------------------------- /l10n/et_EE.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Kliendi ID", 5 | "Update" : "Uuenda", 6 | "Remove" : "Eemalda", 7 | "Submit" : "Saada", 8 | "Client secret" : "Kliendi salasõna", 9 | "Scope" : "Skoop", 10 | "Cancel" : "Tühista", 11 | "Domain" : "Domeen" 12 | }, 13 | "nplurals=2; plural=(n != 1);"); 14 | -------------------------------------------------------------------------------- /l10n/et_EE.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Kliendi ID", 3 | "Update" : "Uuenda", 4 | "Remove" : "Eemalda", 5 | "Submit" : "Saada", 6 | "Client secret" : "Kliendi salasõna", 7 | "Scope" : "Skoop", 8 | "Cancel" : "Tühista", 9 | "Domain" : "Domeen" 10 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 11 | } -------------------------------------------------------------------------------- /l10n/eu.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Bezeroaren IDa", 5 | "Update" : "Eguneratu", 6 | "Remove" : "Kendu", 7 | "Submit" : "Bidali", 8 | "Client secret" : "Bezeroaren sekretua", 9 | "Scope" : "Esparrua", 10 | "Attribute mapping" : "Atributu-esleitzea", 11 | "Cancel" : "Utzi", 12 | "Domain" : "Domeinua" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/eu.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Bezeroaren IDa", 3 | "Update" : "Eguneratu", 4 | "Remove" : "Kendu", 5 | "Submit" : "Bidali", 6 | "Client secret" : "Bezeroaren sekretua", 7 | "Scope" : "Esparrua", 8 | "Attribute mapping" : "Atributu-esleitzea", 9 | "Cancel" : "Utzi", 10 | "Domain" : "Domeinua" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/fa.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "شناسه مشتری", 5 | "Update" : "به‌روزرسانی", 6 | "Remove" : "حذف", 7 | "Submit" : "ارسال", 8 | "Client secret" : "رمز مشتری", 9 | "Scope" : "حوزه", 10 | "Attribute mapping" : "Attribute mapping", 11 | "Cancel" : "ردکردن", 12 | "Domain" : "دامنه" 13 | }, 14 | "nplurals=2; plural=(n > 1);"); 15 | -------------------------------------------------------------------------------- /l10n/fa.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "شناسه مشتری", 3 | "Update" : "به‌روزرسانی", 4 | "Remove" : "حذف", 5 | "Submit" : "ارسال", 6 | "Client secret" : "رمز مشتری", 7 | "Scope" : "حوزه", 8 | "Attribute mapping" : "Attribute mapping", 9 | "Cancel" : "ردکردن", 10 | "Domain" : "دامنه" 11 | },"pluralForm" :"nplurals=2; plural=(n > 1);" 12 | } -------------------------------------------------------------------------------- /l10n/fi.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Asiakas-ID", 5 | "Update" : "Päivitä", 6 | "Remove" : "Poista", 7 | "Submit" : "Lähetä", 8 | "Client secret" : "Asiakassalasana", 9 | "Scope" : "Näkyvyysalue", 10 | "Cancel" : "Peruuta", 11 | "Domain" : "Domaini" 12 | }, 13 | "nplurals=2; plural=(n != 1);"); 14 | -------------------------------------------------------------------------------- /l10n/fi.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Asiakas-ID", 3 | "Update" : "Päivitä", 4 | "Remove" : "Poista", 5 | "Submit" : "Lähetä", 6 | "Client secret" : "Asiakassalasana", 7 | "Scope" : "Näkyvyysalue", 8 | "Cancel" : "Peruuta", 9 | "Domain" : "Domaini" 10 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 11 | } -------------------------------------------------------------------------------- /l10n/fr.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID Client", 5 | "Update" : "Mise à jour", 6 | "Remove" : "Supprimer", 7 | "Submit" : "Envoyer", 8 | "Client secret" : "Secret client", 9 | "Scope" : "Portée", 10 | "Attribute mapping" : "Mappage des attributs", 11 | "Cancel" : "Annuler", 12 | "Domain" : "Domaine" 13 | }, 14 | "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 15 | -------------------------------------------------------------------------------- /l10n/fr.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID Client", 3 | "Update" : "Mise à jour", 4 | "Remove" : "Supprimer", 5 | "Submit" : "Envoyer", 6 | "Client secret" : "Secret client", 7 | "Scope" : "Portée", 8 | "Attribute mapping" : "Mappage des attributs", 9 | "Cancel" : "Annuler", 10 | "Domain" : "Domaine" 11 | },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 12 | } -------------------------------------------------------------------------------- /l10n/gl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Login with %1s" : "Acceder con %1s", 5 | "ID4Me is disabled" : "ID4Me está desactivado", 6 | "Invalid OpenID domain" : "O dominio OpenID non é válido", 7 | "Invalid authority issuer" : "A autoridade emisora non é válida", 8 | "Multiple authority found" : "Atopáronse varias autoridades", 9 | "The received state does not match the expected value." : "O estado recibido non coincide co valor agardado.", 10 | "Authority not found" : "Non se atopou a autoridade", 11 | "The received token is expired." : "O testemuño recibido caducou", 12 | "Client ID" : "ID de cliente", 13 | "Update" : "Actualizar", 14 | "Remove" : "Retirar", 15 | "Submit" : "Enviar", 16 | "Client secret" : "Segredo do cliente", 17 | "Scope" : "Ámbito", 18 | "Attribute mapping" : "Asignación de atributos", 19 | "Cancel" : "Cancelar", 20 | "Domain" : "Dominio" 21 | }, 22 | "nplurals=2; plural=(n != 1);"); 23 | -------------------------------------------------------------------------------- /l10n/gl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Login with %1s" : "Acceder con %1s", 3 | "ID4Me is disabled" : "ID4Me está desactivado", 4 | "Invalid OpenID domain" : "O dominio OpenID non é válido", 5 | "Invalid authority issuer" : "A autoridade emisora non é válida", 6 | "Multiple authority found" : "Atopáronse varias autoridades", 7 | "The received state does not match the expected value." : "O estado recibido non coincide co valor agardado.", 8 | "Authority not found" : "Non se atopou a autoridade", 9 | "The received token is expired." : "O testemuño recibido caducou", 10 | "Client ID" : "ID de cliente", 11 | "Update" : "Actualizar", 12 | "Remove" : "Retirar", 13 | "Submit" : "Enviar", 14 | "Client secret" : "Segredo do cliente", 15 | "Scope" : "Ámbito", 16 | "Attribute mapping" : "Asignación de atributos", 17 | "Cancel" : "Cancelar", 18 | "Domain" : "Dominio" 19 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 20 | } -------------------------------------------------------------------------------- /l10n/he.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "מזהה לקו", 5 | "Update" : "עדכון", 6 | "Remove" : "הסרה", 7 | "Submit" : "שליחה", 8 | "Client secret" : "סוד לקוח", 9 | "Scope" : "היקף", 10 | "Attribute mapping" : "מיפוי מאפיינים", 11 | "Cancel" : "ביטול", 12 | "Domain" : "שם תחום" 13 | }, 14 | "nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;"); 15 | -------------------------------------------------------------------------------- /l10n/he.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "מזהה לקו", 3 | "Update" : "עדכון", 4 | "Remove" : "הסרה", 5 | "Submit" : "שליחה", 6 | "Client secret" : "סוד לקוח", 7 | "Scope" : "היקף", 8 | "Attribute mapping" : "מיפוי מאפיינים", 9 | "Cancel" : "ביטול", 10 | "Domain" : "שם תחום" 11 | },"pluralForm" :"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;" 12 | } -------------------------------------------------------------------------------- /l10n/hr.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID klijenta", 5 | "Update" : "Ažuriraj", 6 | "Remove" : "Ukloni", 7 | "Submit" : "Šalji", 8 | "Client secret" : "Tajni ključ klijenta", 9 | "Scope" : "Opseg", 10 | "Attribute mapping" : "Mapiranje atributa", 11 | "Cancel" : "Odustani", 12 | "Domain" : "Domena" 13 | }, 14 | "nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;"); 15 | -------------------------------------------------------------------------------- /l10n/hr.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID klijenta", 3 | "Update" : "Ažuriraj", 4 | "Remove" : "Ukloni", 5 | "Submit" : "Šalji", 6 | "Client secret" : "Tajni ključ klijenta", 7 | "Scope" : "Opseg", 8 | "Attribute mapping" : "Mapiranje atributa", 9 | "Cancel" : "Odustani", 10 | "Domain" : "Domena" 11 | },"pluralForm" :"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;" 12 | } -------------------------------------------------------------------------------- /l10n/hu.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Kliensazonosító", 5 | "Update" : "Frissítés", 6 | "Remove" : "Eltávolítás", 7 | "Submit" : "Beküldés", 8 | "Client secret" : "Klienstitok", 9 | "Scope" : "Hatókör", 10 | "Attribute mapping" : "Attribútum-hozzárendelés", 11 | "Cancel" : "Mégse", 12 | "Domain" : "Domain" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/hu.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Kliensazonosító", 3 | "Update" : "Frissítés", 4 | "Remove" : "Eltávolítás", 5 | "Submit" : "Beküldés", 6 | "Client secret" : "Klienstitok", 7 | "Scope" : "Hatókör", 8 | "Attribute mapping" : "Attribútum-hozzárendelés", 9 | "Cancel" : "Mégse", 10 | "Domain" : "Domain" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/id.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID Klien", 5 | "Update" : "Perbarui", 6 | "Remove" : "Hapus", 7 | "Client secret" : "Rahasia klien", 8 | "Scope" : "Skop", 9 | "Cancel" : "Batal", 10 | "Domain" : "Domain" 11 | }, 12 | "nplurals=1; plural=0;"); 13 | -------------------------------------------------------------------------------- /l10n/id.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID Klien", 3 | "Update" : "Perbarui", 4 | "Remove" : "Hapus", 5 | "Client secret" : "Rahasia klien", 6 | "Scope" : "Skop", 7 | "Cancel" : "Batal", 8 | "Domain" : "Domain" 9 | },"pluralForm" :"nplurals=1; plural=0;" 10 | } -------------------------------------------------------------------------------- /l10n/is.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Biðlaraauðkenni", 5 | "Update" : "Uppfæra", 6 | "Remove" : "Fjarlægja", 7 | "Submit" : "Senda inn", 8 | "Client secret" : "Leynilykill biðlara", 9 | "Scope" : "Umfang", 10 | "Attribute mapping" : "Vörpun eiginda", 11 | "Cancel" : "Hætta við", 12 | "Domain" : "Lén" 13 | }, 14 | "nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);"); 15 | -------------------------------------------------------------------------------- /l10n/is.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Biðlaraauðkenni", 3 | "Update" : "Uppfæra", 4 | "Remove" : "Fjarlægja", 5 | "Submit" : "Senda inn", 6 | "Client secret" : "Leynilykill biðlara", 7 | "Scope" : "Umfang", 8 | "Attribute mapping" : "Vörpun eiginda", 9 | "Cancel" : "Hætta við", 10 | "Domain" : "Lén" 11 | },"pluralForm" :"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);" 12 | } -------------------------------------------------------------------------------- /l10n/it.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID client", 5 | "Update" : "Aggiorna", 6 | "Remove" : "Rimuovi", 7 | "Submit" : "Invia", 8 | "Client secret" : "Segreto del client", 9 | "Scope" : "Ambito", 10 | "Attribute mapping" : "Associazione degli attributi", 11 | "Cancel" : "Annulla", 12 | "Domain" : "Dominio" 13 | }, 14 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 15 | -------------------------------------------------------------------------------- /l10n/it.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID client", 3 | "Update" : "Aggiorna", 4 | "Remove" : "Rimuovi", 5 | "Submit" : "Invia", 6 | "Client secret" : "Segreto del client", 7 | "Scope" : "Ambito", 8 | "Attribute mapping" : "Associazione degli attributi", 9 | "Cancel" : "Annulla", 10 | "Domain" : "Dominio" 11 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 12 | } -------------------------------------------------------------------------------- /l10n/ja.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "クライアント ID", 5 | "Update" : "更新", 6 | "Remove" : "削除", 7 | "Submit" : "提出する", 8 | "Client secret" : "クライアントシークレット", 9 | "Scope" : "スコープ", 10 | "Attribute mapping" : "属性マッピング", 11 | "Cancel" : "キャンセル", 12 | "Domain" : "ドメイン" 13 | }, 14 | "nplurals=1; plural=0;"); 15 | -------------------------------------------------------------------------------- /l10n/ja.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "クライアント ID", 3 | "Update" : "更新", 4 | "Remove" : "削除", 5 | "Submit" : "提出する", 6 | "Client secret" : "クライアントシークレット", 7 | "Scope" : "スコープ", 8 | "Attribute mapping" : "属性マッピング", 9 | "Cancel" : "キャンセル", 10 | "Domain" : "ドメイン" 11 | },"pluralForm" :"nplurals=1; plural=0;" 12 | } -------------------------------------------------------------------------------- /l10n/ka.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Client ID", 5 | "Update" : "Update", 6 | "Remove" : "Remove", 7 | "Submit" : "Submit", 8 | "Client secret" : "Client secret", 9 | "Scope" : "Scope", 10 | "Attribute mapping" : "Attribute mapping", 11 | "Cancel" : "Cancel", 12 | "Domain" : "Domain" 13 | }, 14 | "nplurals=2; plural=(n!=1);"); 15 | -------------------------------------------------------------------------------- /l10n/ka.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Client ID", 3 | "Update" : "Update", 4 | "Remove" : "Remove", 5 | "Submit" : "Submit", 6 | "Client secret" : "Client secret", 7 | "Scope" : "Scope", 8 | "Attribute mapping" : "Attribute mapping", 9 | "Cancel" : "Cancel", 10 | "Domain" : "Domain" 11 | },"pluralForm" :"nplurals=2; plural=(n!=1);" 12 | } -------------------------------------------------------------------------------- /l10n/ka_GE.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "კლიენტის ID", 5 | "Update" : "განახლება", 6 | "Submit" : "გაგზავნა", 7 | "Client secret" : "კლიენტის საიდუმლო", 8 | "Scope" : "ფარგლები", 9 | "Attribute mapping" : "ატრიბუტების ბმები", 10 | "Cancel" : "გაუქმება", 11 | "Domain" : "დომენი" 12 | }, 13 | "nplurals=2; plural=(n!=1);"); 14 | -------------------------------------------------------------------------------- /l10n/ka_GE.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "კლიენტის ID", 3 | "Update" : "განახლება", 4 | "Submit" : "გაგზავნა", 5 | "Client secret" : "კლიენტის საიდუმლო", 6 | "Scope" : "ფარგლები", 7 | "Attribute mapping" : "ატრიბუტების ბმები", 8 | "Cancel" : "გაუქმება", 9 | "Domain" : "დომენი" 10 | },"pluralForm" :"nplurals=2; plural=(n!=1);" 11 | } -------------------------------------------------------------------------------- /l10n/lt_LT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Kliento ID", 5 | "Update" : "Atnaujinti", 6 | "Remove" : "Šalinti", 7 | "Submit" : "Pateikti", 8 | "Client secret" : "Kliento paslaptis", 9 | "Scope" : "Leidimas", 10 | "Cancel" : "Atsisakyti", 11 | "Domain" : "Domenas" 12 | }, 13 | "nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);"); 14 | -------------------------------------------------------------------------------- /l10n/lt_LT.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Kliento ID", 3 | "Update" : "Atnaujinti", 4 | "Remove" : "Šalinti", 5 | "Submit" : "Pateikti", 6 | "Client secret" : "Kliento paslaptis", 7 | "Scope" : "Leidimas", 8 | "Cancel" : "Atsisakyti", 9 | "Domain" : "Domenas" 10 | },"pluralForm" :"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);" 11 | } -------------------------------------------------------------------------------- /l10n/lv.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Klienta ID", 5 | "Update" : "Atjaunināt", 6 | "Remove" : "Izņemt", 7 | "Submit" : "Iesniegt", 8 | "Scope" : "Darbības joma", 9 | "Authentication and Access Control Settings" : "Autentificēšanās un piekļuves vadības iestatījumi", 10 | "Cancel" : "Atcelt", 11 | "Domain" : "Domain" 12 | }, 13 | "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"); 14 | -------------------------------------------------------------------------------- /l10n/lv.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Klienta ID", 3 | "Update" : "Atjaunināt", 4 | "Remove" : "Izņemt", 5 | "Submit" : "Iesniegt", 6 | "Scope" : "Darbības joma", 7 | "Authentication and Access Control Settings" : "Autentificēšanās un piekļuves vadības iestatījumi", 8 | "Cancel" : "Atcelt", 9 | "Domain" : "Domain" 10 | },"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" 11 | } -------------------------------------------------------------------------------- /l10n/mk.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Клиент ИД", 5 | "Update" : "Ажурирај", 6 | "Remove" : "Отстрани ", 7 | "Submit" : "Испрати", 8 | "Client secret" : "Тајна на клиент", 9 | "Scope" : "Опсег", 10 | "Cancel" : "Откажи", 11 | "Domain" : "Домен" 12 | }, 13 | "nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;"); 14 | -------------------------------------------------------------------------------- /l10n/mk.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Клиент ИД", 3 | "Update" : "Ажурирај", 4 | "Remove" : "Отстрани ", 5 | "Submit" : "Испрати", 6 | "Client secret" : "Тајна на клиент", 7 | "Scope" : "Опсег", 8 | "Cancel" : "Откажи", 9 | "Domain" : "Домен" 10 | },"pluralForm" :"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;" 11 | } -------------------------------------------------------------------------------- /l10n/nb.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Klient-ID", 5 | "Update" : "Oppdater", 6 | "Remove" : "Fjern", 7 | "Submit" : "Send inn", 8 | "Client secret" : "Klient-hemmelighet", 9 | "Scope" : "Omfang", 10 | "Attribute mapping" : "Attributt-binding", 11 | "Cancel" : "Avbryt", 12 | "Domain" : "Domene" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/nb.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Klient-ID", 3 | "Update" : "Oppdater", 4 | "Remove" : "Fjern", 5 | "Submit" : "Send inn", 6 | "Client secret" : "Klient-hemmelighet", 7 | "Scope" : "Omfang", 8 | "Attribute mapping" : "Attributt-binding", 9 | "Cancel" : "Avbryt", 10 | "Domain" : "Domene" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/nl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Client ID", 5 | "Update" : "Update", 6 | "Remove" : "Verwijderen", 7 | "Submit" : "Verwerken", 8 | "Client secret" : "Client secret", 9 | "Scope" : "Scope", 10 | "Attribute mapping" : "mapping toekennen", 11 | "Cancel" : "Annuleren", 12 | "Domain" : "Domein" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/nl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Client ID", 3 | "Update" : "Update", 4 | "Remove" : "Verwijderen", 5 | "Submit" : "Verwerken", 6 | "Client secret" : "Client secret", 7 | "Scope" : "Scope", 8 | "Attribute mapping" : "mapping toekennen", 9 | "Cancel" : "Annuleren", 10 | "Domain" : "Domein" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/pl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Identyfikator klienta", 5 | "Update" : "Aktualizuj", 6 | "Remove" : "Usuń", 7 | "Submit" : "Wyślij", 8 | "Client secret" : "Tajny klucz klienta", 9 | "Scope" : "Zakres", 10 | "Attribute mapping" : "Atrybut mapowania", 11 | "Cancel" : "Anuluj", 12 | "Domain" : "Domena" 13 | }, 14 | "nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);"); 15 | -------------------------------------------------------------------------------- /l10n/pl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Identyfikator klienta", 3 | "Update" : "Aktualizuj", 4 | "Remove" : "Usuń", 5 | "Submit" : "Wyślij", 6 | "Client secret" : "Tajny klucz klienta", 7 | "Scope" : "Zakres", 8 | "Attribute mapping" : "Atrybut mapowania", 9 | "Cancel" : "Anuluj", 10 | "Domain" : "Domena" 11 | },"pluralForm" :"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);" 12 | } -------------------------------------------------------------------------------- /l10n/pt_PT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Id. de Cliente", 5 | "Update" : "Atualizar", 6 | "Remove" : "Remover", 7 | "Submit" : "Submeter", 8 | "Client secret" : "Segredo de cliente", 9 | "Scope" : "Âmbito", 10 | "Attribute mapping" : "Mapeamento de atributos", 11 | "Cancel" : "Cancelar", 12 | "Domain" : "Domínio" 13 | }, 14 | "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 15 | -------------------------------------------------------------------------------- /l10n/pt_PT.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Id. de Cliente", 3 | "Update" : "Atualizar", 4 | "Remove" : "Remover", 5 | "Submit" : "Submeter", 6 | "Client secret" : "Segredo de cliente", 7 | "Scope" : "Âmbito", 8 | "Attribute mapping" : "Mapeamento de atributos", 9 | "Cancel" : "Cancelar", 10 | "Domain" : "Domínio" 11 | },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 12 | } -------------------------------------------------------------------------------- /l10n/ro.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID client", 5 | "Update" : "Actualizare", 6 | "Remove" : "Elimină", 7 | "Submit" : "Trimite", 8 | "Client secret" : "Secret client", 9 | "Scope" : "Scop", 10 | "Cancel" : "Anulare", 11 | "Domain" : "Domeniu" 12 | }, 13 | "nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));"); 14 | -------------------------------------------------------------------------------- /l10n/ro.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID client", 3 | "Update" : "Actualizare", 4 | "Remove" : "Elimină", 5 | "Submit" : "Trimite", 6 | "Client secret" : "Secret client", 7 | "Scope" : "Scop", 8 | "Cancel" : "Anulare", 9 | "Domain" : "Domeniu" 10 | },"pluralForm" :"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));" 11 | } -------------------------------------------------------------------------------- /l10n/ru.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID клиента", 5 | "Update" : "Обновление", 6 | "Remove" : "Исключить", 7 | "Submit" : "Отправить ответ", 8 | "Client secret" : "Клиентский ключ ", 9 | "Scope" : "Объем", 10 | "Attribute mapping" : "Привязка атрибутов", 11 | "Cancel" : "Отмена", 12 | "Domain" : "Домен или рабочая группа (обязательно)" 13 | }, 14 | "nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"); 15 | -------------------------------------------------------------------------------- /l10n/ru.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID клиента", 3 | "Update" : "Обновление", 4 | "Remove" : "Исключить", 5 | "Submit" : "Отправить ответ", 6 | "Client secret" : "Клиентский ключ ", 7 | "Scope" : "Объем", 8 | "Attribute mapping" : "Привязка атрибутов", 9 | "Cancel" : "Отмена", 10 | "Domain" : "Домен или рабочая группа (обязательно)" 11 | },"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);" 12 | } -------------------------------------------------------------------------------- /l10n/sc.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID cliente", 5 | "Update" : "Agiorna", 6 | "Remove" : "Boga", 7 | "Submit" : "Imbia", 8 | "Client secret" : "Segretu de su cliente", 9 | "Scope" : "Àmbitu", 10 | "Attribute mapping" : "Assòtziu de is atributos", 11 | "Cancel" : "Annulla", 12 | "Domain" : "Domìniu" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/sc.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID cliente", 3 | "Update" : "Agiorna", 4 | "Remove" : "Boga", 5 | "Submit" : "Imbia", 6 | "Client secret" : "Segretu de su cliente", 7 | "Scope" : "Àmbitu", 8 | "Attribute mapping" : "Assòtziu de is atributos", 9 | "Cancel" : "Annulla", 10 | "Domain" : "Domìniu" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/sk.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Login with %1s" : "Prihlásiť sa pomocou %1s", 5 | "ID4Me is disabled" : "ID4Me je zakázané", 6 | "Invalid OpenID domain" : "Neplatná OpenID doména", 7 | "Invalid authority issuer" : "Neplatný vydavateľ autority", 8 | "Multiple authority found" : "Bolo nájdených viacero autorít", 9 | "The received state does not match the expected value." : "Prijatý stav nezodpovedá predpokladanej hodnote.", 10 | "Authority not found" : "Autorita nebola nájdená", 11 | "The received token is expired." : "Prijatý token expiroval.", 12 | "User conflict" : "Konflikt užívateľov", 13 | "OpenID Connect" : "OpenID Connect", 14 | "OpenID Connect user backend" : "Uživateľký backend OpenID Connect", 15 | "Could not remove provider: {msg}" : "Nepodarilo sa odstrániť poskytovateľa: {msg}", 16 | "Could not register provider:" : "Nepodarilo sa zaregistrovať poskytovateľa:", 17 | "Allows users to authenticate via OpenID Connect providers." : "Povoliť užívateľom autentifikáciu cez OpenID Connect poskytovateľov.", 18 | "Enable ID4me" : "Povoliť ID4me", 19 | "Store login tokens" : "Uložiť prihlasovacie tokeny", 20 | "Registered Providers" : "Preferovaní poskytovatelia", 21 | "Register new provider" : "Registrovať nového poskytovateľa", 22 | "Register a new provider" : "Registrovať nového poskytovateľa", 23 | "Configure your provider to redirect back to {url}" : "Nastavte vášho poskytovateľa pre presmerovanie na {url}", 24 | "No providers registered." : "Neboli zaregistrovaný žiadny poskytovatelia.", 25 | "Client ID" : "Client ID", 26 | "Update" : "Aktualizovať", 27 | "Remove" : "Odobrať", 28 | "Update provider" : "Upraviť poskytovateľa", 29 | "Submit" : "Odoslať", 30 | "Client configuration" : "Konfigurácia klienta", 31 | "Identifier (max 128 characters)" : "Identifikátor (max 128 znakov)", 32 | "Display name to identify the provider" : "Zobrazované meno pre identifikáciu poskytovateľa", 33 | "Client secret" : "Heslo klienta", 34 | "Leave empty to keep existing" : "Ponechajte prázdne pre zachovanie existujúceho", 35 | "Scope" : "Rozsah", 36 | "Attribute mapping" : "Mapovanie atribútov", 37 | "Cancel" : "Zrušiť", 38 | "Domain" : "Doména", 39 | "your.domain" : "vasa.domena" 40 | }, 41 | "nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);"); 42 | -------------------------------------------------------------------------------- /l10n/sk.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Login with %1s" : "Prihlásiť sa pomocou %1s", 3 | "ID4Me is disabled" : "ID4Me je zakázané", 4 | "Invalid OpenID domain" : "Neplatná OpenID doména", 5 | "Invalid authority issuer" : "Neplatný vydavateľ autority", 6 | "Multiple authority found" : "Bolo nájdených viacero autorít", 7 | "The received state does not match the expected value." : "Prijatý stav nezodpovedá predpokladanej hodnote.", 8 | "Authority not found" : "Autorita nebola nájdená", 9 | "The received token is expired." : "Prijatý token expiroval.", 10 | "User conflict" : "Konflikt užívateľov", 11 | "OpenID Connect" : "OpenID Connect", 12 | "OpenID Connect user backend" : "Uživateľký backend OpenID Connect", 13 | "Could not remove provider: {msg}" : "Nepodarilo sa odstrániť poskytovateľa: {msg}", 14 | "Could not register provider:" : "Nepodarilo sa zaregistrovať poskytovateľa:", 15 | "Allows users to authenticate via OpenID Connect providers." : "Povoliť užívateľom autentifikáciu cez OpenID Connect poskytovateľov.", 16 | "Enable ID4me" : "Povoliť ID4me", 17 | "Store login tokens" : "Uložiť prihlasovacie tokeny", 18 | "Registered Providers" : "Preferovaní poskytovatelia", 19 | "Register new provider" : "Registrovať nového poskytovateľa", 20 | "Register a new provider" : "Registrovať nového poskytovateľa", 21 | "Configure your provider to redirect back to {url}" : "Nastavte vášho poskytovateľa pre presmerovanie na {url}", 22 | "No providers registered." : "Neboli zaregistrovaný žiadny poskytovatelia.", 23 | "Client ID" : "Client ID", 24 | "Update" : "Aktualizovať", 25 | "Remove" : "Odobrať", 26 | "Update provider" : "Upraviť poskytovateľa", 27 | "Submit" : "Odoslať", 28 | "Client configuration" : "Konfigurácia klienta", 29 | "Identifier (max 128 characters)" : "Identifikátor (max 128 znakov)", 30 | "Display name to identify the provider" : "Zobrazované meno pre identifikáciu poskytovateľa", 31 | "Client secret" : "Heslo klienta", 32 | "Leave empty to keep existing" : "Ponechajte prázdne pre zachovanie existujúceho", 33 | "Scope" : "Rozsah", 34 | "Attribute mapping" : "Mapovanie atribútov", 35 | "Cancel" : "Zrušiť", 36 | "Domain" : "Doména", 37 | "your.domain" : "vasa.domena" 38 | },"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);" 39 | } -------------------------------------------------------------------------------- /l10n/sl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID Odjemalca", 5 | "Update" : "Posodobi", 6 | "Remove" : "Odstrani", 7 | "Submit" : "Pošlji", 8 | "Client secret" : "Skrivni ključ odjemalca", 9 | "Scope" : "Obseg", 10 | "Attribute mapping" : "Preslikave atributov", 11 | "Cancel" : "Prekliči", 12 | "Domain" : "Domena" 13 | }, 14 | "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"); 15 | -------------------------------------------------------------------------------- /l10n/sl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID Odjemalca", 3 | "Update" : "Posodobi", 4 | "Remove" : "Odstrani", 5 | "Submit" : "Pošlji", 6 | "Client secret" : "Skrivni ključ odjemalca", 7 | "Scope" : "Obseg", 8 | "Attribute mapping" : "Preslikave atributov", 9 | "Cancel" : "Prekliči", 10 | "Domain" : "Domena" 11 | },"pluralForm" :"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);" 12 | } -------------------------------------------------------------------------------- /l10n/sq.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID klienti", 5 | "Update" : "Përditëso", 6 | "Remove" : "Hiqe", 7 | "Submit" : "Dërgo", 8 | "Client secret" : "E fshehtë klienti", 9 | "Scope" : "Shtrirje", 10 | "Attribute mapping" : "Përcaktimi i atributeve", 11 | "Cancel" : "Anuloje", 12 | "Domain" : "Përkatësi" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/sq.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID klienti", 3 | "Update" : "Përditëso", 4 | "Remove" : "Hiqe", 5 | "Submit" : "Dërgo", 6 | "Client secret" : "E fshehtë klienti", 7 | "Scope" : "Shtrirje", 8 | "Attribute mapping" : "Përcaktimi i atributeve", 9 | "Cancel" : "Anuloje", 10 | "Domain" : "Përkatësi" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/sv.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Klient-ID", 5 | "Update" : "Uppdatera", 6 | "Remove" : "Ta bort", 7 | "Submit" : "Skicka", 8 | "Client secret" : "Klienthemlighet", 9 | "Scope" : "Sammanhang", 10 | "Attribute mapping" : "Attributmappning", 11 | "Cancel" : "Avbryt", 12 | "Domain" : "Domän" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/sv.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Klient-ID", 3 | "Update" : "Uppdatera", 4 | "Remove" : "Ta bort", 5 | "Submit" : "Skicka", 6 | "Client secret" : "Klienthemlighet", 7 | "Scope" : "Sammanhang", 8 | "Attribute mapping" : "Attributmappning", 9 | "Cancel" : "Avbryt", 10 | "Domain" : "Domän" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/th.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "รหัสไคลเอ็นต์", 5 | "Update" : "อัปเดต", 6 | "Remove" : "ลบออก", 7 | "Submit" : "ส่ง", 8 | "Client secret" : "ข้อมูลลับไคลเอ็นต์", 9 | "Scope" : "ขอบเขต", 10 | "Cancel" : "ยกเลิก", 11 | "Domain" : "โดเมน" 12 | }, 13 | "nplurals=1; plural=0;"); 14 | -------------------------------------------------------------------------------- /l10n/th.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "รหัสไคลเอ็นต์", 3 | "Update" : "อัปเดต", 4 | "Remove" : "ลบออก", 5 | "Submit" : "ส่ง", 6 | "Client secret" : "ข้อมูลลับไคลเอ็นต์", 7 | "Scope" : "ขอบเขต", 8 | "Cancel" : "ยกเลิก", 9 | "Domain" : "โดเมน" 10 | },"pluralForm" :"nplurals=1; plural=0;" 11 | } -------------------------------------------------------------------------------- /l10n/ug.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Client ID", 5 | "Update" : "يېڭىلاش", 6 | "Remove" : "ئۆچۈرۈڭ", 7 | "Submit" : "يوللاڭ", 8 | "Client secret" : "Client secret", 9 | "Scope" : "دائىرىسى", 10 | "Attribute mapping" : "خەرىتە سىزىش", 11 | "Cancel" : "بىكار قىلىش", 12 | "Domain" : "Domain" 13 | }, 14 | "nplurals=2; plural=(n != 1);"); 15 | -------------------------------------------------------------------------------- /l10n/ug.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Client ID", 3 | "Update" : "يېڭىلاش", 4 | "Remove" : "ئۆچۈرۈڭ", 5 | "Submit" : "يوللاڭ", 6 | "Client secret" : "Client secret", 7 | "Scope" : "دائىرىسى", 8 | "Attribute mapping" : "خەرىتە سىزىش", 9 | "Cancel" : "بىكار قىلىش", 10 | "Domain" : "Domain" 11 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 12 | } -------------------------------------------------------------------------------- /l10n/uk.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "Ідентифікатор клієнта", 5 | "Update" : "Оновлювати", 6 | "Remove" : "Вилучити", 7 | "Submit" : "Відправити", 8 | "Client secret" : "Ключ клієнта", 9 | "Scope" : "Область", 10 | "Attribute mapping" : "Мапування атрибутів", 11 | "Cancel" : "Скасувати", 12 | "Domain" : "Домен" 13 | }, 14 | "nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"); 15 | -------------------------------------------------------------------------------- /l10n/uk.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "Ідентифікатор клієнта", 3 | "Update" : "Оновлювати", 4 | "Remove" : "Вилучити", 5 | "Submit" : "Відправити", 6 | "Client secret" : "Ключ клієнта", 7 | "Scope" : "Область", 8 | "Attribute mapping" : "Мапування атрибутів", 9 | "Cancel" : "Скасувати", 10 | "Domain" : "Домен" 11 | },"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);" 12 | } -------------------------------------------------------------------------------- /l10n/vi.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "ID khách hàng", 5 | "Update" : "Cập nhật", 6 | "Remove" : "Xoá", 7 | "Submit" : "Gửi", 8 | "Cancel" : "Hủy bỏ", 9 | "Domain" : "Miền" 10 | }, 11 | "nplurals=1; plural=0;"); 12 | -------------------------------------------------------------------------------- /l10n/vi.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "ID khách hàng", 3 | "Update" : "Cập nhật", 4 | "Remove" : "Xoá", 5 | "Submit" : "Gửi", 6 | "Cancel" : "Hủy bỏ", 7 | "Domain" : "Miền" 8 | },"pluralForm" :"nplurals=1; plural=0;" 9 | } -------------------------------------------------------------------------------- /l10n/zh_CN.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Client ID" : "客户端 ID", 5 | "Update" : "更新", 6 | "Remove" : "移除", 7 | "Submit" : "提交", 8 | "Client secret" : "客户端 secret", 9 | "Scope" : "适用范围", 10 | "Attribute mapping" : "属性映射", 11 | "Cancel" : "取消", 12 | "Domain" : "域名" 13 | }, 14 | "nplurals=1; plural=0;"); 15 | -------------------------------------------------------------------------------- /l10n/zh_CN.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Client ID" : "客户端 ID", 3 | "Update" : "更新", 4 | "Remove" : "移除", 5 | "Submit" : "提交", 6 | "Client secret" : "客户端 secret", 7 | "Scope" : "适用范围", 8 | "Attribute mapping" : "属性映射", 9 | "Cancel" : "取消", 10 | "Domain" : "域名" 11 | },"pluralForm" :"nplurals=1; plural=0;" 12 | } -------------------------------------------------------------------------------- /l10n/zh_TW.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "user_oidc", 3 | { 4 | "Login with %1s" : "以 %1s 登入", 5 | "ID4Me is disabled" : "已停用 ID4Me", 6 | "Invalid OpenID domain" : "無效的 OpenID 網域", 7 | "Invalid authority issuer" : "無效的權威發行者", 8 | "Multiple authority found" : "找到多個權威", 9 | "The received state does not match the expected value." : "接收到的狀態與預期值不符。", 10 | "Authority not found" : "找不到權威", 11 | "Failed to decrypt the ID4ME provider client secret" : "無法解密 ID4ME 提供者的客戶端祕密", 12 | "The received token is expired." : "接收到的權杖已過期。", 13 | "Client ID" : "客戶端 ID", 14 | "Update" : "更新", 15 | "Remove" : "移除", 16 | "Submit" : "提交", 17 | "Client secret" : "客戶端密碼", 18 | "Scope" : "範圍", 19 | "Attribute mapping" : "屬性對映", 20 | "Cancel" : "取消", 21 | "Domain" : "網域" 22 | }, 23 | "nplurals=1; plural=0;"); 24 | -------------------------------------------------------------------------------- /l10n/zh_TW.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Login with %1s" : "以 %1s 登入", 3 | "ID4Me is disabled" : "已停用 ID4Me", 4 | "Invalid OpenID domain" : "無效的 OpenID 網域", 5 | "Invalid authority issuer" : "無效的權威發行者", 6 | "Multiple authority found" : "找到多個權威", 7 | "The received state does not match the expected value." : "接收到的狀態與預期值不符。", 8 | "Authority not found" : "找不到權威", 9 | "Failed to decrypt the ID4ME provider client secret" : "無法解密 ID4ME 提供者的客戶端祕密", 10 | "The received token is expired." : "接收到的權杖已過期。", 11 | "Client ID" : "客戶端 ID", 12 | "Update" : "更新", 13 | "Remove" : "移除", 14 | "Submit" : "提交", 15 | "Client secret" : "客戶端密碼", 16 | "Scope" : "範圍", 17 | "Attribute mapping" : "屬性對映", 18 | "Cancel" : "取消", 19 | "Domain" : "網域" 20 | },"pluralForm" :"nplurals=1; plural=0;" 21 | } -------------------------------------------------------------------------------- /lib/Command/DeleteProvider.php: -------------------------------------------------------------------------------- 1 | setName('user_oidc:provider:delete') 36 | ->setDescription('Delete an OpenId connect provider') 37 | ->addArgument('identifier', InputArgument::REQUIRED, 'Administrative identifier name of the provider to delete') 38 | ->addOption('force', 'f', InputOption::VALUE_NONE, 'Skip confirmation'); 39 | parent::configure(); 40 | } 41 | 42 | protected function execute(InputInterface $input, OutputInterface $output) { 43 | try { 44 | $identifier = $input->getArgument('identifier'); 45 | try { 46 | $provider = $this->providerMapper->findProviderByIdentifier($identifier); 47 | } catch (DoesNotExistException $e) { 48 | $output->writeln('Provider not found.'); 49 | return -1; 50 | } 51 | $helper = $this->getHelper('question'); 52 | $question = new ConfirmationQuestion('Are you sure you want to delete OpenID Provider "' . $provider->getIdentifier() . '"? It may invalidate all associated user accounts [y/N] ', false); 53 | if ($input->getOption('force') || $helper->ask($input, $output, $question)) { 54 | $this->providerMapper->delete($provider); 55 | $this->providerService->deleteSettings($provider->getId()); 56 | $output->writeln('"' . $provider->getIdentifier() . '" has been deleted.'); 57 | } 58 | } catch (Exception $e) { 59 | $output->writeln($e->getMessage()); 60 | return -1; 61 | } 62 | return 0; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/Controller/ApiController.php: -------------------------------------------------------------------------------- 1 | userMapper->getOrCreate($providerId, $userId); 45 | $user = $this->userManager->get($backendUser->getUserId()); 46 | 47 | if ($displayName) { 48 | if ($displayName !== $backendUser->getDisplayName()) { 49 | $backendUser->setDisplayName($displayName); 50 | $this->userMapper->update($backendUser); 51 | } 52 | } 53 | 54 | if ($email) { 55 | $user->setSystemEMailAddress($email); 56 | } 57 | 58 | if ($quota) { 59 | $user->setQuota($quota); 60 | } 61 | 62 | $userFolder = $this->root->getUserFolder($user->getUID()); 63 | try { 64 | // copy skeleton 65 | \OC_Util::copySkeleton($user->getUID(), $userFolder); 66 | } catch (NotPermittedException $ex) { 67 | // read only uses 68 | } 69 | 70 | return new DataResponse(['user_id' => $user->getUID()]); 71 | } 72 | 73 | /** 74 | * @NoCSRFRequired 75 | * 76 | * @param string $userId 77 | * @return DataResponse 78 | */ 79 | public function deleteUser(string $userId): DataResponse { 80 | $user = $this->userManager->get($userId); 81 | if (is_null($user) || $user->getBackendClassName() !== Application::APP_ID) { 82 | return new DataResponse(['message' => 'User not found'], Http::STATUS_NOT_FOUND); 83 | } 84 | 85 | $user->delete(); 86 | return new DataResponse(['user_id' => $userId], Http::STATUS_OK); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/Controller/BaseOidcController.php: -------------------------------------------------------------------------------- 1 | config->getSystemValueBool('debug', false); 32 | } 33 | 34 | /** 35 | * @param string $message 36 | * @param int $statusCode 37 | * @param array $throttleMetadata 38 | * @param bool|null $throttle 39 | * @return TemplateResponse 40 | */ 41 | protected function buildErrorTemplateResponse(string $message, int $statusCode, array $throttleMetadata = [], ?bool $throttle = null): TemplateResponse { 42 | $params = [ 43 | 'errors' => [ 44 | ['error' => $message], 45 | ], 46 | ]; 47 | return $this->buildFailureTemplateResponse('', 'error', $params, $statusCode, $throttleMetadata, $throttle); 48 | } 49 | 50 | /** 51 | * @param string $message 52 | * @param int $statusCode 53 | * @param array $throttleMetadata 54 | * @param bool|null $throttle 55 | * @return TemplateResponse 56 | */ 57 | protected function build403TemplateResponse(string $message, int $statusCode, array $throttleMetadata = [], ?bool $throttle = null): TemplateResponse { 58 | $params = ['message' => $message]; 59 | return $this->buildFailureTemplateResponse('core', '403', $params, $statusCode, $throttleMetadata, $throttle); 60 | } 61 | 62 | /** 63 | * @param string $appName 64 | * @param string $templateName 65 | * @param array $params 66 | * @param int $statusCode 67 | * @param array $throttleMetadata 68 | * @param bool|null $throttle 69 | * @return TemplateResponse 70 | */ 71 | protected function buildFailureTemplateResponse(string $appName, string $templateName, array $params, int $statusCode, 72 | array $throttleMetadata = [], ?bool $throttle = null): TemplateResponse { 73 | $response = new TemplateResponse( 74 | $appName, 75 | $templateName, 76 | $params, 77 | TemplateResponse::RENDER_AS_ERROR 78 | ); 79 | $response->setStatus($statusCode); 80 | // if not specified, throttle if debug mode is off 81 | if (($throttle === null && !$this->isDebugModeEnabled()) || $throttle) { 82 | $response->throttle($throttleMetadata); 83 | } 84 | return $response; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/Controller/OcsApiController.php: -------------------------------------------------------------------------------- 1 | userMapper->getOrCreate($providerId, $userId); 43 | $user = $this->userManager->get($backendUser->getUserId()); 44 | 45 | if ($displayName) { 46 | if ($displayName !== $backendUser->getDisplayName()) { 47 | $backendUser->setDisplayName($displayName); 48 | $this->userMapper->update($backendUser); 49 | } 50 | } 51 | 52 | if ($email) { 53 | $user->setSystemEMailAddress($email); 54 | } 55 | 56 | if ($quota) { 57 | $user->setQuota($quota); 58 | } 59 | 60 | $userFolder = $this->root->getUserFolder($user->getUID()); 61 | try { 62 | // copy skeleton 63 | \OC_Util::copySkeleton($user->getUID(), $userFolder); 64 | } catch (NotPermittedException $ex) { 65 | // read only uses 66 | } 67 | 68 | return new DataResponse(['user_id' => $user->getUID()]); 69 | } 70 | 71 | /** 72 | * @param string $userId 73 | * @return DataResponse 74 | */ 75 | public function deleteUser(string $userId): DataResponse { 76 | $user = $this->userManager->get($userId); 77 | if (is_null($user) || $user->getBackendClassName() !== Application::APP_ID) { 78 | return new DataResponse(['message' => 'User not found'], Http::STATUS_NOT_FOUND); 79 | } 80 | 81 | $user->delete(); 82 | return new DataResponse(['user_id' => $userId], Http::STATUS_OK); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/Controller/TimezoneController.php: -------------------------------------------------------------------------------- 1 | config->setUserValue($this->userId, 'core', 'timezone', $timezone); 40 | $this->session->set('timezone', $timezoneOffset); 41 | 42 | return new JSONResponse(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/Cron/CleanupSessions.php: -------------------------------------------------------------------------------- 1 | setInterval(24 * 60 * 60); 27 | if (method_exists($this, 'setTimeSensitivity')) { 28 | $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); 29 | } 30 | } 31 | 32 | /** 33 | * @param $argument 34 | * @return void 35 | */ 36 | protected function run($argument): void { 37 | $nowTimestamp = (new DateTime())->getTimestamp(); 38 | $configSessionLifetime = $this->config->getSystemValueInt('session_lifetime', 60 * 60 * 24); 39 | $configCookieLifetime = $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); 40 | $since = $nowTimestamp - max($configSessionLifetime, $configCookieLifetime); 41 | $this->sessionMapper->cleanupSessions($since); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/Db/Id4Me.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Id4MeMapper extends QBMapper { 18 | public function __construct(IDBConnection $db) { 19 | parent::__construct($db, 'user_oidc_id4me', Id4Me::class); 20 | } 21 | 22 | /** 23 | * @param string $identifier 24 | * @return Id4Me 25 | * @throws \OCP\AppFramework\Db\DoesNotExistException 26 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException 27 | */ 28 | public function findByIdentifier(string $identifier): Id4Me { 29 | $qb = $this->db->getQueryBuilder(); 30 | 31 | $qb->select('*') 32 | ->from($this->getTableName()) 33 | ->where( 34 | $qb->expr()->eq('identifier', $qb->createNamedParameter($identifier)) 35 | ); 36 | 37 | return $this->findEntity($qb); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/Db/Provider.php: -------------------------------------------------------------------------------- 1 | scope ?: ' '; 51 | } 52 | 53 | #[\ReturnTypeWillChange] 54 | public function jsonSerialize() { 55 | return [ 56 | 'id' => $this->id, 57 | 'identifier' => $this->identifier, 58 | 'clientId' => $this->clientId, 59 | 'discoveryEndpoint' => $this->discoveryEndpoint, 60 | 'endSessionEndpoint' => $this->endSessionEndpoint, 61 | 'scope' => trim($this->getScope()), 62 | ]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/Db/ProviderMapper.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ProviderMapper extends QBMapper { 23 | public function __construct(IDBConnection $db) { 24 | parent::__construct($db, 'user_oidc_providers', Provider::class); 25 | } 26 | 27 | /** 28 | * @param int $id 29 | * @return Provider 30 | * @throws \OCP\AppFramework\Db\DoesNotExistException 31 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException 32 | */ 33 | public function getProvider(int $id): Provider { 34 | $qb = $this->db->getQueryBuilder(); 35 | 36 | $qb->select('*') 37 | ->from($this->getTableName()) 38 | ->where( 39 | $qb->expr()->eq('id', $qb->createNamedParameter($id)) 40 | ); 41 | 42 | return $this->findEntity($qb); 43 | } 44 | 45 | /** 46 | * Find provider by provider identifier, the admin-given name for 47 | * the provider configuration. 48 | * @param string $identifier 49 | * @return Provider 50 | * @throws \OCP\AppFramework\Db\DoesNotExistException 51 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException 52 | */ 53 | public function findProviderByIdentifier(string $identifier): Provider { 54 | $qb = $this->db->getQueryBuilder(); 55 | 56 | $qb->select('*') 57 | ->from($this->getTableName()) 58 | ->where( 59 | $qb->expr()->eq('identifier', $qb->createNamedParameter($identifier)) 60 | ); 61 | 62 | return $this->findEntity($qb); 63 | } 64 | 65 | /** 66 | * @return Provider[] 67 | */ 68 | public function getProviders() { 69 | $qb = $this->db->getQueryBuilder(); 70 | 71 | $qb->select('*') 72 | ->from($this->getTableName()); 73 | 74 | return $this->findEntities($qb); 75 | } 76 | 77 | /** 78 | * Create or update provider settings 79 | * 80 | * @param string $identifier 81 | * @param string|null $clientid 82 | * @param string|null $clientsecret 83 | * @param string|null $discoveryuri 84 | * @param string $scope 85 | * @param string|null $endsessionendpointuri 86 | * @return Provider|Entity 87 | * @throws DoesNotExistException 88 | * @throws MultipleObjectsReturnedException 89 | * @throws Exception 90 | */ 91 | public function createOrUpdateProvider(string $identifier, ?string $clientid = null, 92 | ?string $clientsecret = null, ?string $discoveryuri = null, string $scope = 'openid email profile', 93 | ?string $endsessionendpointuri = null) { 94 | try { 95 | $provider = $this->findProviderByIdentifier($identifier); 96 | } catch (DoesNotExistException $eNotExist) { 97 | $provider = null; 98 | } 99 | 100 | if ($provider === null) { 101 | $provider = new Provider(); 102 | if (($clientid === null) || ($clientsecret === null) || ($discoveryuri === null)) { 103 | throw new DoesNotExistException('Provider must be created. All provider parameters required.'); 104 | } 105 | $provider->setIdentifier($identifier); 106 | $provider->setClientId($clientid); 107 | $provider->setClientSecret($clientsecret); 108 | $provider->setDiscoveryEndpoint($discoveryuri); 109 | $provider->setEndSessionEndpoint($endsessionendpointuri); 110 | $provider->setScope($scope); 111 | return $this->insert($provider); 112 | } else { 113 | if ($clientid !== null) { 114 | $provider->setClientId($clientid); 115 | } 116 | if ($clientsecret !== null) { 117 | $provider->setClientSecret($clientsecret); 118 | } 119 | if ($discoveryuri !== null) { 120 | $provider->setDiscoveryEndpoint($discoveryuri); 121 | } 122 | if ($endsessionendpointuri !== null) { 123 | $provider->setEndSessionEndpoint($endsessionendpointuri ?: null); 124 | } 125 | $provider->setScope($scope); 126 | return $this->update($provider); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/Db/Session.php: -------------------------------------------------------------------------------- 1 | addType('sid', 'string'); 49 | $this->addType('sub', 'string'); 50 | $this->addType('iss', 'string'); 51 | $this->addType('authtoken_id', 'integer'); 52 | $this->addType('nc_session_id', 'string'); 53 | $this->addType('created_at', 'integer'); 54 | } 55 | 56 | #[\ReturnTypeWillChange] 57 | public function jsonSerialize() { 58 | return [ 59 | 'id' => $this->id, 60 | 'sid' => $this->sid, 61 | 'sub' => $this->sub, 62 | 'iss' => $this->iss, 63 | 'authtoken_id' => $this->authtokenId, 64 | 'nc_session_id' => $this->ncSessionId, 65 | 'created_at' => $this->createdAt, 66 | ]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/Db/SessionMapper.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class SessionMapper extends QBMapper { 23 | public function __construct(IDBConnection $db) { 24 | parent::__construct($db, 'user_oidc_sessions', Session::class); 25 | } 26 | 27 | /** 28 | * @param int $id 29 | * @return Session 30 | * @throws \OCP\AppFramework\Db\DoesNotExistException 31 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException 32 | */ 33 | public function getSession(int $id): Session { 34 | $qb = $this->db->getQueryBuilder(); 35 | 36 | $qb->select('*') 37 | ->from($this->getTableName()) 38 | ->where( 39 | $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) 40 | ); 41 | 42 | return $this->findEntity($qb); 43 | } 44 | 45 | /** 46 | * Find session by sid (from the OIDC id token) 47 | * 48 | * @param string $sid 49 | * @return Session 50 | * @throws \OCP\AppFramework\Db\DoesNotExistException 51 | * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException 52 | */ 53 | public function findSessionBySid(string $sid): Session { 54 | $qb = $this->db->getQueryBuilder(); 55 | 56 | $qb->select('*') 57 | ->from($this->getTableName()) 58 | ->where( 59 | $qb->expr()->eq('sid', $qb->createNamedParameter($sid, IQueryBuilder::PARAM_STR)) 60 | ); 61 | 62 | return $this->findEntity($qb); 63 | } 64 | 65 | /** 66 | * @param string $ncSessionId 67 | * @return int 68 | * @throws \OCP\DB\Exception 69 | */ 70 | public function deleteFromNcSessionId(string $ncSessionId): int { 71 | $qb = $this->db->getQueryBuilder(); 72 | 73 | $qb->delete($this->getTableName()) 74 | ->where( 75 | $qb->expr()->eq('nc_session_id', $qb->createNamedParameter($ncSessionId, IQueryBuilder::PARAM_STR)) 76 | ); 77 | return $qb->executeStatement(); 78 | } 79 | 80 | /** 81 | * @param int $minCreationTimestamp 82 | * @throws \OCP\DB\Exception 83 | */ 84 | public function cleanupSessions(int $minCreationTimestamp): void { 85 | $qb = $this->db->getQueryBuilder(); 86 | 87 | $qb->delete($this->getTableName()) 88 | ->where( 89 | $qb->expr()->lt('created_at', $qb->createNamedParameter($minCreationTimestamp, IQueryBuilder::PARAM_INT)) 90 | ); 91 | $qb->executeStatement(); 92 | } 93 | 94 | /** 95 | * Create a session 96 | * 97 | * @param string $sid 98 | * @param string $sub 99 | * @param string $iss 100 | * @param int $authtokenId 101 | * @param string $ncSessionid 102 | * @return mixed|Session|\OCP\AppFramework\Db\Entity 103 | */ 104 | public function createSession(string $sid, string $sub, string $iss, int $authtokenId, string $ncSessionid) { 105 | try { 106 | // do not create if one with same sid already exists (which should not happen) 107 | return $this->findSessionBySid($sid); 108 | } catch (MultipleObjectsReturnedException $e) { 109 | // this can't happen 110 | return null; 111 | } catch (DoesNotExistException $e) { 112 | } 113 | 114 | $createdAt = (new DateTime())->getTimestamp(); 115 | 116 | $session = new Session(); 117 | $session->setSid($sid); 118 | $session->setSub($sub); 119 | $session->setIss($iss); 120 | $session->setAuthtokenId($authtokenId); 121 | $session->setNcSessionId($ncSessionid); 122 | $session->setCreatedAt($createdAt); 123 | return $this->insert($session); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/Db/User.php: -------------------------------------------------------------------------------- 1 | addType('user_id', 'string'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/Event/AttributeMappedEvent.php: -------------------------------------------------------------------------------- 1 | stopPropagation(); 20 | */ 21 | class AttributeMappedEvent extends Event { 22 | 23 | private ?string $value; 24 | 25 | public function __construct( 26 | private string $attribute, 27 | private object $claims, 28 | ?string $default = null, 29 | ) { 30 | parent::__construct(); 31 | $this->value = $default; 32 | } 33 | 34 | /** 35 | * @return string One of the ProviderService::SETTING_MAPPING_* constants for the attribute mapping that is currently processed 36 | */ 37 | public function getAttribute(): string { 38 | return $this->attribute; 39 | } 40 | 41 | /** 42 | * @return object the array of claim values associated with the event 43 | */ 44 | public function getClaims(): object { 45 | return $this->claims; 46 | } 47 | 48 | public function hasValue() : bool { 49 | return ($this->value != null); 50 | } 51 | 52 | /** 53 | * @return string value for the logged in user attribute 54 | */ 55 | public function getValue(): ?string { 56 | return $this->value; 57 | } 58 | 59 | public function setValue(?string $value): void { 60 | $this->value = $value; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/Event/ExchangedTokenRequestedEvent.php: -------------------------------------------------------------------------------- 1 | targetAudience; 30 | } 31 | 32 | public function setTargetAudience(string $targetAudience): void { 33 | $this->targetAudience = $targetAudience; 34 | } 35 | 36 | public function getExtraScopes(): array { 37 | return $this->extraScopes; 38 | } 39 | 40 | public function getToken(): ?Token { 41 | return $this->token; 42 | } 43 | 44 | public function setToken(?Token $token): void { 45 | $this->token = $token; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/Event/ExternalTokenRequestedEvent.php: -------------------------------------------------------------------------------- 1 | token; 27 | } 28 | 29 | public function setToken(?Token $token): void { 30 | $this->token = $token; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/Event/InternalTokenRequestedEvent.php: -------------------------------------------------------------------------------- 1 | targetAudience; 31 | } 32 | 33 | public function setTargetAudience(string $targetAudience): void { 34 | $this->targetAudience = $targetAudience; 35 | } 36 | 37 | public function getExtraScopes(): array { 38 | return $this->extraScopes; 39 | } 40 | 41 | public function getResource(): string { 42 | return $this->resource; 43 | } 44 | 45 | public function getToken(): ?Token { 46 | return $this->token; 47 | } 48 | 49 | public function setToken(?Token $token): void { 50 | $this->token = $token; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/Event/TokenObtainedEvent.php: -------------------------------------------------------------------------------- 1 | token; 32 | } 33 | 34 | public function getProvider(): Provider { 35 | return $this->provider; 36 | } 37 | 38 | public function getDiscovery(): array { 39 | return $this->discovery; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/Event/TokenValidatedEvent.php: -------------------------------------------------------------------------------- 1 | token; 32 | } 33 | 34 | public function getProvider(): Provider { 35 | return $this->provider; 36 | } 37 | 38 | public function getDiscovery(): array { 39 | return $this->discovery; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/Exception/GetExternalTokenFailedException.php: -------------------------------------------------------------------------------- 1 | error; 27 | } 28 | 29 | public function getErrorDescription(): ?string { 30 | return $this->errorDescription; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/Exception/TokenExchangeFailedException.php: -------------------------------------------------------------------------------- 1 | error; 27 | } 28 | 29 | public function getErrorDescription(): ?string { 30 | return $this->errorDescription; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/Helper/HttpClientHelper.php: -------------------------------------------------------------------------------- 1 | clientService->newClient(); 25 | 26 | return $client->get($url, [ 27 | 'headers' => $headers, 28 | ])->getBody(); 29 | } 30 | 31 | public function post($url, $body, array $headers = []) { 32 | $client = $this->clientService->newClient(); 33 | 34 | return $client->post($url, [ 35 | 'headers' => $headers, 36 | 'body' => $body, 37 | ])->getBody(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/Listener/ExchangedTokenRequestedListener.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ExchangedTokenRequestedListener implements IEventListener { 22 | 23 | public function __construct( 24 | private IUserSession $userSession, 25 | private TokenService $tokenService, 26 | private LoggerInterface $logger, 27 | ) { 28 | } 29 | 30 | public function handle(Event $event): void { 31 | if (!$event instanceof ExchangedTokenRequestedEvent) { 32 | return; 33 | } 34 | 35 | if (!$this->userSession->isLoggedIn()) { 36 | return; 37 | } 38 | 39 | $targetAudience = $event->getTargetAudience(); 40 | $extraScopes = $event->getExtraScopes(); 41 | $this->logger->debug('[ExchangedTokenRequestedListener] received request for audience: ' . $targetAudience); 42 | 43 | // classic token exchange with an external provider 44 | $token = $this->tokenService->getExchangedToken($targetAudience, $extraScopes); 45 | $event->setToken($token); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/Listener/ExternalTokenRequestedListener.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class ExternalTokenRequestedListener implements IEventListener { 25 | 26 | public function __construct( 27 | private IUserSession $userSession, 28 | private TokenService $tokenService, 29 | private IConfig $config, 30 | private LoggerInterface $logger, 31 | ) { 32 | } 33 | 34 | public function handle(Event $event): void { 35 | if (!$event instanceof ExternalTokenRequestedEvent) { 36 | return; 37 | } 38 | 39 | if (!$this->userSession->isLoggedIn()) { 40 | return; 41 | } 42 | 43 | $this->logger->debug('[ExternalTokenRequestedListener] received request'); 44 | 45 | $storeLoginTokenEnabled = $this->config->getAppValue(Application::APP_ID, 'store_login_token', '0') === '1'; 46 | if (!$storeLoginTokenEnabled) { 47 | throw new GetExternalTokenFailedException('Failed to get external token, login token is not stored', 0); 48 | } 49 | 50 | $token = $this->tokenService->getToken(); 51 | $event->setToken($token); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/Listener/InternalTokenRequestedListener.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class InternalTokenRequestedListener implements IEventListener { 22 | 23 | public function __construct( 24 | private IUserSession $userSession, 25 | private TokenService $tokenService, 26 | private LoggerInterface $logger, 27 | ) { 28 | } 29 | 30 | public function handle(Event $event): void { 31 | if (!$event instanceof InternalTokenRequestedEvent) { 32 | return; 33 | } 34 | 35 | if (!$this->userSession->isLoggedIn()) { 36 | return; 37 | } 38 | 39 | $targetAudience = $event->getTargetAudience(); 40 | $extraScopes = $event->getExtraScopes(); 41 | $resource = $event->getResource(); 42 | $this->logger->debug('[InternalTokenRequestedListener] received request for audience: ' . $targetAudience); 43 | 44 | // generate a token pair with the Oidc provider app 45 | $userId = $this->userSession->getUser()?->getUID(); 46 | if ($userId !== null) { 47 | $ncProviderToken = $this->tokenService->getTokenFromOidcProviderApp($userId, $targetAudience, $extraScopes, $resource); 48 | if ($ncProviderToken !== null) { 49 | $event->setToken($ncProviderToken); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/Listener/TimezoneHandlingListener.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class TimezoneHandlingListener implements IEventListener { 23 | 24 | public function __construct( 25 | private IUserSession $userSession, 26 | private ISession $session, 27 | private IConfig $config, 28 | ) { 29 | } 30 | 31 | public function handle(Event $event): void { 32 | if (!$event instanceof LoadAdditionalScriptsEvent) { 33 | return; 34 | } 35 | 36 | if (!$this->userSession->isLoggedIn()) { 37 | return; 38 | } 39 | 40 | $user = $this->userSession->getUser(); 41 | $timezoneDB = $this->config->getUserValue($user->getUID(), 'core', 'timezone'); 42 | 43 | if ($timezoneDB === '' || !$this->session->exists('timezone')) { 44 | Util::addScript(Application::APP_ID, Application::APP_ID . '-timezone'); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/Migration/Version00001Date20200322114947.php: -------------------------------------------------------------------------------- 1 | createTable('user_oidc'); 34 | $table->addColumn('id', 'integer', [ 35 | 'autoincrement' => true, 36 | 'notnull' => true, 37 | 'length' => 4, 38 | ]); 39 | $table->addColumn('user_id', 'string', [ 40 | 'notnull' => true, 41 | 'length' => 64, 42 | ]); 43 | $table->setPrimaryKey(['id']); 44 | $table->addUniqueIndex(['user_id'], 'user_oidc_uid'); 45 | 46 | return $schema; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/Migration/Version00003Date20200420120107.php: -------------------------------------------------------------------------------- 1 | createTable('user_oidc_id4me'); 31 | $table->addColumn('id', 'integer', [ 32 | 'autoincrement' => true, 33 | 'notnull' => true, 34 | 'length' => 4, 35 | ]); 36 | $table->addColumn('identifier', 'string', [ 37 | 'notnull' => true, 38 | 'length' => 128, 39 | ]); 40 | $table->addColumn('client_id', 'string', [ 41 | 'notnull' => true, 42 | 'length' => 64, 43 | ]); 44 | $table->addColumn('client_secret', 'string', [ 45 | 'notnull' => true, 46 | 'length' => 64, 47 | ]); 48 | $table->setPrimaryKey(['id']); 49 | 50 | return $schema; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/Migration/Version00004Date20200428102743.php: -------------------------------------------------------------------------------- 1 | createTable('user_oidc_providers'); 34 | $table->addColumn('id', 'integer', [ 35 | 'autoincrement' => true, 36 | 'notnull' => true, 37 | 'length' => 4, 38 | ]); 39 | $table->addColumn('identifier', 'string', [ 40 | 'notnull' => true, 41 | 'length' => 128, 42 | ]); 43 | $table->addColumn('client_id', 'string', [ 44 | 'notnull' => true, 45 | 'length' => 64, 46 | ]); 47 | $table->addColumn('client_secret', 'string', [ 48 | 'notnull' => true, 49 | 'length' => 64, 50 | ]); 51 | $table->addColumn('discovery_endpoint', 'string', [ 52 | 'notnull' => false, 53 | 'length' => 255, 54 | ]); 55 | $table->setPrimaryKey(['id']); 56 | 57 | return $schema; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/Migration/Version00005Date20200428123958.php: -------------------------------------------------------------------------------- 1 | getTable('user_oidc'); 33 | $table->addColumn('display_name', 'string', [ 34 | 'length' => 255, 35 | 'default' => '', 36 | 'notnull' => false, 37 | ]); 38 | 39 | return $schema; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/Migration/Version00006Date20210630120251.php: -------------------------------------------------------------------------------- 1 | getTable('user_oidc_providers'); 23 | $table->addUniqueIndex(['identifier'], 'user_oidc_prov_idtf'); 24 | 25 | return $schema; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/Migration/Version00007Date20210730170713.php: -------------------------------------------------------------------------------- 1 | getTable('user_oidc_providers'); 23 | $table->addColumn('scope', 'string', [ 24 | 'length' => 128, 25 | 'default' => 'openid email profile', 26 | 'notnull' => true, 27 | ]); 28 | 29 | return $schema; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/Migration/Version01021Date20220719114355.php: -------------------------------------------------------------------------------- 1 | createTable('user_oidc_sessions'); 35 | $table->addColumn('id', Types::INTEGER, [ 36 | 'autoincrement' => true, 37 | 'notnull' => true, 38 | 'length' => 4, 39 | ]); 40 | // https://openid.net/specs/openid-connect-core-1_0.html#IDToken 41 | $table->addColumn('sid', Types::STRING, [ 42 | 'notnull' => true, 43 | 'length' => 256, 44 | ]); 45 | $table->addColumn('sub', Types::STRING, [ 46 | 'notnull' => true, 47 | 'length' => 256, 48 | ]); 49 | $table->addColumn('iss', Types::STRING, [ 50 | 'notnull' => true, 51 | 'length' => 512, 52 | ]); 53 | $table->addColumn('authtoken_id', Types::INTEGER, [ 54 | 'notnull' => true, 55 | 'length' => 4, 56 | ]); 57 | $table->addColumn('nc_session_id', Types::STRING, [ 58 | 'notnull' => true, 59 | 'length' => 200, 60 | ]); 61 | $table->addColumn('created_at', Types::BIGINT, [ 62 | 'notnull' => true, 63 | 'length' => 20, 64 | 'unsigned' => true, 65 | ]); 66 | $table->setPrimaryKey(['id']); 67 | $table->addIndex(['created_at'], 'user_oidc_sess_crat'); 68 | $table->addUniqueIndex(['sid'], 'user_oidc_sess_sid'); 69 | $table->addUniqueIndex(['nc_session_id'], 'user_oidc_sess_sess_id'); 70 | 71 | return $schema; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/Migration/Version01022Date20221202161257.php: -------------------------------------------------------------------------------- 1 | hasTable('user_oidc_sessions')) { 37 | $table = $schema->getTable('user_oidc_sessions'); 38 | $indexes = $table->getIndexes(); 39 | foreach ($indexes as $index) { 40 | // fix created_at index which is not unique 41 | if ($index->isUnique() && $index->hasColumnAtPosition('created_at')) { 42 | $table->dropIndex($index->getName()); 43 | $table->addIndex(['created_at'], 'user_oidc_sess_crat'); 44 | $somethingChanged = true; 45 | } 46 | // rename indexes on sid and nc_session_id if needed 47 | if ($index->isUnique() && $index->hasColumnAtPosition('sid') && $index->getName() !== 'user_oidc_sess_sid') { 48 | $table->renameIndex($index->getName(), 'user_oidc_sess_sid'); 49 | $somethingChanged = true; 50 | } 51 | if ($index->isUnique() && $index->hasColumnAtPosition('nc_session_id') && $index->getName() !== 'user_oidc_sess_sess_id') { 52 | $table->renameIndex($index->getName(), 'user_oidc_sess_sess_id'); 53 | $somethingChanged = true; 54 | } 55 | } 56 | } 57 | 58 | if ($schema->hasTable('user_oidc_providers')) { 59 | $table = $schema->getTable('user_oidc_providers'); 60 | $indexes = $table->getIndexes(); 61 | foreach ($indexes as $index) { 62 | // rename index on identifier if needed 63 | if ($index->isUnique() && $index->hasColumnAtPosition('identifier') && $index->getName() !== 'user_oidc_prov_idtf') { 64 | $table->renameIndex($index->getName(), 'user_oidc_prov_idtf'); 65 | $somethingChanged = true; 66 | } 67 | } 68 | } 69 | 70 | return $somethingChanged ? $schema : null; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/Migration/Version010303Date20230602125945.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 36 | $this->crypto = $crypto; 37 | } 38 | 39 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { 40 | /** @var ISchemaWrapper $schema */ 41 | $schema = $schemaClosure(); 42 | 43 | foreach (['user_oidc_providers', 'user_oidc_id4me'] as $tableName) { 44 | if ($schema->hasTable($tableName)) { 45 | $table = $schema->getTable($tableName); 46 | if ($table->hasColumn('client_secret')) { 47 | $column = $table->getColumn('client_secret'); 48 | $column->setLength(512); 49 | return $schema; 50 | } 51 | } 52 | } 53 | 54 | return null; 55 | } 56 | 57 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) { 58 | // update secrets in user_oidc_providers and user_oidc_id4me 59 | foreach (['user_oidc_providers', 'user_oidc_id4me'] as $tableName) { 60 | $qbUpdate = $this->connection->getQueryBuilder(); 61 | $qbUpdate->update($tableName) 62 | ->set('client_secret', $qbUpdate->createParameter('updateSecret')) 63 | ->where( 64 | $qbUpdate->expr()->eq('id', $qbUpdate->createParameter('updateId')) 65 | ); 66 | 67 | $qbSelect = $this->connection->getQueryBuilder(); 68 | $qbSelect->select('id', 'client_secret') 69 | ->from($tableName); 70 | $req = $qbSelect->executeQuery(); 71 | while ($row = $req->fetch()) { 72 | $id = $row['id']; 73 | $secret = $row['client_secret']; 74 | $encryptedSecret = $this->crypto->encrypt($secret); 75 | $qbUpdate->setParameter('updateSecret', $encryptedSecret, IQueryBuilder::PARAM_STR); 76 | $qbUpdate->setParameter('updateId', $id, IQueryBuilder::PARAM_INT); 77 | $qbUpdate->executeStatement(); 78 | } 79 | $req->closeCursor(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/Migration/Version010304Date20231121102449.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 35 | $this->crypto = $crypto; 36 | } 37 | 38 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { 39 | /** @var ISchemaWrapper $schema */ 40 | $schema = $schemaClosure(); 41 | 42 | $schemaChanged = false; 43 | foreach (['user_oidc_providers', 'user_oidc_id4me'] as $tableName) { 44 | if ($schema->hasTable($tableName)) { 45 | $table = $schema->getTable($tableName); 46 | if ($table->hasColumn('client_secret')) { 47 | $column = $table->getColumn('client_secret'); 48 | $column->setLength(2048); 49 | $schemaChanged = true; 50 | } 51 | if ($table->hasColumn('client_id')) { 52 | $column = $table->getColumn('client_id'); 53 | $column->setLength(2048); 54 | $schemaChanged = true; 55 | } 56 | } 57 | } 58 | if ($schemaChanged) { 59 | return $schema; 60 | } 61 | return null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/Migration/Version010304Date20231130104459.php: -------------------------------------------------------------------------------- 1 | hasTable('user_oidc_providers')) { 33 | $table = $schema->getTable('user_oidc_providers'); 34 | if (!$table->hasColumn('end_session_endpoint')) { 35 | $table->addColumn('end_session_endpoint', Types::STRING, [ 36 | 'notnull' => false, 37 | 'length' => 255, 38 | ]); 39 | return $schema; 40 | } 41 | } 42 | 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/Model/Token.php: -------------------------------------------------------------------------------- 1 | idToken = $tokenData['id_token'] ?? null; 25 | $this->accessToken = $tokenData['access_token']; 26 | $this->expiresIn = $tokenData['expires_in']; 27 | $this->refreshExpiresIn = $tokenData['refresh_expires_in'] ?? null; 28 | $this->refreshToken = $tokenData['refresh_token'] ?? null; 29 | $this->createdAt = $tokenData['created_at'] ?? time(); 30 | $this->providerId = $tokenData['provider_id'] ?? null; 31 | } 32 | 33 | public function getAccessToken(): string { 34 | return $this->accessToken; 35 | } 36 | 37 | public function getIdToken(): ?string { 38 | return $this->idToken; 39 | } 40 | 41 | public function getExpiresIn(): int { 42 | return $this->expiresIn; 43 | } 44 | 45 | public function getExpiresInFromNow(): int { 46 | $expiresAt = $this->createdAt + $this->expiresIn; 47 | return $expiresAt - time(); 48 | } 49 | 50 | public function getRefreshExpiresIn(): ?int { 51 | return $this->refreshExpiresIn; 52 | } 53 | 54 | public function getRefreshExpiresInFromNow(): int { 55 | // if there is no refresh_expires_in, we assume the refresh token never expires 56 | // so we don't need getRefreshExpiresInFromNow 57 | if ($this->refreshExpiresIn === null) { 58 | return 0; 59 | } 60 | $refreshExpiresAt = $this->createdAt + $this->refreshExpiresIn; 61 | return $refreshExpiresAt - time(); 62 | } 63 | 64 | public function getRefreshToken(): ?string { 65 | return $this->refreshToken; 66 | } 67 | 68 | public function getProviderId(): ?int { 69 | return $this->providerId; 70 | } 71 | 72 | public function isExpired(): bool { 73 | return time() > ($this->createdAt + $this->expiresIn); 74 | } 75 | 76 | public function isExpiring(): bool { 77 | return time() > ($this->createdAt + (int)($this->expiresIn / 2)); 78 | } 79 | 80 | public function refreshIsExpired(): bool { 81 | // if there is no refresh_expires_in, we assume the refresh token never expires 82 | if ($this->refreshExpiresIn === null) { 83 | return false; 84 | } 85 | return time() > ($this->createdAt + $this->refreshExpiresIn); 86 | } 87 | 88 | public function refreshIsExpiring(): bool { 89 | // if there is no refresh_expires_in, we assume the refresh token never expires 90 | if ($this->refreshExpiresIn === null) { 91 | return false; 92 | } 93 | return time() > ($this->createdAt + (int)($this->refreshExpiresIn / 2)); 94 | } 95 | 96 | public function getCreatedAt() { 97 | return $this->createdAt; 98 | } 99 | 100 | public function jsonSerialize(): array { 101 | return [ 102 | 'id_token' => $this->idToken, 103 | 'access_token' => $this->accessToken, 104 | 'expires_in' => $this->expiresIn, 105 | 'refresh_expires_in' => $this->refreshExpiresIn, 106 | 'refresh_token' => $this->refreshToken, 107 | 'created_at' => $this->createdAt, 108 | 'provider_id' => $this->providerId, 109 | ]; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/Service/ID4MeService.php: -------------------------------------------------------------------------------- 1 | config->setAppValue(Application::APP_ID, 'id4me_enabled', $enabled ? '1' : '0'); 23 | } 24 | 25 | public function getID4ME(): bool { 26 | return $this->config->getAppValue(Application::APP_ID, 'id4me_enabled', '0') === '1'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/Service/LdapService.php: -------------------------------------------------------------------------------- 1 | appManager->isEnabledForUser('user_ldap'); 27 | } 28 | 29 | /** 30 | * @param IUser $user 31 | * @return bool 32 | * @throws \Psr\Container\ContainerExceptionInterface 33 | * @throws \Psr\Container\NotFoundExceptionInterface 34 | */ 35 | public function isLdapDeletedUser(IUser $user): bool { 36 | if ($this->isLDAPEnabled()) { 37 | return false; 38 | } 39 | 40 | $className = $user->getBackendClassName(); 41 | if ($className !== 'LDAP') { 42 | return false; 43 | } 44 | 45 | try { 46 | /** @var \OCA\User_LDAP\User\DeletedUsersIndex */ 47 | $dui = \OC::$server->get(\OCA\User_LDAP\User\DeletedUsersIndex::class); 48 | } catch (QueryException $e) { 49 | $this->logger->debug('\OCA\User_LDAP\User\DeletedUsersIndex class not found'); 50 | return false; 51 | } 52 | 53 | if (!$dui->hasUsers()) { 54 | return false; 55 | } 56 | $disabledUsers = $dui->getUsers(); 57 | $searchDisabledUser = current( 58 | array_filter($disabledUsers, function ($disabledUser) use ($user) { 59 | return $disabledUser->getUID() === $user->getUID(); 60 | }) 61 | ); 62 | // did we find the user in the LDAP deleted user list? 63 | return $searchDisabledUser !== false; 64 | } 65 | 66 | /** 67 | * This triggers User_LDAP::getLDAPUserByLoginName which does a LDAP query with the login filter 68 | * so the user ID we got from the OIDC IdP should work as a login in LDAP (the login filter should use a matching attribute) 69 | * @param string $userId 70 | * @return void 71 | */ 72 | public function syncUser(string $userId): void { 73 | try { 74 | /** @var \OCA\User_LDAP\User_Proxy */ 75 | $ldapUserProxy = \OC::$server->get(\OCA\User_LDAP\User_Proxy::class); 76 | $ldapUserProxy->loginName2UserName($userId); 77 | } catch (QueryException $e) { 78 | $this->logger->debug('\OCA\User_LDAP\User_Proxy class not found'); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/Service/LocalIdService.php: -------------------------------------------------------------------------------- 1 | providerService->getSetting($providerId, ProviderService::SETTING_UNIQUE_UID, '1') === '1' || $id4me) { 22 | $newId = $providerId . '_'; 23 | 24 | if ($id4me) { 25 | $newId .= '1_'; 26 | } else { 27 | $newId .= '0_'; 28 | } 29 | 30 | $newId .= $id; 31 | $newId = hash('sha256', $newId); 32 | } elseif ($this->providerService->getSetting($providerId, ProviderService::SETTING_PROVIDER_BASED_ID, '0') === '1') { 33 | $providerName = $this->providerMapper->getProvider($providerId)->getIdentifier(); 34 | $newId = $providerName . '-' . $id; 35 | } else { 36 | $newId = $id; 37 | } 38 | 39 | return $newId; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/Service/OIDCService.php: -------------------------------------------------------------------------------- 1 | discoveryService->obtainDiscovery($provider)['userinfo_endpoint'] ?? null; 30 | if ($url === null) { 31 | return []; 32 | } 33 | 34 | $client = $this->clientService->newClient(); 35 | $this->logger->debug('Fetching user info endpoint'); 36 | $options = [ 37 | 'headers' => [ 38 | 'Authorization' => 'Bearer ' . $accessToken, 39 | ], 40 | ]; 41 | try { 42 | return json_decode($client->get($url, $options)->getBody(), true); 43 | } catch (Throwable $e) { 44 | return []; 45 | } 46 | } 47 | 48 | public function introspection(Provider $provider, string $accessToken): array { 49 | try { 50 | $providerClientSecret = $this->crypto->decrypt($provider->getClientSecret()); 51 | } catch (\Exception $e) { 52 | $this->logger->error('Failed to decrypt the client secret', ['exception' => $e]); 53 | return []; 54 | } 55 | $url = $this->discoveryService->obtainDiscovery($provider)['introspection_endpoint'] ?? null; 56 | if ($url === null) { 57 | return []; 58 | } 59 | 60 | $client = $this->clientService->newClient(); 61 | $this->logger->debug('Fetching user info endpoint'); 62 | $options = [ 63 | 'headers' => [ 64 | 'Authorization' => base64_encode($provider->getClientId() . ':' . $providerClientSecret), 65 | ], 66 | 'body' => [ 67 | 'token' => $accessToken, 68 | ], 69 | ]; 70 | try { 71 | return json_decode($client->post($url, $options)->getBody(), true); 72 | } catch (Throwable $e) { 73 | return []; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/Service/SettingsService.php: -------------------------------------------------------------------------------- 1 | config->getAppValue(Application::APP_ID, 'allow_multiple_user_backends', '1') === '1'; 25 | } 26 | 27 | public function setAllowMultipleUserBackEnds(bool $value): void { 28 | $this->config->setAppValue(Application::APP_ID, 'allow_multiple_user_backends', $value ? '1' : '0'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/Settings/AdminSettings.php: -------------------------------------------------------------------------------- 1 | initialStateService->provideInitialState( 35 | 'id4meState', 36 | $this->Id4MeService->getID4ME() 37 | ); 38 | $this->initialStateService->provideInitialState( 39 | 'storeLoginTokenState', 40 | $this->config->getAppValue(Application::APP_ID, 'store_login_token', '0') === '1' 41 | ); 42 | $this->initialStateService->provideInitialState( 43 | 'providers', 44 | $this->providerService->getProvidersWithSettings() 45 | ); 46 | $this->initialStateService->provideInitialState( 47 | 'redirectUrl', 48 | $this->urlGenerator->linkToRouteAbsolute('user_oidc.login.code') 49 | ); 50 | 51 | Util::addScript(Application::APP_ID, Application::APP_ID . '-admin-settings'); 52 | 53 | return new TemplateResponse(Application::APP_ID, 'admin-settings'); 54 | } 55 | 56 | public function getSection() { 57 | return Application::APP_ID; 58 | } 59 | 60 | public function getPriority() { 61 | return 90; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/Settings/Section.php: -------------------------------------------------------------------------------- 1 | l->t('OpenID Connect'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function getPriority() { 43 | return 75; 44 | } 45 | 46 | public function getIcon() { 47 | return $this->urlGenerator->imagePath(Application::APP_ID, 'app-dark.svg'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/User/Provisioning/IProvisioningStrategy.php: -------------------------------------------------------------------------------- 1 | discoveryService->obtainJWK($provider, $bearerToken); 31 | $payload = JWT::decode($bearerToken, $jwks); 32 | } catch (Throwable $e) { 33 | $this->logger->error('Impossible to decode OIDC token:' . $e->getMessage()); 34 | return null; 35 | } 36 | 37 | return $this->provisioningService->provisionUser($tokenUserId, $provider->getId(), $payload, $userFromOtherBackend); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/User/Validator/IBearerTokenValidator.php: -------------------------------------------------------------------------------- 1 | get(ProviderService::class); 37 | $providerId = $provider->getId(); 38 | $uidAttribute = $providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_UID, ProviderService::SETTING_MAPPING_UID_DEFAULT); 39 | 40 | // try to decode the bearer token 41 | JWT::$leeway = 60; 42 | try { 43 | $jwks = $this->discoveryService->obtainJWK($provider, $bearerToken); 44 | $payload = JWT::decode($bearerToken, $jwks); 45 | } catch (Throwable $e) { 46 | $this->logger->debug('Impossible to decode OIDC token:' . $e->getMessage()); 47 | return null; 48 | } 49 | 50 | // check if the token has expired 51 | if ($payload->exp < $this->timeFactory->getTime()) { 52 | $this->logger->debug('OIDC token has expired'); 53 | return null; 54 | } 55 | 56 | $discovery = $this->discoveryService->obtainDiscovery($provider); 57 | if ($payload->iss !== $discovery['issuer']) { 58 | $this->logger->debug('This token is issued by the wrong issuer, it does not match the one from the discovery endpoint'); 59 | return null; 60 | } 61 | 62 | $oidcSystemConfig = $this->config->getSystemValue('user_oidc', []); 63 | // ref https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation 64 | $checkAudience = !isset($oidcSystemConfig['selfencoded_bearer_validation_audience_check']) 65 | || !in_array($oidcSystemConfig['selfencoded_bearer_validation_audience_check'], [false, 'false', 0, '0'], true); 66 | $providerClientId = $provider->getClientId(); 67 | if ($checkAudience) { 68 | $tokenAudience = $payload->aud; 69 | if ( 70 | (is_string($tokenAudience) && $tokenAudience !== $providerClientId) 71 | || (is_array($tokenAudience) && !in_array($providerClientId, $tokenAudience, true)) 72 | ) { 73 | $this->logger->debug('This token is not for us, the audience does not match the client ID'); 74 | return null; 75 | } 76 | } 77 | 78 | // find the user ID 79 | $uid = $this->provisioningService->getClaimValue($payload, $uidAttribute, $providerId); 80 | return $uid ?: null; 81 | } 82 | 83 | public function getProvisioningStrategy(): string { 84 | return SelfEncodedTokenProvisioning::class; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/User/Validator/UserInfoValidator.php: -------------------------------------------------------------------------------- 1 | userInfoService->userinfo($provider, $bearerToken); 28 | $providerId = $provider->getId(); 29 | $uidAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_UID, ProviderService::SETTING_MAPPING_UID_DEFAULT); 30 | // find the user ID 31 | $uid = $this->provisioningService->getClaimValue($userInfo, $uidAttribute, $providerId); 32 | return $uid ?: null; 33 | } 34 | 35 | public function getProvisioningStrategy(): string { 36 | // TODO implement provisioning over user info endpoint 37 | return ''; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user_oidc", 3 | "description": "OIDC connect user backend for Nextcloud", 4 | "version": "6.3.0-dev.0", 5 | "author": "Roeland Jago Douma ", 6 | "repository": { 7 | "url": "https://github.com/nextcloud/user_oidc", 8 | "type": "git" 9 | }, 10 | "license": "AGPL-3.0-or-later", 11 | "private": true, 12 | "scripts": { 13 | "build": "NODE_ENV=production webpack --progress --config webpack.js", 14 | "dev": "NODE_ENV=development webpack --progress --config webpack.js", 15 | "watch": "NODE_ENV=development webpack --progress --watch --config webpack.js", 16 | "cli:build": "webpack-cli --mode=production --progress --config webpack.js", 17 | "cli:dev": "webpack-cli --mode=development --progress --config webpack.js", 18 | "cli:watch": "webpack-cli --mode=development --progress --watch --config webpack.js", 19 | "lint": "eslint --ext .js,.vue src", 20 | "lint:fix": "eslint --ext .js,.vue src --fix", 21 | "stylelint": "stylelint **/*.css **/*.scss **/*.vue", 22 | "stylelint:fix": "stylelint **/*.css **/*.scss **/*.vue --fix" 23 | }, 24 | "dependencies": { 25 | "@nextcloud/axios": "^2.5.1", 26 | "@nextcloud/dialogs": "^6.3.0", 27 | "@nextcloud/initial-state": "^2.2.0", 28 | "@nextcloud/logger": "^3.0.2", 29 | "@nextcloud/password-confirmation": "^5.3.1", 30 | "@nextcloud/router": "^3.0.1", 31 | "@nextcloud/vue": "^8.27.0", 32 | "jstz": "^2.1.1", 33 | "vue": "^2.7.14", 34 | "vue-material-design-icons": "^5.3.1" 35 | }, 36 | "browserslist": [ 37 | "extends @nextcloud/browserslist-config" 38 | ], 39 | "engines": { 40 | "node": "^20.0.0", 41 | "npm": "^8.0.0 || ^9.0.0" 42 | }, 43 | "devDependencies": { 44 | "@nextcloud/babel-config": "^1.2.0", 45 | "@nextcloud/browserslist-config": "^3.0.1", 46 | "@nextcloud/eslint-config": "^8.4.2", 47 | "@nextcloud/stylelint-config": "^3.1.0", 48 | "@nextcloud/webpack-vue-config": "^6.3.0", 49 | "eslint-webpack-plugin": "^4.2.0", 50 | "stylelint-webpack-plugin": "^5.0.1", 51 | "vue-template-compiler": "^2.7.16" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { getLoggerBuilder } from '@nextcloud/logger' 7 | 8 | export default getLoggerBuilder().setApp('user_oidc').detectUser().build() 9 | -------------------------------------------------------------------------------- /src/main-settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { loadState } from '@nextcloud/initial-state' 7 | import '@nextcloud/dialogs/style.css' 8 | import Vue from 'vue' 9 | 10 | import App from './components/AdminSettings.vue' 11 | 12 | Vue.prototype.t = t 13 | Vue.prototype.n = n 14 | Vue.prototype.OC = OC 15 | Vue.prototype.OCA = OCA 16 | 17 | const View = Vue.extend(App) 18 | new View({ 19 | propsData: { 20 | initialId4MeState: loadState('user_oidc', 'id4meState'), 21 | initialStoreLoginTokenState: loadState('user_oidc', 'storeLoginTokenState'), 22 | initialProviders: loadState('user_oidc', 'providers'), 23 | redirectUrl: loadState('user_oidc', 'redirectUrl'), 24 | }, 25 | }).$mount('#user-oidc-settings') 26 | -------------------------------------------------------------------------------- /src/timezone.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { generateUrl } from '@nextcloud/router' 7 | import axios from '@nextcloud/axios' 8 | 9 | import jstz from 'jstz' 10 | 11 | console.debug('updating timezone and offset for OIDC user') 12 | 13 | const url = generateUrl('/apps/user_oidc/config/timezone') 14 | const params = { 15 | timezone: jstz.determine().name(), 16 | timezoneOffset: (-new Date().getTimezoneOffset() / 60), 17 | } 18 | axios.post(url, params).then(response => { 19 | console.debug('Successfully set OIDC user\'s timezone') 20 | }).catch((error) => { 21 | console.error('Error while setting the OIDC user\'s timezone', error) 22 | }) 23 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | const stylelintConfig = require('@nextcloud/stylelint-config') 7 | 8 | module.exports = stylelintConfig 9 | -------------------------------------------------------------------------------- /templates/admin-settings.php: -------------------------------------------------------------------------------- 1 | 7 |
8 | -------------------------------------------------------------------------------- /templates/id4me/login.php: -------------------------------------------------------------------------------- 1 | 9 |
10 |
11 |

12 | 13 | 14 | 17 | 19 |

20 |
21 |
22 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | loadApp('user_oidc'); 22 | -------------------------------------------------------------------------------- /tests/docker-compose.integration.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | version: '3' 4 | services: 5 | keycloak: 6 | image: quay.io/keycloak/keycloak:13.0.1 7 | ports: 8 | - 8999:8080 9 | volumes: 10 | - ./integration/config:/tmp/keycloak 11 | environment: 12 | KEYCLOAK_USER: admin 13 | KEYCLOAK_PASSWORD: admin 14 | PROXY_ADDRESS_FORWARDING: "true" 15 | KEYCLOAK_IMPORT: /tmp/keycloak/realm-export.json 16 | -------------------------------------------------------------------------------- /tests/phpunit.integration.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | integration 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | unit 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/stubs/oc_app.php: -------------------------------------------------------------------------------- 1 | config = $this->createMock(IConfig::class); 38 | $this->idService = $this->createMock(LocalIdService::class); 39 | $this->db = $this->createMock(IDBConnection::class); 40 | $this->userMapper = $this->getMockBuilder(UserMapper::class) 41 | ->setConstructorArgs([$this->db, $this->idService, $this->config]) 42 | ->setMethods(['getUser', 'insert']) 43 | ->getMock(); 44 | } 45 | 46 | 47 | public function dataCreate(): array { 48 | return [ 49 | // unique uid 50 | [1, 'user@example.com', '2f891889123bd20b298fced19fc270faf0013523c5949ac629fbb8a0ac7d5d29', false, '2f891889123bd20b298fced19fc270faf0013523c5949ac629fbb8a0ac7d5d29'], 51 | [2, 'user@example.com', '4486f1cf00ba2d6d6c7c668a31b238c2b140d469f3bd86cc1a671e8136bac2c0', false, '4486f1cf00ba2d6d6c7c668a31b238c2b140d469f3bd86cc1a671e8136bac2c0'], 52 | [1, 'user1@example.com', 'f322b84fc7b972957d0f3cadb70d5528a77fc2b076ab4d7de4e5a41c44f729b9', false, 'f322b84fc7b972957d0f3cadb70d5528a77fc2b076ab4d7de4e5a41c44f729b9'], 53 | 54 | // no unique uid 55 | [1, 'user1@example.com', 'user1@example.com', false, 'user1@example.com'], 56 | [2, 'user1@example.com', 'user1@example.com', false, 'user1@example.com'], 57 | [2, 'very-long-user-email-adress-with-over-64-characters!!@example.com', 'very-long-user-email-adress-with-over-64-characters!!@example.com', false, 'd58d7aafa7642529dfa27dcf89f8d70dfdce97fbc8bcd80ef75f0bdb1b8fd527'], 58 | 59 | // id4me always uses unique uid 60 | [1, 'user1@example.com', '29d8436003cedbf722538e94a9f72e7412471403bbbc1799029424661317a571', true, '29d8436003cedbf722538e94a9f72e7412471403bbbc1799029424661317a571'], 61 | [1, 'user1@example.com', '29d8436003cedbf722538e94a9f72e7412471403bbbc1799029424661317a571', true, '29d8436003cedbf722538e94a9f72e7412471403bbbc1799029424661317a571'], 62 | 63 | // unique uid with provider prefix 64 | [1, 'user1@example.com', 'provider-user1@example.com', false, 'provider-user1@example.com'], 65 | [2, 'user1@example.com', 'provider-user1@example.com', false, 'provider-user1@example.com'], 66 | ]; 67 | } 68 | 69 | /** @dataProvider dataCreate */ 70 | public function testCreate(int $providerId, string $sub, string $generatedId, bool $id4me, string $expected): void { 71 | $this->idService->expects(self::once())->method('getId')->with($providerId, $sub, $id4me)->willReturn($generatedId); 72 | 73 | $this->userMapper->expects(self::once()) 74 | ->method('getUser') 75 | ->willThrowException(new DoesNotExistException('No user')); 76 | 77 | $this->userMapper->expects(self::once()) 78 | ->method('insert') 79 | ->willReturnCallback(function ($arg) { 80 | return $arg; 81 | }); 82 | 83 | Assert::assertEquals($expected, $this->userMapper->getOrCreate($providerId, $sub, $id4me)->getUserId()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/unit/Service/IdServiceTest.php: -------------------------------------------------------------------------------- 1 | providerService = $this->createMock(ProviderService::class); 30 | $this->providerMapper = $this->createMock(ProviderMapper::class); 31 | 32 | $this->idService = new LocalIdService($this->providerService, $this->providerMapper); 33 | } 34 | 35 | public function dataGetId() { 36 | return [ 37 | [1, 'provider1', 'id1', false, false, false, 'id1'], 38 | [2, 'provider2', 'id2', true, false, false, 'a86d8ab935af1778a321e615db5116e850c85a7a3070049ccd824b8989ccb4d5'], 39 | [3, 'provider3', 'id3', true, true, false, '9d0fe311ee92abbc64b0b6caf1c9540b128f694f2627a3e926ea22118fa37ebb'], 40 | [4, 'provider4', 'id4', true, false, true, 'd86655557c03285dc91483102a5935ce8e9e2d424ba6e5ef8b4e6c08cf331275'], 41 | [5, 'provider5', 'id5', true, true, true, '6c872bc08d76ef132c9ac3fabbcda24cc4866e5a9fd4d2fd592ce3fc2866c972'], 42 | [6, 'provider6', 'id6', false, true, false, '1d64d63dbda703f2598581b8bfc440e191c7a668dfb1de40c7daf41c6f9204c7'], 43 | [7, 'provider7', 'id7', false, true, true, '1b517769ab0d33c275bc88747917c38f1e1c7aa2582330fbe2d936fb6cfcabb3'], 44 | [8, 'provider8', 'id8', false, false, true, 'provider8-id8'], 45 | ]; 46 | } 47 | 48 | /** @dataProvider dataGetId */ 49 | public function testGetId(int $providerId, string $providerName, string $id, bool $id4me, bool $uniqueId, bool $providerBasedId, string $expected): void { 50 | $provider = new Provider(); 51 | $provider->setIdentifier($providerName); 52 | 53 | $this->providerMapper->method('getProvider')->willReturn($provider); 54 | 55 | $this->providerService 56 | ->method('getSetting') 57 | ->will($this->returnValueMap( 58 | [ 59 | [$providerId, ProviderService::SETTING_UNIQUE_UID, '1', $uniqueId ? '1' : '0'], 60 | [$providerId, ProviderService::SETTING_PROVIDER_BASED_ID, '0', $providerBasedId ? '1' : '0'], 61 | ] 62 | )); 63 | 64 | $result = $this->idService->getId($providerId, $id, $id4me); 65 | 66 | $this->assertEquals($expected, $result); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /vendor-bin/mozart/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "coenjacobs/mozart": "^0.7.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /webpack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | const path = require('path') 7 | const webpackConfig = require('@nextcloud/webpack-vue-config') 8 | const ESLintPlugin = require('eslint-webpack-plugin') 9 | const StyleLintPlugin = require('stylelint-webpack-plugin') 10 | 11 | const buildMode = process.env.NODE_ENV 12 | const isDev = buildMode === 'development' 13 | webpackConfig.devtool = isDev ? 'cheap-source-map' : 'source-map' 14 | 15 | webpackConfig.entry = { 16 | 'admin-settings': path.join(__dirname, 'src', 'main-settings.js'), 17 | timezone: path.join(__dirname, 'src', 'timezone.js'), 18 | } 19 | 20 | webpackConfig.plugins.push( 21 | new ESLintPlugin({ 22 | extensions: ['js', 'vue'], 23 | files: 'src', 24 | failOnError: !isDev, 25 | }) 26 | ) 27 | webpackConfig.plugins.push( 28 | new StyleLintPlugin({ 29 | files: 'src/**/*.{css,scss,vue}', 30 | failOnError: !isDev, 31 | }), 32 | ) 33 | 34 | module.exports = webpackConfig 35 | --------------------------------------------------------------------------------