diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 00000000..a942804d --- /dev/null +++ b/.github/README.md @@ -0,0 +1,54 @@ +# MagentaCLOUD user_oidc + +Customisation of the Nextcloud delivered OpenID connect app for MagentaCLOUD. + +The app extends the standard `user_oidc` Nextcloud app, +see [upstream configuration hints for basic setup](https://github.com/nextcloud/user_oidc/blob/main/README.md) + + +## Feature: Event-based provisioning (upstream contribution candidate) +The mechanism allows to implement custom puser provisioning logic in a separate Nextcloud app by +registering and handling a attribute change and provisioning event: + +``` +use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; +class Application extends App implements IBootstrap { +... + public function register(IRegistrationContext $context): void { + $context->registerEventListener(AttributeMappedEvent::class, MyUserAttributeListener::class); + $context->registerEventListener(UserAccountChangeEvent::class, MyUserAccountChangeListener::class); + } +... +} +``` +The provisioning handler should return a `OCA\UserOIDC\Event\UserAccountChangeResult` object + +## Feature: Telekom-specific bearer token + +Due to historic reason, Telekom bearer tokens have a close to standard structure, but +require special security implementation in detail. The customisation overrides te standard + + +### Requiring web-token libraries +The central configuration branch `nmc/2372-central-setup` automatic merge will frequently fail if composer +upstream + +The fast and easy way to bring it back to sync with upstream is: +``` +git checkout nmc/2372-central-setup +git rebase --onto main nmc/2372-central-setup +# manually take over everything from upstream for composer.lock (TODO: automate that) +# ALWAYS update web-token dependencies in composer.lock +# to avoid upstream conflicts. The lock file diff should only contain adds to upstream state! +composer update "web-token/jwt-*" +``` + + +### Configuring an additional Bearer preshared secret with provider +TODO + +### Testing Bearer secrets +TODO diff --git a/.github/workflows/nmc-custom-app-release.yml b/.github/workflows/nmc-custom-app-release.yml new file mode 100644 index 00000000..64d287ce --- /dev/null +++ b/.github/workflows/nmc-custom-app-release.yml @@ -0,0 +1,68 @@ +### +# SPDX-License-Identifier: AGPL-3.0 +# +# Author: Bernd rederlechner +# +# Builds a stable release package based on a release assembly +# customisation-- +# +# As soon as a package is deployed to production, the tag and the branch +# MUST STAY FOR 2 years and not deleted. +# +# Release packages, tags and customisation branches not delivered to production should +# be deleted asap a newer release is available. +# + +name: MCLOUD custom app release + +on: + workflow_dispatch: + inputs: + increment: + description: 'Release increment' + required: true + type: number + branch: + type: choice + description: Branch to build a package from + options: + - main + - stable25 + - stable26 + - stable27 + default: main + +jobs: + check-custom: + uses: nextmcloud/.github/.github/workflows/nmc-app-precond.yml@master + with: + versionbranch: ${{ inputs.branch }} + increment: ${{ inputs.increment }} + secrets: inherit + assemble-custom: + uses: nextmcloud/.github/.github/workflows/nmc-custom-assembly.yml@master + needs: check-custom + with: + trunk: 'main' + stable: ${{ inputs.branch }} + result: ${{ format('customisation-{0}-{1}', inputs.branch, inputs.increment ) }} + secrets: inherit + + composerdep: + strategy: + fail-fast: false + uses: ./.github/workflows/nmc-custom-oidc-composer.yml + needs: assemble-custom + with: + assembly: ${{ format('customisation-{0}-{1}', inputs.branch, inputs.increment) }} + secrets: inherit + + build-custom: + uses: nextmcloud/.github/.github/workflows/nmc-custom-app-build.yml@master + needs: [ check-custom, composerdep ] + with: + appname: ${{ needs.check-custom.outputs.appname }} + assembly: ${{ format('customisation-{0}-{1}', inputs.branch , inputs.increment ) }} + tag: ${{ needs.check-custom.outputs.tag }} + prerelease: ${{ inputs.branch == 'main' && true || false }} + secrets: inherit diff --git a/.github/workflows/nmc-custom-app-versions.yml b/.github/workflows/nmc-custom-app-versions.yml new file mode 100644 index 00000000..e18f1941 --- /dev/null +++ b/.github/workflows/nmc-custom-app-versions.yml @@ -0,0 +1,72 @@ +### +# SPDX-License-Identifier: AGPL-3.0 +# +# Author: Bernd rederlechner +# +# Assemble a customisation for trunk (no backports) and stable +# (backport xor trunk) +# +# It creates review (user-specific) customisations branches +# - customisation-- +# - customisation-- + +name: MCLOUD custom app versions + +### +# The customisation-* branches are always reassembled if a customisation branch +# is updated or included into a custom PR +on: + workflow_dispatch: + pull_request: + types: + - opened + - reopened + - synchronize + branches: + - master + - main + - trunk + - nmcstable/** + # - stable/** + +jobs: + + assemble: + strategy: + fail-fast: false + matrix: + custombase: [ "main" ] + uses: nextmcloud/.github/.github/workflows/nmc-custom-assembly.yml@master + with: + trunk: "main" + stable: ${{ matrix.custombase }} + result: ${{ format('customisation-{0}-{1}', github.actor, matrix.custombase) }} + secrets: inherit + + composerdep: + strategy: + fail-fast: false + matrix: + custombase: [ "main" ] + uses: ./.github/workflows/nmc-custom-oidc-composer.yml + needs: assemble + with: + assembly: ${{ format('customisation-{0}-{1}', github.actor, matrix.custombase) }} + secrets: inherit + + phpunit: + strategy: + fail-fast: false + matrix: + phpversion: ['8.0', '8.1'] + database: ['mysql'] + custombase: [ "main" ] + uses: nextmcloud/.github/.github/workflows/nmc-custom-app-phpunit.yml@master + needs: composerdep + with: + assembly: ${{ format('customisation-{0}-{1}', github.actor, matrix.custombase) }} + appname: 'user_oidc' + server-branch: ${{ matrix.custombase }} + phpversion: ${{ matrix.phpversion }} + database: ${{ matrix.database }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/nmc-custom-oidc-composer.yml b/.github/workflows/nmc-custom-oidc-composer.yml new file mode 100644 index 00000000..d4f2c527 --- /dev/null +++ b/.github/workflows/nmc-custom-oidc-composer.yml @@ -0,0 +1,81 @@ +### +# SPDX-License-Identifier: AGPL-3.0 +# +# Author: Bernd Rederlechner 'login#code', 'url' => '/code', 'verb' => 'GET'], ['name' => 'login#singleLogoutService', 'url' => '/sls', 'verb' => 'GET'], ['name' => 'login#backChannelLogout', 'url' => '/backchannel-logout/{providerIdentifier}', 'verb' => 'POST'], + ['name' => 'login#telekomBackChannelLogout', 'url' => '/logout', 'verb' => 'POST'], - ['name' => 'api#createUser', 'url' => '/user', 'verb' => 'POST'], - ['name' => 'api#deleteUser', 'url' => '/user/{userId}', 'verb' => 'DELETE'], + // ['name' => 'api#createUser', 'url' => '/user', 'verb' => 'POST'], + // ['name' => 'api#deleteUser', 'url' => '/user/{userId}', 'verb' => 'DELETE'], ['name' => 'id4me#showLogin', 'url' => '/id4me', 'verb' => 'GET'], ['name' => 'id4me#login', 'url' => '/id4me', 'verb' => 'POST'], diff --git a/composer.json b/composer.json index 438248e8..a100b2fc 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,17 @@ "require": { "id4me/id4me-rp": "^1.2", "firebase/php-jwt": "^6.8.1", - "bamarni/composer-bin-plugin": "^1.4" + "bamarni/composer-bin-plugin": "^1.4", + "web-token/jwt-core": "^2.0", + "web-token/jwt-encryption": "^2.2", + "web-token/jwt-signature": "^2.2", + "web-token/jwt-encryption-algorithm-aescbc": "^2.2", + "web-token/jwt-encryption-algorithm-ecdh-es": "^2.2", + "web-token/jwt-encryption-algorithm-rsa": "^2.2", + "web-token/jwt-encryption-algorithm-pbes2": "^2.2", + "web-token/jwt-signature-algorithm-hmac": "^2.2", + "web-token/jwt-signature-algorithm-rsa": "^2.2", + "web-token/jwt-util-ecc": "^2.2" }, "require-dev": { "nextcloud/coding-standard": "^1.0.0", diff --git a/composer.lock b/composer.lock index 0955e1d7..2ca3ee0a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "04286fb7b53817717fac1f09a0a61a40", + "content-hash": "bb379f676922e3656d7bc0ed16205df8", "packages": [ { "name": "bamarni/composer-bin-plugin", @@ -63,6 +63,142 @@ }, "time": "2022-10-31T08:38:03+00:00" }, + { + "name": "brick/math", + "version": "0.9.3", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae", + "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", + "vimeo/psalm": "4.9.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.9.3" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/brick/math", + "type": "tidelift" + } + ], + "time": "2021-08-15T20:50:18+00:00" + }, + { + "name": "fgrosse/phpasn1", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/fgrosse/PHPASN1.git", + "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/42060ed45344789fb9f21f9f1864fc47b9e3507b", + "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "~2.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "suggest": { + "ext-bcmath": "BCmath is the fallback extension for big integer calculations", + "ext-curl": "For loading OID information from the web if they have not bee defined statically", + "ext-gmp": "GMP is the preferred extension for big integer calculations", + "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "FG\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Friedrich Große", + "email": "friedrich.grosse@gmail.com", + "homepage": "https://github.com/FGrosse", + "role": "Author" + }, + { + "name": "All contributors", + "homepage": "https://github.com/FGrosse/PHPASN1/contributors" + } + ], + "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", + "homepage": "https://github.com/FGrosse/PHPASN1", + "keywords": [ + "DER", + "asn.1", + "asn1", + "ber", + "binary", + "decoding", + "encoding", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/fgrosse/PHPASN1/issues", + "source": "https://github.com/fgrosse/PHPASN1/tree/v2.5.0" + }, + "abandoned": true, + "time": "2022-12-19T11:08:26+00:00" + }, { "name": "firebase/php-jwt", "version": "v6.11.1", @@ -279,6 +415,1106 @@ } ], "time": "2024-12-14T21:03:54+00:00" + }, + { + "name": "spomky-labs/aes-key-wrap", + "version": "v6.0.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/aes-key-wrap.git", + "reference": "97388255a37ad6fb1ed332d07e61fa2b7bb62e0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/aes-key-wrap/zipball/97388255a37ad6fb1ed332d07e61fa2b7bb62e0d", + "reference": "97388255a37ad6fb1ed332d07e61fa2b7bb62e0d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "lib-openssl": "*", + "php": ">=7.2", + "thecodingmachine/safe": "^1.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-beberlei-assert": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^7.0|^8.0|^9.0", + "thecodingmachine/phpstan-safe-rule": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "AESKW\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky-Labs/aes-key-wrap/contributors" + } + ], + "description": "AES Key Wrap for PHP.", + "homepage": "https://github.com/Spomky-Labs/aes-key-wrap", + "keywords": [ + "A128KW", + "A192KW", + "A256KW", + "RFC3394", + "RFC5649", + "aes", + "key", + "padding", + "wrap" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/aes-key-wrap/issues", + "source": "https://github.com/Spomky-Labs/aes-key-wrap/tree/v6.0.0" + }, + "time": "2020-08-01T14:07:55+00:00" + }, + { + "name": "spomky-labs/base64url", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/base64url.git", + "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d", + "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.11|^0.12", + "phpstan/phpstan-beberlei-assert": "^0.11|^0.12", + "phpstan/phpstan-deprecation-rules": "^0.11|^0.12", + "phpstan/phpstan-phpunit": "^0.11|^0.12", + "phpstan/phpstan-strict-rules": "^0.11|^0.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Base64Url\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky-Labs/base64url/contributors" + } + ], + "description": "Base 64 URL Safe Encoding/Decoding PHP Library", + "homepage": "https://github.com/Spomky-Labs/base64url", + "keywords": [ + "base64", + "rfc4648", + "safe", + "url" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/base64url/issues", + "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2020-11-03T09:10:25+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpstan/phpstan": "^0.12", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^0.12" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1-dev" + } + }, + "autoload": { + "files": [ + "deprecated/apc.php", + "deprecated/libevent.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/ingres-ii.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/msql.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/mysqlndMs.php", + "generated/mysqlndQc.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/password.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pdf.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/simplexml.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "psr-4": { + "Safe\\": [ + "lib/", + "deprecated/", + "generated/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" + }, + "time": "2020-10-28T17:51:34+00:00" + }, + { + "name": "web-token/jwt-core", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-core.git", + "reference": "53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-core/zipball/53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678", + "reference": "53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.17|^0.9", + "ext-json": "*", + "ext-mbstring": "*", + "fgrosse/phpasn1": "^2.0", + "php": ">=7.2", + "spomky-labs/base64url": "^1.0|^2.0" + }, + "conflict": { + "spomky-labs/jose": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Core\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "Core component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-core/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2021-03-17T14:55:52+00:00" + }, + { + "name": "web-token/jwt-encryption", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-encryption.git", + "reference": "3b8d67d7c5c013750703e7c27f1001544407bbb2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-encryption/zipball/3b8d67d7c5c013750703e7c27f1001544407bbb2", + "reference": "3b8d67d7c5c013750703e7c27f1001544407bbb2", + "shasum": "" + }, + "require": { + "web-token/jwt-core": "^2.1" + }, + "suggest": { + "web-token/jwt-encryption-algorithm-aescbc": "AES CBC Based Content Encryption Algorithms", + "web-token/jwt-encryption-algorithm-aesgcm": "AES GCM Based Content Encryption Algorithms", + "web-token/jwt-encryption-algorithm-aesgcmkw": "AES GCM Key Wrapping Based Key Encryption Algorithms", + "web-token/jwt-encryption-algorithm-aeskw": "AES Key Wrapping Based Key Encryption Algorithms", + "web-token/jwt-encryption-algorithm-dir": "Direct Key Encryption Algorithms", + "web-token/jwt-encryption-algorithm-ecdh-es": "ECDH-ES Based Key Encryption Algorithms", + "web-token/jwt-encryption-algorithm-experimental": "Experimental Key and Signature Algorithms", + "web-token/jwt-encryption-algorithm-pbes2": "PBES2 Based Key Encryption Algorithms", + "web-token/jwt-encryption-algorithm-rsa": "RSA Based Key Encryption Algorithms" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Encryption\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-encryption/contributors" + } + ], + "description": "Encryption component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-encryption/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2021-03-17T14:55:52+00:00" + }, + { + "name": "web-token/jwt-encryption-algorithm-aescbc", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-encryption-algorithm-aescbc.git", + "reference": "0359b82b349c8bbc82c19ba0382e1a1b3f788421" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-encryption-algorithm-aescbc/zipball/0359b82b349c8bbc82c19ba0382e1a1b3f788421", + "reference": "0359b82b349c8bbc82c19ba0382e1a1b3f788421", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "web-token/jwt-encryption": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Encryption\\Algorithm\\ContentEncryption\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "AES CBC Based Content Encryption Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-encryption-algorithm-aescbc/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/jwt-encryption-algorithm-ecdh-es", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-encryption-algorithm-ecdh-es.git", + "reference": "736c2c5a3997e4cfb84e9f8f63bf17b5d7ca4fd0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-encryption-algorithm-ecdh-es/zipball/736c2c5a3997e4cfb84e9f8f63bf17b5d7ca4fd0", + "reference": "736c2c5a3997e4cfb84e9f8f63bf17b5d7ca4fd0", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "spomky-labs/aes-key-wrap": "^5.0|^6.0", + "web-token/jwt-encryption": "^2.1", + "web-token/jwt-util-ecc": "^2.1" + }, + "suggest": { + "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Encryption\\Algorithm\\KeyEncryption\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "ECDH-ES Based Key Encryption Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-encryption-algorithm-ecdh-es/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2021-03-17T14:55:52+00:00" + }, + { + "name": "web-token/jwt-encryption-algorithm-pbes2", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-encryption-algorithm-pbes2.git", + "reference": "d0294e7821d4a9b70454d3b13441add59c525275" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-encryption-algorithm-pbes2/zipball/d0294e7821d4a9b70454d3b13441add59c525275", + "reference": "d0294e7821d4a9b70454d3b13441add59c525275", + "shasum": "" + }, + "require": { + "web-token/jwt-encryption": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Encryption\\Algorithm\\KeyEncryption\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "PBES2* Based Key Encryption Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-encryption-algorithm-pbes2/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/jwt-encryption-algorithm-rsa", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-encryption-algorithm-rsa.git", + "reference": "2aab79c4cda093d2ee94756d0b1b46e93b380f55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-encryption-algorithm-rsa/zipball/2aab79c4cda093d2ee94756d0b1b46e93b380f55", + "reference": "2aab79c4cda093d2ee94756d0b1b46e93b380f55", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.17|^0.9", + "ext-openssl": "*", + "symfony/polyfill-mbstring": "^1.12", + "web-token/jwt-encryption": "^2.1" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance", + "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Encryption\\Algorithm\\KeyEncryption\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "RSA Based Key Encryption Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-encryption-algorithm-rsa/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/jwt-signature", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature.git", + "reference": "015b59aaf3b6e8fb9f5bd1338845b7464c7d8103" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature/zipball/015b59aaf3b6e8fb9f5bd1338845b7464c7d8103", + "reference": "015b59aaf3b6e8fb9f5bd1338845b7464c7d8103", + "shasum": "" + }, + "require": { + "web-token/jwt-core": "^2.1" + }, + "suggest": { + "web-token/jwt-signature-algorithm-ecdsa": "ECDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-eddsa": "EdDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-experimental": "Experimental Signature Algorithms", + "web-token/jwt-signature-algorithm-hmac": "HMAC Based Signature Algorithms", + "web-token/jwt-signature-algorithm-none": "None Signature Algorithm", + "web-token/jwt-signature-algorithm-rsa": "RSA Based Signature Algorithms" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-signature/contributors" + } + ], + "description": "Signature component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2021-03-01T19:55:28+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-hmac", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-hmac.git", + "reference": "d208b1c50b408fa711bfeedeed9fb5d9be1d3080" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-hmac/zipball/d208b1c50b408fa711bfeedeed9fb5d9be1d3080", + "reference": "d208b1c50b408fa711bfeedeed9fb5d9be1d3080", + "shasum": "" + }, + "require": { + "web-token/jwt-signature": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\Algorithm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "HMAC Based Signature Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-hmac/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-rsa", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-rsa.git", + "reference": "513ad90eb5ef1886ff176727a769bda4618141b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-rsa/zipball/513ad90eb5ef1886ff176727a769bda4618141b0", + "reference": "513ad90eb5ef1886ff176727a769bda4618141b0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.17|^0.9", + "ext-openssl": "*", + "web-token/jwt-signature": "^2.1" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance", + "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\Algorithm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "RSA Based Signature Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-rsa/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/jwt-util-ecc", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-util-ecc.git", + "reference": "915f3fde86f5236c205620d61177b9ef43863deb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-util-ecc/zipball/915f3fde86f5236c205620d61177b9ef43863deb", + "reference": "915f3fde86f5236c205620d61177b9ef43863deb", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.17|^0.9" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance", + "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Core\\Util\\Ecc\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "ECC Tools for the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-util-ecc/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2021-03-24T13:35:17+00:00" } ], "packages-dev": [ diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 9842067f..de4272e2 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -20,7 +20,10 @@ use OCA\UserOIDC\Listener\InternalTokenRequestedListener; use OCA\UserOIDC\Listener\TimezoneHandlingListener; use OCA\UserOIDC\Listener\TokenInvalidatedListener; +use OCA\UserOIDC\MagentaBearer\MBackend; use OCA\UserOIDC\Service\ID4MeService; +use OCA\UserOIDC\Service\ProvisioningEventService; +use OCA\UserOIDC\Service\ProvisioningService; use OCA\UserOIDC\Service\SettingsService; use OCA\UserOIDC\Service\TokenService; use OCA\UserOIDC\User\Backend; @@ -31,9 +34,14 @@ use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; +use OCP\ISession; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Security\ISecureRandom; + +// this is needed only for the special, shortened client login flow +use Psr\Container\ContainerInterface; use Throwable; class Application extends App implements IBootstrap { @@ -48,11 +56,19 @@ public function __construct(array $urlParams = []) { } public function register(IRegistrationContext $context): void { + // Register the composer autoloader required for the added jwt-token libs + include_once __DIR__ . '/../../vendor/autoload.php'; + + // override registration of provisioning srevice to use event-based solution + $this->getContainer()->registerService(ProvisioningService::class, function (ContainerInterface $c): ProvisioningService { + return $c->get(ProvisioningEventService::class); + }); + /** @var IUserManager $userManager */ $userManager = $this->getContainer()->get(IUserManager::class); /* Register our own user backend */ - $this->backend = $this->getContainer()->get(Backend::class); + $this->backend = $this->getContainer()->get(MBackend::class); $config = $this->getContainer()->get(IConfig::class); if (version_compare($config->getSystemValueString('version', '0.0.0'), '32.0.0', '>=')) { @@ -74,7 +90,7 @@ public function register(IRegistrationContext $context): void { public function boot(IBootContext $context): void { $context->injectFn(\Closure::fromCallable([$this->backend, 'injectSession'])); - $context->injectFn(\Closure::fromCallable([$this, 'checkLoginToken'])); + // $context->injectFn(\Closure::fromCallable([$this, 'checkLoginToken'])); /** @var IUserSession $userSession */ $userSession = $this->getContainer()->get(IUserSession::class); if ($userSession->isLoggedIn()) { @@ -84,10 +100,73 @@ public function boot(IBootContext $context): void { try { $context->injectFn(\Closure::fromCallable([$this, 'registerRedirect'])); $context->injectFn(\Closure::fromCallable([$this, 'registerLogin'])); + + // this is the custom auto-redirect for MagentaCLOUD client access + $context->injectFn(\Closure::fromCallable([$this, 'registerNmcClientFlow'])); } catch (Throwable $e) { } } + /** + * This is the automatic redirect exclusively for Nextcloud/Magentacloud clients completely skipping consent layer + */ + private function registerNmcClientFlow(IRequest $request, + IURLGenerator $urlGenerator, + ProviderMapper $providerMapper, + ISession $session, + ISecureRandom $random): void { + $providers = $this->getCachedProviders($providerMapper); + + // Handle immediate redirect on client first-time login + $isClientLoginFlow = false; + + try { + $isClientLoginFlow = $request->getPathInfo() === '/login/flow'; + } catch (Exception $e) { + // in case any errors happen when checking for the path do not apply redirect logic as it is only needed for the login + } + + if ($isClientLoginFlow) { + // only redirect if Telekom provider registered + $tproviders = array_values(array_filter($providers, function ($p) { + return strtolower($p->getIdentifier()) === 'telekom'; + })); + + if (count($tproviders) == 0) { + // always show normal login flow as error fallback + return; + } + + $stateToken = $random->generate(64, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); + $session->set('client.flow.state.token', $stateToken); + + // call the service to get the params, but suppress the template + // compute grant redirect Url to go directly to Telekom login + $redirectUrl = $urlGenerator->linkToRoute('core.ClientFlowLogin.grantPage', [ + 'stateToken' => $stateToken, + // grantPage service operation is deriving oauth2 client name (again), + // so we simply pass on clientIdentifier or empty string + 'clientIdentifier' => $request->getParam('clientIdentifier', ''), + 'direct' => $request->getParam('direct', '0') + ]); + + if ($redirectUrl === null) { + + // always show normal login flow as error fallback + return; + } + + // direct login, consent layer later + $targetUrl = $urlGenerator->linkToRoute(self::APP_ID . '.login.login', [ + 'providerId' => $tproviders[0]->getId(), + 'redirectUrl' => $redirectUrl + ]); + + header('Location: ' . $targetUrl); + exit(); + } + } + private function checkLoginToken(TokenService $tokenService): void { $tokenService->checkLoginToken(); } diff --git a/lib/Command/UpsertProvider.php b/lib/Command/UpsertProvider.php index f6690046..825100e4 100644 --- a/lib/Command/UpsertProvider.php +++ b/lib/Command/UpsertProvider.php @@ -179,6 +179,7 @@ protected function configure() { ->addOption('clientid', 'c', InputOption::VALUE_REQUIRED, 'OpenID client identifier') ->addOption('clientsecret', 's', InputOption::VALUE_REQUIRED, 'OpenID client secret') ->addOption('discoveryuri', 'd', InputOption::VALUE_REQUIRED, 'OpenID discovery endpoint uri') + ->addOption('bearersecret', 'bs', InputOption::VALUE_OPTIONAL, 'Telekom bearer token requires a different client secret for bearer tokens') ->addOption('endsessionendpointuri', 'e', InputOption::VALUE_REQUIRED, 'OpenID end session endpoint uri') ->addOption('postlogouturi', 'p', InputOption::VALUE_REQUIRED, 'Post logout URI') ->addOption('scope', 'o', InputOption::VALUE_OPTIONAL, 'OpenID requested value scopes, if not set defaults to "openid email profile"'); @@ -206,10 +207,17 @@ protected function execute(InputInterface $input, OutputInterface $output) { return $this->listProviders($input, $output); } + // bearersecret is usually base64 encoded, but SAM delivers it non-encoded by default + // so always encode/decode for this field + $bearersecret = $input->getOption('bearersecret'); + if ($bearersecret !== null) { + $bearersecret = $this->crypto->encrypt($this->base64UrlEncode($bearersecret)); + } + // check if any option for updating is provided $updateOptions = array_filter($input->getOptions(), static function ($value, $option) { return in_array($option, [ - 'identifier', 'clientid', 'clientsecret', 'discoveryuri', 'endsessionendpointuri', 'postlogouturi', 'scope', + 'identifier', 'clientid', 'clientsecret', 'discoveryuri', 'endsessionendpointuri', 'postlogouturi', 'scope', 'bearersecret', ...array_keys(self::EXTRA_OPTIONS), ]) && $value !== null; }, ARRAY_FILTER_USE_BOTH); @@ -250,7 +258,7 @@ protected function execute(InputInterface $input, OutputInterface $output) { } try { $provider = $this->providerMapper->createOrUpdateProvider( - $identifier, $clientid, $clientsecret, $discoveryuri, $scope, $endsessionendpointuri, $postLogoutUri + $identifier, $clientid, $clientsecret, $discoveryuri, $scope, $endsessionendpointuri, $postLogoutUri, $bearersecret ); // invalidate JWKS cache (even if it was just created) $this->providerService->setSetting($provider->getId(), ProviderService::SETTING_JWKS_CACHE, ''); @@ -306,4 +314,8 @@ private function listProviders(InputInterface $input, OutputInterface $output) { $table->render(); return 0; } + + private function base64UrlEncode(string $data): string { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } } diff --git a/lib/Controller/LoginController.php b/lib/Controller/LoginController.php index da786660..dcc0873e 100644 --- a/lib/Controller/LoginController.php +++ b/lib/Controller/LoginController.php @@ -24,6 +24,7 @@ use OCA\UserOIDC\Service\LdapService; use OCA\UserOIDC\Service\OIDCService; use OCA\UserOIDC\Service\ProviderService; +use OCA\UserOIDC\Service\ProvisioningDeniedException; use OCA\UserOIDC\Service\ProvisioningService; use OCA\UserOIDC\Service\SettingsService; use OCA\UserOIDC\Service\TokenService; @@ -171,12 +172,26 @@ public function login(int $providerId, ?string $redirectUrl = null) { return $this->buildErrorTemplateResponse($message, Http::STATUS_NOT_FOUND, ['reason' => 'provider unreachable']); } - $state = $this->random->generate(32, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_UPPER); - $this->session->set(self::STATE, $state); - $this->session->set(self::REDIRECT_AFTER_LOGIN, $redirectUrl); + // $state = $this->random->generate(32, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_UPPER); + // $this->session->set(self::STATE, $state); + // $this->session->set(self::REDIRECT_AFTER_LOGIN, $redirectUrl); - $nonce = $this->random->generate(32, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_UPPER); - $this->session->set(self::NONCE, $nonce); + // $nonce = $this->random->generate(32, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_UPPER); + // $this->session->set(self::NONCE, $nonce); + + // check if oidc state is present in session data + if ($this->session->exists(self::STATE)) { + $state = $this->session->get(self::STATE); + $nonce = $this->session->get(self::NONCE); + } else { + $state = $this->random->generate(32, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_UPPER); + $this->session->set(self::STATE, $state); + $this->session->set(self::REDIRECT_AFTER_LOGIN, $redirectUrl); + + $nonce = $this->random->generate(32, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_UPPER); + $this->session->set(self::NONCE, $nonce); + $this->session->set(self::PROVIDERID, $providerId); + } $oidcSystemConfig = $this->config->getSystemValue('user_oidc', []); $isPkceSupported = in_array('S256', $discovery['code_challenge_methods_supported'] ?? [], true); @@ -188,7 +203,7 @@ public function login(int $providerId, ?string $redirectUrl = null) { $this->session->set(self::CODE_VERIFIER, $code_verifier); } - $this->session->set(self::PROVIDERID, $providerId); + // $this->session->set(self::PROVIDERID, $providerId); $this->session->close(); // get attribute mapping settings @@ -321,6 +336,11 @@ public function code(string $state = '', string $code = '', string $scope = '', $this->logger->debug('Code login with core: ' . $code . ' and state: ' . $state); if ($error !== '') { + if (!$this->isMobileDevice()) { + $cancelRedirectUrl = $this->config->getSystemValue('user_oidc.cancel_redirect_url', 'https://cloud.telekom-dienste.de/'); + return new RedirectResponse($cancelRedirectUrl); + } + $this->logger->warning('Code login error', ['error' => $error, 'error_description' => $error_description]); if ($this->isDebugModeEnabled()) { return new JSONResponse([ @@ -554,6 +574,24 @@ public function code(string $state = '', string $code = '', string $scope = '', } if ($autoProvisionAllowed) { + $user = null; + + try { + // use potential user from other backend, create it in our backend if it does not exist + $user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload, $existingUser); + } catch (ProvisioningDeniedException $denied) { + // TODO: MagentaCLOUD should upstream the exception handling + $redirectUrl = $denied->getRedirectUrl(); + if ($redirectUrl === null) { + $message = $this->l10n->t('Failed to provision user'); + return $this->build403TemplateResponse($message, Http::STATUS_BAD_REQUEST, ['reason' => $denied->getMessage()]); + } else { + // error response is a redirect, e.g. to a booking site + // so that you can immediately get the registration page + return new RedirectResponse($redirectUrl); + } + } + if (!$softAutoProvisionAllowed && $existingUser !== null && $existingUser->getBackendClassName() !== Application::APP_ID) { // if soft auto-provisioning is disabled, // we refuse login for a user that already exists in another backend @@ -601,16 +639,20 @@ public function code(string $state = '', string $code = '', string $scope = '', $this->eventDispatcher->dispatchTyped(new UserLoggedInEvent($user, $user->getUID(), null, false)); } - $storeLoginTokenEnabled = $this->appConfig->getValueString(Application::APP_ID, 'store_login_token', '0') === '1'; - if ($storeLoginTokenEnabled) { + // $storeLoginTokenEnabled = $this->appConfig->getValueString(Application::APP_ID, 'store_login_token', '0') === '1'; + // if ($storeLoginTokenEnabled) { // store all token information for potential token exchange requests - $tokenData = array_merge( - $data, - ['provider_id' => $providerId], - ); - $this->tokenService->storeToken($tokenData); - } - $this->config->setUserValue($user->getUID(), Application::APP_ID, 'had_token_once', '1'); + // $tokenData = array_merge( + // $data, + // ['provider_id' => $providerId], + // ); + // $this->tokenService->storeToken($tokenData); + // } + // $this->config->setUserValue($user->getUID(), Application::APP_ID, 'had_token_once', '1'); + + // remove code login session values + $this->session->remove(self::STATE); + $this->session->remove(self::NONCE); // Set last password confirm to the future as we don't have passwords to confirm against with SSO $this->session->set('last-password-confirm', strtotime('+4 year', time())); @@ -619,7 +661,7 @@ public function code(string $state = '', string $code = '', string $scope = '', try { $authToken = $this->authTokenProvider->getToken($this->session->getId()); $this->sessionMapper->createOrUpdateSession( - $idTokenPayload->sid ?? 'fallback-sid', + $idTokenPayload->{'urn:telekom.com:session_token'} ?? 'fallback-sid', $idTokenPayload->sub ?? 'fallback-sub', $idTokenPayload->iss ?? 'fallback-iss', $authToken->getId(), @@ -897,10 +939,26 @@ private function getBackchannelLogoutErrorResponse( 'error' => $error, 'error_description' => $description, ], - Http::STATUS_BAD_REQUEST, + Http::STATUS_OK, ); } + /** + * Backward compatible function for MagentaCLOUD to smoothly transition to new config + * + * @PublicPage + * @NoCSRFRequired + * @BruteForceProtection(action=userOidcBackchannelLogout) + * + * @param string $logout_token + * @return JSONResponse + * @throws Exception + * @throws \JsonException + */ + public function telekomBackChannelLogout(string $logout_token = '') { + return $this->backChannelLogout('Telekom', $logout_token); + } + private function toCodeChallenge(string $data): string { // Basically one big work around for the base64url decode being weird $h = pack('H*', hash('sha256', $data)); @@ -910,4 +968,20 @@ private function toCodeChallenge(string $data): string { $s = str_replace('/', '_', $s); // 63rd char of encoding return $s; } + + private function isMobileDevice(): bool { + $mobileKeywords = $this->config->getSystemValue('user_oidc.mobile_keywords', ['Android', 'iPhone', 'iPad', 'iPod', 'Windows Phone', 'Mobile', 'webOS', 'BlackBerry', 'Opera Mini', 'IEMobile']); + + if (!isset($_SERVER['HTTP_USER_AGENT'])) { + return false; // if no user-agent is set, assume desktop + } + + foreach ($mobileKeywords as $keyword) { + if (stripos($_SERVER['HTTP_USER_AGENT'], $keyword) !== false) { + return true; // device is mobile + } + } + + return false; // device is desktop + } } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index d4d6b42d..319c55ba 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -77,7 +77,7 @@ public function isDiscoveryEndpointValid($url) { } #[PasswordConfirmationRequired] - public function createProvider(string $identifier, string $clientId, string $clientSecret, string $discoveryEndpoint, + public function createProvider(string $identifier, string $clientId, string $clientSecret, string $discoveryEndpoint, string $bearerSecret, array $settings = [], string $scope = 'openid email profile', ?string $endSessionEndpoint = null, ?string $postLogoutUri = null): JSONResponse { if ($this->providerService->getProviderByIdentifier($identifier) !== null) { @@ -102,6 +102,8 @@ public function createProvider(string $identifier, string $clientId, string $cli $provider->setEndSessionEndpoint($endSessionEndpoint ?: null); $provider->setPostLogoutUri($postLogoutUri ?: null); $provider->setScope($scope); + $encryptedBearerSecret = $this->crypto->encrypt($this->base64UrlEncode($bearerSecret)); + $provider->setBearerSecret($encryptedBearerSecret); $provider = $this->providerMapper->insert($provider); $providerSettings = $this->providerService->setSettings($provider->getId(), $settings); @@ -110,7 +112,7 @@ public function createProvider(string $identifier, string $clientId, string $cli } #[PasswordConfirmationRequired] - public function updateProvider(int $providerId, string $identifier, string $clientId, string $discoveryEndpoint, ?string $clientSecret = null, + public function updateProvider(int $providerId, string $identifier, string $clientId, string $discoveryEndpoint, ?string $clientSecret = null, ?string $bearerSecret = null, array $settings = [], string $scope = 'openid email profile', ?string $endSessionEndpoint = null, ?string $postLogoutUri = null): JSONResponse { $provider = $this->providerMapper->getProvider($providerId); @@ -134,6 +136,9 @@ public function updateProvider(int $providerId, string $identifier, string $clie $encryptedClientSecret = $this->crypto->encrypt($clientSecret); $provider->setClientSecret($encryptedClientSecret); } + if ($bearerSecret) { + $provider->setBearerSecret($this->base64UrlEncode($bearerSecret)); + } $provider->setDiscoveryEndpoint($discoveryEndpoint); $provider->setEndSessionEndpoint($endSessionEndpoint ?: null); $provider->setPostLogoutUri($postLogoutUri ?: null); @@ -185,4 +190,8 @@ public function setAdminConfig(array $values): JSONResponse { } return new JSONResponse([]); } + + private function base64UrlEncode(string $data): string { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } } diff --git a/lib/Db/Provider.php b/lib/Db/Provider.php index 838f10ff..6bf61ea8 100644 --- a/lib/Db/Provider.php +++ b/lib/Db/Provider.php @@ -23,6 +23,9 @@ * @method \void setEndSessionEndpoint(?string $endSessionEndpoint) * @method \string|\null getPostLogoutUri() * @method \void setPostLogoutUri(?string $postLogoutUri) + * @method string getBearerSecret() + * @method void setBearerSecret(string $bearerSecret) + * @method string getScope() * @method \void setScope(string $scope) */ class Provider extends Entity implements \JsonSerializable { @@ -40,6 +43,8 @@ class Provider extends Entity implements \JsonSerializable { /** @var string */ protected $postLogoutUri; /** @var string */ + protected $bearerSecret; + /** @var string */ protected $scope; /** diff --git a/lib/Db/ProviderMapper.php b/lib/Db/ProviderMapper.php index d724436d..107d7101 100644 --- a/lib/Db/ProviderMapper.php +++ b/lib/Db/ProviderMapper.php @@ -81,6 +81,7 @@ public function getProviders() { * @param string|null $clientid * @param string|null $clientsecret * @param string|null $discoveryuri + * @param string|null $bearersecret * @param string $scope * @param string|null $endsessionendpointuri * @param string|null $postLogoutUri @@ -90,7 +91,7 @@ public function getProviders() { * @throws MultipleObjectsReturnedException */ public function createOrUpdateProvider(string $identifier, ?string $clientid = null, - ?string $clientsecret = null, ?string $discoveryuri = null, string $scope = 'openid email profile', + ?string $clientsecret = null, ?string $discoveryuri = null, string $scope = 'openid email profile', ?string $bearersecret = null, ?string $endsessionendpointuri = null, ?string $postLogoutUri = null) { try { $provider = $this->findProviderByIdentifier($identifier); @@ -109,6 +110,7 @@ public function createOrUpdateProvider(string $identifier, ?string $clientid = n $provider->setDiscoveryEndpoint($discoveryuri); $provider->setEndSessionEndpoint($endsessionendpointuri); $provider->setPostLogoutUri($postLogoutUri); + $provider->setBearerSecret($bearersecret ?? ''); $provider->setScope($scope); return $this->insert($provider); } else { @@ -127,6 +129,9 @@ public function createOrUpdateProvider(string $identifier, ?string $clientid = n if ($postLogoutUri !== null) { $provider->setPostLogoutUri($postLogoutUri ?: null); } + if ($bearersecret !== null) { + $provider->setBearerSecret($bearersecret); + } $provider->setScope($scope); return $this->update($provider); } diff --git a/lib/Event/UserAccountChangeEvent.php b/lib/Event/UserAccountChangeEvent.php new file mode 100644 index 00000000..de9fb5e5 --- /dev/null +++ b/lib/Event/UserAccountChangeEvent.php @@ -0,0 +1,125 @@ + + * + * @license GNU AGPL version 3 or any later version + * + */ + +declare(strict_types=1); + +namespace OCA\UserOIDC\Event; + +use OCP\EventDispatcher\Event; + +/** + * Event to provide custom mapping logic based on the OIDC token data + * In order to avoid further processing the event propagation should be stopped + * in the listener after processing as the value might get overwritten afterwards + * by other listeners through $event->stopPropagation(); + */ +class UserAccountChangeEvent extends Event { + + /** @var string */ + private $uid; + + /** @var string|null */ + private $displayname; + + /** @var string|null */ + private $mainEmail; + + /** @var string|null */ + private $quota; + + /** @var object */ + private $claims; + + /** @var UserAccountChangeResult */ + private $result; + + public function __construct( + string $uid, + ?string $displayname, + ?string $mainEmail, + ?string $quota, + object $claims, + bool $accessAllowed = false + ) { + parent::__construct(); + $this->uid = $uid; + $this->displayname = $displayname; + $this->mainEmail = $mainEmail; + $this->quota = $quota; + $this->claims = $claims; + $this->result = new UserAccountChangeResult($accessAllowed, 'default'); + } + + /** + * Get the user ID (UID) associated with the event. + * + * @return string + */ + public function getUid(): string { + return $this->uid; + } + + /** + * Get the display name for the account. + * + * @return string|null + */ + public function getDisplayName(): ?string { + return $this->displayname; + } + + /** + * Get the primary email address. + * + * @return string|null + */ + public function getMainEmail(): ?string { + return $this->mainEmail; + } + + /** + * Get the quota assigned to the account. + * + * @return string|null + */ + public function getQuota(): ?string { + return $this->quota; + } + + /** + * Get the OIDC claims associated with the event. + * + * @return object + */ + public function getClaims(): object { + return $this->claims; + } + + /** + * Get the current result object. + * + * @return UserAccountChangeResult + */ + public function getResult(): UserAccountChangeResult { + return $this->result; + } + + /** + * Replace the result object with a new one. + * + * @param bool $accessAllowed Whether access should be allowed + * @param string $reason Optional reason for the decision + * @param string|null $redirectUrl Optional redirect URL + * @return void + */ + public function setResult(bool $accessAllowed, string $reason = '', ?string $redirectUrl = null): void { + $this->result = new UserAccountChangeResult($accessAllowed, $reason, $redirectUrl); + } +} diff --git a/lib/Event/UserAccountChangeResult.php b/lib/Event/UserAccountChangeResult.php new file mode 100644 index 00000000..1b19d639 --- /dev/null +++ b/lib/Event/UserAccountChangeResult.php @@ -0,0 +1,92 @@ + + * + * @license GNU AGPL version 3 or any later version + * + */ + +declare(strict_types=1); + +namespace OCA\UserOIDC\Event; + +/** + * Represents the result of an account change event decision. + * Used to signal whether access is allowed and optional redirect/reason info. + */ +class UserAccountChangeResult { + + /** @var bool */ + private $accessAllowed; + + /** @var string */ + private $reason; + + /** @var string|null */ + private $redirectUrl; + + public function __construct(bool $accessAllowed, string $reason = '', ?string $redirectUrl = null) { + $this->accessAllowed = $accessAllowed; + $this->redirectUrl = $redirectUrl; + $this->reason = $reason; + } + + /** + * Whether access for this user is allowed. + * + * @return bool + */ + public function isAccessAllowed(): bool { + return $this->accessAllowed; + } + + /** + * Set whether access for this user is allowed. + * + * @param bool $accessAllowed + * @return void + */ + public function setAccessAllowed(bool $accessAllowed): void { + $this->accessAllowed = $accessAllowed; + } + + /** + * Returns the optional alternate redirect URL. + * + * @return string|null + */ + public function getRedirectUrl(): ?string { + return $this->redirectUrl; + } + + /** + * Sets the optional alternate redirect URL. + * + * @param string|null $redirectUrl + * @return void + */ + public function setRedirectUrl(?string $redirectUrl): void { + $this->redirectUrl = $redirectUrl; + } + + /** + * Returns the decision reason. + * + * @return string + */ + public function getReason(): string { + return $this->reason; + } + + /** + * Sets the decision reason. + * + * @param string $reason + * @return void + */ + public function setReason(string $reason): void { + $this->reason = $reason; + } +} diff --git a/lib/Exception/AttributeValueException.php b/lib/Exception/AttributeValueException.php new file mode 100644 index 00000000..a4c80de4 --- /dev/null +++ b/lib/Exception/AttributeValueException.php @@ -0,0 +1,32 @@ +error; + } + + public function getErrorDescription(): ?string { + return $this->errorDescription; + } +} diff --git a/lib/MagentaBearer/InvalidTokenException.php b/lib/MagentaBearer/InvalidTokenException.php new file mode 100644 index 00000000..af97581b --- /dev/null +++ b/lib/MagentaBearer/InvalidTokenException.php @@ -0,0 +1,8 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\UserOIDC\MagentaBearer; + +use OCA\UserOIDC\AppInfo\Application; +use OCA\UserOIDC\Db\Provider; +use OCA\UserOIDC\Db\ProviderMapper; +use OCA\UserOIDC\Db\UserMapper; +use OCA\UserOIDC\Event\TokenValidatedEvent; +use OCA\UserOIDC\Service\DiscoveryService; +use OCA\UserOIDC\Service\ProviderService; +use OCA\UserOIDC\Service\ProvisioningDeniedException; +use OCA\UserOIDC\Service\ProvisioningEventService; +use OCA\UserOIDC\User\AbstractOidcBackend; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Security\ICrypto; +use Psr\Log\LoggerInterface; + +class MBackend extends AbstractOidcBackend { + + /** + * @var TokenService + */ + protected $mtokenService; + + /** + * @var ProvisioningEventService + */ + protected $provisioningService; + + /** + * @var ICrypto + */ + protected $crypto; + + public function __construct(IConfig $config, + UserMapper $userMapper, + LoggerInterface $logger, + IRequest $request, + ISession $session, + IURLGenerator $urlGenerator, + IEventDispatcher $eventDispatcher, + DiscoveryService $discoveryService, + ProviderMapper $providerMapper, + ProviderService $providerService, + IUserManager $userManager, + ICrypto $crypto, + TokenService $mtokenService, + ProvisioningEventService $provisioningService, + ) { + parent::__construct($config, $userMapper, $logger, $request, $session, + $urlGenerator, $eventDispatcher, $discoveryService, + $providerMapper, $providerService, $userManager); + + $this->mtokenService = $mtokenService; + $this->provisioningService = $provisioningService; + $this->crypto = $crypto; + } + + public function getBackendName(): string { + return Application::APP_ID . '\\MagentaBearer'; + } + + /** + * Backend is activated if header bearer token is detected. + * + * @return bool ture if bearer header found + */ + public function isSessionActive(): bool { + // if this returns true, getCurrentUserId is called + // not sure if we should rather to the validation in here as otherwise it might fail for other backends or bave other side effects + $headerToken = $this->request->getHeader(Application::OIDC_API_REQ_HEADER); + // session is active if we have a bearer token (API request) OR if we logged in via user_oidc (we have a provider ID in the session) + return (preg_match('/^\s*bearer\s+/i', $headerToken) != false); + } + + /** + * Return the id of the current user + * @return string + */ + public function getCurrentUserId(): string { + // get the bearer token from headers + $headerToken = $this->request->getHeader(Application::OIDC_API_REQ_HEADER); + $headerToken = preg_replace('/^bearer\s+/i', '', $headerToken); + if ($headerToken === '') { + $this->logger->debug('No Bearer token'); + return ''; + } + + $providers = $this->providerMapper->getProviders(); + if (count($providers) === 0) { + $this->logger->debug('no OIDC providers'); + return ''; + } + + // we implement only Telekom behavior (which includes auto-provisioning) + // so we neglect switches from the upstream Nexrcloud oidc handling + + // try to validate with all providers + foreach ($providers as $provider) { + if ($this->providerService->getSetting($provider->getId(), ProviderService::SETTING_CHECK_BEARER, '0') === '1') { + try { + $sharedSecret = $this->crypto->decrypt($provider->getBearerSecret()); + $bearerToken = $this->mtokenService->decryptToken($headerToken, $sharedSecret); + $this->mtokenService->verifySignature($bearerToken, $sharedSecret); + $payload = $this->mtokenService->decode($bearerToken); + $this->mtokenService->verifyClaims($payload, ['http://auth.magentacloud.de']); + } catch (InvalidTokenException $eToken) { + // there is + $this->logger->debug('Invalid token:' . $eToken->getMessage() . '. Trying another provider.'); + continue; + } catch (SignatureException $eSignature) { + // only the key seems not to fit, so try the next provider + $this->logger->debug($eSignature->getMessage() . '. Trying another provider.'); + continue; + } catch (\Throwable $e) { + // there is + $this->logger->debug('General non matching provider problem:' . $e->getMessage()); + continue; + } + + $uidAttribute = $this->providerService->getSetting($provider->getId(), ProviderService::SETTING_MAPPING_UID, 'sub'); + $userId = $payload->{$uidAttribute}; + if ($userId === null) { + $this->logger->debug('No extractable user id, check mapping!'); + return ''; + } + + // check bearercache here, not skipping validation for security reasons + + // Telekom bearer does not support refersh_token, so the pupose of TokenValidatedEvent is not given, + // but could produce trouble if not send with the field, apart from performance aspects. + // + // $discovery = $this->discoveryService->obtainDiscovery($provider); + // $this->eventDispatcher->dispatchTyped(new TokenValidatedEvent(['token' => $payload], $provider, $discovery)); + + try { + $this->provisioningService->provisionUser($userId, $provider->getId(), $payload); + $this->checkFirstLogin($userId); // create the folders same as on web login + return $userId; + } catch (ProvisioningDeniedException $denied) { + $this->logger->error('Bearer token access denied: ' . $denied->getMessage()); + return ''; + } + } + } + + $this->logger->debug('Could not find provider for token'); + return ''; + } + + /** + * FIXXME: send proper error status from BAckend errors + * + * This function sets an https status code here (early in the failing backend operation) + * to pass on bearer errors cleanly with correct status code and a readable reason + * + * For this, there is a "tricky" setting of a header needed to make it working in all + * known situations, see + * https://stackoverflow.com/questions/3258634/php-how-to-send-http-response-code + */ + // protected function sendHttpStatus(int $httpStatusCode, string $httpStatusMsg) { + // $phpSapiName = substr(php_sapi_name(), 0, 3); + // if ($phpSapiName == 'cgi' || $phpSapiName == 'fpm') { + // header('Status: ' . $httpStatusCode . ' ' . $httpStatusMsg); + // } else { + // $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0'; + // header($protocol . ' ' . $httpStatusCode . ' ' . $httpStatusMsg); + // } + // } +} diff --git a/lib/MagentaBearer/SignatureException.php b/lib/MagentaBearer/SignatureException.php new file mode 100644 index 00000000..ef04a4e0 --- /dev/null +++ b/lib/MagentaBearer/SignatureException.php @@ -0,0 +1,6 @@ + + * + * @author Bernd Rederlechner + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +declare(strict_types=1); + +namespace OCA\UserOIDC\MagentaBearer; + +use Jose\Component\Core\Algorithm; +use Jose\Component\Core\AlgorithmManager; + +use Jose\Component\Core\JWK; +use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512; +use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHESA256KW; + +use Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS512A256KW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP256; +use Jose\Component\Encryption\Compression\CompressionMethodManager; + +use Jose\Component\Encryption\Compression\Deflate; +use Jose\Component\Encryption\JWEDecrypter; +use Jose\Component\Signature\Algorithm\HS256; +use Jose\Component\Signature\Algorithm\HS384; + +use Jose\Component\Signature\Algorithm\HS512; +use Jose\Component\Signature\JWS; +use Jose\Component\Signature\JWSVerifier; +use OCP\AppFramework\Utility\ITimeFactory; +use Psr\Log\LoggerInterface; + +class TokenService { + + /** @var LoggerInterface */ + private $logger; + + /** @var ITimeFactory */ + private $timeFactory; + + /** @var DiscoveryService */ + private $discoveryService; + + public function __construct(LoggerInterface $logger, + ITimeFactory $timeFactory) { + $this->logger = $logger; + $this->timeFactory = $timeFactory; + + // The key encryption algorithm manager with the A256KW algorithm. + $keyEncryptionAlgorithmManager = new AlgorithmManager([ + new PBES2HS512A256KW(), + new RSAOAEP256(), + new ECDHESA256KW() ]); + + // The content encryption algorithm manager with the A256CBC-HS256 algorithm. + $contentEncryptionAlgorithmManager = new AlgorithmManager([ + new A256CBCHS512(), + ]); + + // The compression method manager with the DEF (Deflate) method. + $compressionMethodManager = new CompressionMethodManager([ + new Deflate(), + ]); + + $signatureAlgorithmManager = new AlgorithmManager([ + new HS256(), + new HS384(), + new HS512(), + ]); + + // We instantiate our JWE Decrypter. + $this->jweDecrypter = new JWEDecrypter( + $keyEncryptionAlgorithmManager, + $contentEncryptionAlgorithmManager, + $compressionMethodManager + ); + + // We try to load the token. + $this->encryptionSerializerManager = new \Jose\Component\Encryption\Serializer\JWESerializerManager([ + new \Jose\Component\Encryption\Serializer\CompactSerializer(), + ]); + + + // We instantiate our JWS Verifier. + $this->jwsVerifier = new JWSVerifier( + $signatureAlgorithmManager + ); + + // The serializer manager. We only use the JWE Compact Serialization Mode. + $this->serializerManager = new \Jose\Component\Signature\Serializer\JWSSerializerManager([ + new \Jose\Component\Signature\Serializer\CompactSerializer(), + ]); + } + + /** + * Implement JOSE decryption for SAM3 tokens + */ + public function decryptToken(string $rawToken, string $decryptKey) : JWS { + + // web-token library does not like underscores in headers, so replace them with - (which is valid in JWT) + $numSegments = substr_count($rawToken, '.') + 1; + $this->logger->debug('Bearer access token(segments=' . $numSegments . ')=' . $rawToken); + if ($numSegments > 3) { + // trusted authenticator and myself share the client secret, + // so use it is used for encrypted web tokens + $clientSecret = new JWK([ + 'kty' => 'oct', + 'k' => $decryptKey + ]); + + $jwe = $this->encryptionSerializerManager->unserialize($rawToken); + + // We decrypt the token. This method does NOT check the header. + if ($this->jweDecrypter->decryptUsingKey($jwe, $clientSecret, 0)) { + return $this->serializerManager->unserialize($jwe->getPayload()); + } else { + throw new InvalidTokenException('Unknown bearer encryption format'); + } + } else { + return $this->serializerManager->unserialize($rawToken); + } + } + + /** + * Get claims (even before verification to access e.g. aud standard field ...) + * Transform them in a format compatible with id_token representation. + */ + public function decode(JWS $decodedToken) : object { + $this->logger->debug('Telekom SAM3 access token: ' . $decodedToken->getPayload()); + $samContent = json_decode($decodedToken->getPayload(), false); + + // remap all the custom claims + // adapt into OpenId id_token format (as far as possible) + $claimArray = $samContent->{'urn:telekom.com:idm:at:attributes'}; + foreach ($claimArray as $claimKeyValue) { + $samContent->{'urn:telekom.com:' . $claimKeyValue->name} = $claimKeyValue->value; + } + unset($samContent->{'urn:telekom.com:idm:at:attributes'}); + + $this->logger->debug('Adapted OpenID-like token; ' . json_encode($samContent)); + return $samContent; + } + + + public function verifySignature(JWS $decodedToken, string $signKey) { + $accessSecret = new JWK([ + 'kty' => 'oct', + 'k' => $signKey + ]); // TODO: take the additional access key secret from settings + + if (!$this->jwsVerifier->verifyWithKey($decodedToken, $accessSecret, 0)) { + throw new SignatureException('Invalid Signature'); + } + } + + public function verifyClaims(object $claims, array $audiences = [], int $leeway = 60) { + $timestamp = $this->timeFactory->getTime(); + + // Check the nbf if it is defined. This is the time that the + // token can actually be used. If it's not yet that time, abort. + if (isset($claims->nbf) && $claims->nbf > ($timestamp + $leeway)) { + throw new InvalidTokenException( + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $claims->nbf) + ); + } + + // Check that this token has been created before 'now'. This prevents + // using tokens that have been created for later use (and haven't + // correctly used the nbf claim). + if (isset($claims->iat) && $claims->iat > ($timestamp + $leeway)) { + throw new InvalidTokenException( + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $claims->iat) + ); + } + + // Check if this token has expired. + if (isset($claims->exp) && ($timestamp - $leeway) >= $claims->exp) { + throw new InvalidTokenException('Expired token'); + } + + // Check target audience (if given) + // Check if this token has expired. + if (empty(array_intersect($claims->aud, $audiences))) { + throw new InvalidTokenException('No acceptable audience in token.'); + } + } +} diff --git a/lib/Migration/Version00008Date20211114183344.php b/lib/Migration/Version00008Date20211114183344.php new file mode 100644 index 00000000..1c9cf6ea --- /dev/null +++ b/lib/Migration/Version00008Date20211114183344.php @@ -0,0 +1,25 @@ +getTable('user_oidc_providers'); + $table->addColumn('bearer_secret', 'string', [ + 'notnull' => true, + 'length' => 64, + ]); + + return $schema; + } +} diff --git a/lib/Migration/Version010304Date20230902125945.php b/lib/Migration/Version010304Date20230902125945.php new file mode 100644 index 00000000..bbc04849 --- /dev/null +++ b/lib/Migration/Version010304Date20230902125945.php @@ -0,0 +1,97 @@ + + * + * @author B. Rederlechner + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\UserOIDC\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; +use OCP\Security\ICrypto; + +class Version010304Date20230902125945 extends SimpleMigrationStep { + + /** + * @var IDBConnection + */ + private $connection; + /** + * @var ICrypto + */ + private $crypto; + + public function __construct( + IDBConnection $connection, + ICrypto $crypto, + ) { + $this->connection = $connection; + $this->crypto = $crypto; + } + + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + $tableName = 'user_oidc_providers'; + + if ($schema->hasTable($tableName)) { + $table = $schema->getTable($tableName); + if ($table->hasColumn('bearer_secret')) { + $column = $table->getColumn('bearer_secret'); + $column->setLength(512); + return $schema; + } + } + + return null; + } + + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) { + $tableName = 'user_oidc_providers'; + + // update secrets in user_oidc_providers and user_oidc_id4me + $qbUpdate = $this->connection->getQueryBuilder(); + $qbUpdate->update($tableName) + ->set('bearer_secret', $qbUpdate->createParameter('updateSecret')) + ->where( + $qbUpdate->expr()->eq('id', $qbUpdate->createParameter('updateId')) + ); + + $qbSelect = $this->connection->getQueryBuilder(); + $qbSelect->select('id', 'bearer_secret') + ->from($tableName); + $req = $qbSelect->executeQuery(); + while ($row = $req->fetch()) { + $id = $row['id']; + $secret = $row['bearer_secret']; + $encryptedSecret = $this->crypto->encrypt($secret); + $qbUpdate->setParameter('updateSecret', $encryptedSecret, IQueryBuilder::PARAM_STR); + $qbUpdate->setParameter('updateId', $id, IQueryBuilder::PARAM_INT); + $qbUpdate->executeStatement(); + } + $req->closeCursor(); + } +} diff --git a/lib/Service/ProvisioningDeniedException.php b/lib/Service/ProvisioningDeniedException.php new file mode 100644 index 00000000..9f317170 --- /dev/null +++ b/lib/Service/ProvisioningDeniedException.php @@ -0,0 +1,69 @@ + + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\UserOIDC\Service; + +/** + * Exception if the precondition of the config update method isn't met + * @since 1.4.0 + */ +class ProvisioningDeniedException extends \Exception { + private $redirectUrl; + + /** + * Exception constructor including an option redirect url. + * + * @param string $message The error message. It will be not revealed to the + * the user (unless the hint is empty) and thus + * should be not translated. + * @param string $hint A useful message that is presented to the end + * user. It should be translated, but must not + * contain sensitive data. + * @param int $code Set default to 403 (Forbidden) + * @param \Exception|null $previous + */ + public function __construct(string $message, ?string $redirectUrl = null, int $code = 403, ?\Exception $previous = null) { + parent::__construct($message, $code, $previous); + $this->redirectUrl = $redirectUrl; + } + + /** + * Read optional failure redirect if available + * @return string|null + */ + public function getRedirectUrl(): ?string { + return $this->redirectUrl; + } + + /** + * Include redirect in string serialisation. + * + * @return string + */ + public function __toString(): string { + $redirect = $this->redirectUrl ?? ''; + return __CLASS__ . ": [{$this->code}]: {$this->message} ({$redirect})\n"; + } +} diff --git a/lib/Service/ProvisioningEventService.php b/lib/Service/ProvisioningEventService.php new file mode 100644 index 00000000..abcaf940 --- /dev/null +++ b/lib/Service/ProvisioningEventService.php @@ -0,0 +1,184 @@ + + * + * @author Bernd Rederlechner + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + +namespace OCA\UserOIDC\Service; + +use OCA\UserOIDC\Db\UserMapper; +use OCA\UserOIDC\Event\AttributeMappedEvent; +use OCA\UserOIDC\Event\UserAccountChangeEvent; +use OCA\UserOIDC\Exception\AttributeValueException; +use OCP\Accounts\IAccountManager; +use OCP\DB\Exception; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Http\Client\IClientService; +use OCP\IAvatarManager; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; + +// FIXME there should be an interface for both variations +class ProvisioningEventService extends ProvisioningService { + + /** @var IEventDispatcher */ + private $eventDispatcher; + + /** @var LoggerInterface */ + private $logger; + + /** @var IUserManager */ + private $userManager; + + /** @var ProviderService */ + private $providerService; + + public function __construct( + LocalIdService $idService, + ProviderService $providerService, + UserMapper $userMapper, + IUserManager $userManager, + IGroupManager $groupManager, + IEventDispatcher $eventDispatcher, + LoggerInterface $logger, + IAccountManager $accountManager, + IClientService $clientService, + IAvatarManager $avatarManager, + IConfig $config, + ISession $session, + IFactory $l10nFactory, + ) { + parent::__construct($idService, + $providerService, + $userMapper, + $userManager, + $groupManager, + $eventDispatcher, + $logger, + $accountManager, + $clientService, + $avatarManager, + $config, + $session, + $l10nFactory, + ); + $this->eventDispatcher = $eventDispatcher; + $this->logger = $logger; + $this->userManager = $userManager; + $this->providerService = $providerService; + } + + protected function mapDispatchUID(int $providerid, object $payload, string $tokenUserId) { + $uidAttribute = $this->providerService->getSetting($providerid, ProviderService::SETTING_MAPPING_UID, 'sub'); + $mappedUserId = $payload->{$uidAttribute} ?? $tokenUserId; + $event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_UID, $payload, $mappedUserId); + $this->eventDispatcher->dispatchTyped($event); + return $event->getValue(); + } + + protected function mapDispatchDisplayname(int $providerid, object $payload) { + $displaynameAttribute = $this->providerService->getSetting($providerid, ProviderService::SETTING_MAPPING_DISPLAYNAME, 'displayname'); + $mappedDisplayName = $payload->{$displaynameAttribute} ?? null; + + if (isset($mappedDisplayName)) { + $limitedDisplayName = mb_substr($mappedDisplayName, 0, 255); + $event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_DISPLAYNAME, $payload, $limitedDisplayName); + } else { + $event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_DISPLAYNAME, $payload); + } + $this->eventDispatcher->dispatchTyped($event); + return $event->getValue(); + } + + protected function mapDispatchEmail(int $providerid, object $payload) { + $emailAttribute = $this->providerService->getSetting($providerid, ProviderService::SETTING_MAPPING_EMAIL, 'email'); + $mappedEmail = $payload->{$emailAttribute} ?? null; + $event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_EMAIL, $payload, $mappedEmail); + $this->eventDispatcher->dispatchTyped($event); + return $event->getValue(); + } + + protected function mapDispatchQuota(int $providerid, object $payload) { + $quotaAttribute = $this->providerService->getSetting($providerid, ProviderService::SETTING_MAPPING_QUOTA, 'quota'); + $mappedQuota = $payload->{$quotaAttribute} ?? null; + $event = new AttributeMappedEvent(ProviderService::SETTING_MAPPING_QUOTA, $payload, $mappedQuota); + $this->eventDispatcher->dispatchTyped($event); + return $event->getValue(); + } + + protected function dispatchUserAccountUpdate(string $uid, ?string $displayName, ?string $email, ?string $quota, object $payload) { + $event = new UserAccountChangeEvent($uid, $displayName, $email, $quota, $payload); + $this->eventDispatcher->dispatchTyped($event); + return $event->getResult(); + } + + /** + * Trigger a provisioning via event system. + * This allows to flexibly implement complex provisioning strategies - + * even in a separate app. + * + * On error, the provisioning logic can deliver failure reasons and + * even a redirect to a different endpoint. + * + * @param string $tokenUserId + * @param int $providerId + * @param object $idTokenPayload + * @param IUser|null $existingLocalUser + * @return array{user: ?IUser, userData: array} + * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws ProvisioningDeniedException + */ + public function provisionUser(string $tokenUserId, int $providerId, object $idTokenPayload, ?IUser $existingLocalUser = null): array { + try { + $uid = $tokenUserId; + $displayname = $this->mapDispatchDisplayname($providerId, $idTokenPayload); + $email = $this->mapDispatchEmail($providerId, $idTokenPayload); + $quota = $this->mapDispatchQuota($providerId, $idTokenPayload); + } catch (AttributeValueException $eAttribute) { + $this->logger->info("{$tokenUserId}: user rejected by OpenId web authorization, reason: " . $eAttribute->getMessage()); + throw new ProvisioningDeniedException($eAttribute->getMessage()); + } + + $userReaction = $this->dispatchUserAccountUpdate($uid, $displayname, $email, $quota, $idTokenPayload); + + if ($userReaction->isAccessAllowed()) { + $this->logger->info("{$uid}: account accepted, reason: " . $userReaction->getReason()); + $user = $this->userManager->get($uid); + return [ + 'user' => $user, + 'userData' => get_object_vars($idTokenPayload), // optional, analog zu ProvisioningService + ]; + } else { + $this->logger->info("{$uid}: account rejected, reason: " . $userReaction->getReason()); + throw new ProvisioningDeniedException($userReaction->getReason(), $userReaction->getRedirectUrl()); + } + } +} diff --git a/lib/User/AbstractOidcBackend.php b/lib/User/AbstractOidcBackend.php new file mode 100644 index 00000000..d1e5de7b --- /dev/null +++ b/lib/User/AbstractOidcBackend.php @@ -0,0 +1,220 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\UserOIDC\User; + +use OCA\UserOIDC\Db\ProviderMapper; +use OCA\UserOIDC\Db\UserMapper; +use OCA\UserOIDC\Service\DiscoveryService; +use OCA\UserOIDC\Service\ProviderService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\Authentication\IApacheBackend; +use OCP\DB\Exception; +use OCP\EventDispatcher\GenericEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\NotPermittedException; +use OCP\IConfig; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\Backend\ABackend; +use OCP\User\Backend\ICustomLogout; +use OCP\User\Backend\IGetDisplayNameBackend; +use OCP\User\Backend\IPasswordConfirmationBackend; +use Psr\Log\LoggerInterface; + +/** + * Introduce a baseclass to derive multiple backend from depending on + * the required bearer behavior. + * + * The class contains the OIDC part without the bearer aspects. + * + * FIXME: we should derive also the previous standard bearer backend from + * this class + */ +abstract class AbstractOidcBackend extends ABackend implements IPasswordConfirmationBackend, IGetDisplayNameBackend, IApacheBackend, ICustomLogout { + + /** @var UserMapper */ + protected $userMapper; + /** @var LoggerInterface */ + protected $logger; + /** @var IRequest */ + protected $request; + /** @var ProviderMapper */ + protected $providerMapper; + /** + * @var ProviderService + */ + protected $providerService; + /** + * @var IConfig + */ + protected $config; + /** + * @var IEventDispatcher + */ + protected $eventDispatcher; + /** + * @var DiscoveryService + */ + protected $discoveryService; + /** + * @var IURLGenerator + */ + protected $urlGenerator; + /** + * @var ISession + */ + protected $session; + /** + * @var IUserManager + */ + protected $userManager; + + public function __construct(IConfig $config, + UserMapper $userMapper, + LoggerInterface $logger, + IRequest $request, + ISession $session, + IURLGenerator $urlGenerator, + IEventDispatcher $eventDispatcher, + DiscoveryService $discoveryService, + ProviderMapper $providerMapper, + ProviderService $providerService, + IUserManager $userManager) { + $this->config = $config; + $this->userMapper = $userMapper; + $this->logger = $logger; + $this->request = $request; + $this->providerMapper = $providerMapper; + $this->providerService = $providerService; + $this->eventDispatcher = $eventDispatcher; + $this->discoveryService = $discoveryService; + $this->session = $session; + $this->urlGenerator = $urlGenerator; + $this->userManager = $userManager; + } + + public function deleteUser($uid): bool { + try { + $user = $this->userMapper->getUser($uid); + $this->userMapper->delete($user); + return true; + } catch (Exception $e) { + $this->logger->error('Failed to delete user', [ 'exception' => $e ]); + return false; + } + } + + public function getUsers($search = '', $limit = null, $offset = null) { + return array_map(function ($user) { + return $user->getUserId(); + }, $this->userMapper->find($search, $limit, $offset)); + } + + public function userExists($uid): bool { + return $this->userMapper->userExists($uid); + } + + public function getDisplayName($uid): string { + try { + $user = $this->userMapper->getUser($uid); + } catch (DoesNotExistException $e) { + return $uid; + } + + return $user->getDisplayName(); + } + + public function getDisplayNames($search = '', $limit = null, $offset = null): array { + return $this->userMapper->findDisplayNames($search, $limit, $offset); + } + + public function hasUserListings(): bool { + return true; + } + + public function canConfirmPassword(string $uid): bool { + return false; + } + + /** + * As session cannot be injected in the constructor here, we inject it later + * + * @param ISession $session + * @return void + */ + public function injectSession(ISession $session): void { + $this->session = $session; + } + + /** + * {@inheritdoc} + */ + public function getLogoutUrl(): string { + return $this->urlGenerator->linkToRouteAbsolute( + 'user_oidc.login.singleLogoutService', + [ + 'requesttoken' => \OC::$server->getCsrfTokenManager()->getToken()->getEncryptedValue(), + ] + ); + } + + + /** + * Inspired by lib/private/User/Session.php::prepareUserLogin() + * + * @param string $userId + * @return bool + * @throws NotFoundException + */ + protected function checkFirstLogin(string $userId): bool { + $user = $this->userManager->get($userId); + + if ($user === null) { + return false; + } + + $firstLogin = $user->getLastLogin() === 0; + if ($firstLogin) { + \OC_Util::setupFS($userId); + // trigger creation of user home and /files folder + $userFolder = \OC::$server->getUserFolder($userId); + try { + // copy skeleton + \OC_Util::copySkeleton($userId, $userFolder); + } catch (NotPermittedException $ex) { + // read only uses + } + + // trigger any other initialization + \OC::$server->getEventDispatcher()->dispatch(IUser::class . '::firstLogin', new GenericEvent($user)); + } + $user->updateLastLoginTimestamp(); + return $firstLogin; + } +} diff --git a/src/components/SettingsForm.vue b/src/components/SettingsForm.vue index 4e79259b..31712128 100644 --- a/src/components/SettingsForm.vue +++ b/src/components/SettingsForm.vue @@ -32,6 +32,15 @@ :required="!update" autocomplete="off">

+

+ + +

{{ t('user_oidc', 'Warning, if the protocol of the URLs in the discovery content is HTTP, the ID token will be delivered through an insecure connection.') }} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 942fe846..511150ac 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -18,4 +18,9 @@ require_once __DIR__ . '/../../../tests/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php'; +\OC::$loader->addValidRoot(OC::$SERVERROOT . '/tests'); +\OC::$composerAutoloader->addPsr4('OCA\\UserOIDC\\BaseTest\\', dirname(__FILE__) . '/unit/MagentaCloud/', true); + Server::get(IAppManager::class)->loadApp('user_oidc'); + +OC_Hook::clear(); diff --git a/tests/unit/BaseTest/OpenidTokenTestCase.php b/tests/unit/BaseTest/OpenidTokenTestCase.php new file mode 100644 index 00000000..c0955093 --- /dev/null +++ b/tests/unit/BaseTest/OpenidTokenTestCase.php @@ -0,0 +1,141 @@ +realOidClaims; + } + + public function getOidClientId() { + return 'USER_NC_OPENID_TEST'; + } + + public function getOidNonce() { + return 'CVMI8I3JZPALSL5DIM6I1PDP8SDSEN4K'; + } + + public function getOidClientSecret() { + return \Base64Url\Base64Url::encode('JQ17C99A-DAF8-4E27-FBW4-GV23B043C993'); + } + + public function getOidServerKey() { + return \Base64Url\Base64Url::encode('JQ17DAF8-C99A-4E27-FBW4-GV23B043C993'); + } + + public function getOidPrivateServerKey() { + return [ + 'p' => '9US9kD6Q8nicR1se1U_iRI9x1iK0__HF7E9yhqrza9DHldC2h7PLuR7y9bITAUtcBmVvqEQlVUXRZPMrNUpLFI9hTdZXAACRqYBYGHg7Mvyzq-2JXhEE5CFDy9wSCPunc8bRq4TsY0ocSXugXKGjx-t1uO3fkF1UgNgNMjdzSPM', + 'kty' => 'RSA', + 'q' => '85auJF6W3c91EebGpjMX-g_U0fLBMgO2oxBsldus9x2diRd3wVvUnrTg5fQctODdr4if8dBCPDdLxBUKul4MXULC_nCkGkDjORdESb7j8amGnOvxnaVcQT6C5yHivAawa4R8NchR7n23VrQWO8fHhQBYUHTTy01G3A8D6dznCC8', + 'd' => 'tP-lT4FJBKrhhBUk7J1fR0638jVjn46yIfSaB5l_JlqNItmRJtbz3QWopy4oDfvrY_ccZIYG9tLvJH-3LHtuEddwxFsL-9MSUx5qxWB4sKpKA6EpxWNR5EFnFKxR_B2P2yFYiRDdbBh8h9pNaOuNjZU5iitAGvSOfW4X5hyJyu9t9zsEX9O6stEtP3yK5sx-bt7osGDMIguFBMcPVHbYw_Pl7-aNPuQ4ioxVXa3JlO6tTcDrcyMy7d3CWuGACj3juEnO-1n8E_OSR9sMp1k_L7i-qQ3OnLCOx07HeTWklCvNxz7U9qLcQXGcfpdWmhWZt6MO3SIXV4f6Md0U836v0Q', + 'e' => 'AQAB', + 'use' => 'sig', + 'kid' => '0123456789', + 'qi' => 'T3-NLCpVoITdS6PB9XYCsXsfhQSiE_4eTQnZf_Zya5hSd0xZDrrwNiXL8Dzy3YLjsZAFC0U6wAeC2wTBJ8c-6VxdP34J0sGj2I_TNhFFArksLy9ZaRbskCxKAPLipEFi8b1H2-aaRFRLs6BQJbfesQ5mcX2kB5AItAX3R6tcc0A', + 'dp' => 'ExUtFor3phXiOt8JEBmuBh2PAtUidgNuncs0ouusEshkrvBVM0u23wlcZ-dZ-TDO0SSVQmdC7FaJSyxsQTItk0TwkijKDhL9Qk3dDNJV8MqehBLwLCRw1_sKllLiCFbkGWrvp0OpTLRYbRM0T-C3qHdWanP_f_DzAS9OH4kW7Cc', + 'alg' => 'RS256', + 'dq' => 'xr3XAWeHkhw0uVFgHLQtSOJn0pBM3qC2_95jqfVc7xZjtTnHhKSHGqIbqKL-VPnvBcvkK-iuUfEPyUEdyqb3UZQqAm0nByCQA8Ge_shXtJGLejbroKMNXVJCfZBhLOYMRP0IVt1FM9-wmXY_ebDrcfGxHJvlPcekG-HIYKPSgBM', + 'n' => '6WCdDo8KuksEFaFlzvmsaoYhfOoMt5XgnX98dx-F1OUz53SG0lQlFt-xkwra5B4GZ-13lki0qCa2CjA1aLa9kEvDdYhz_0Uc5qOy5haDj8Jn547s6gFyaLzJ0RN5i5eKeDMHcjeEC0_NjiB2UNUFJJ61b2nXIlUvp_vBfKCv4A-8C3mLSbCKJQhX84QRDgt_Abz0MXj_ga72Ka2cwVLo4OFQAK5m57Qfu9ZvseMcgoinyhIQ18b98SkWinn3DM0W1KXLkWLk0S3XEMxLV1M7-9RLo4fgEGOpX1xmmM6KbsC5SxXvRUO7tjU-o35fcewDwXYHnRbxqhRkEFfWb7b8nQ' + ]; + } + + + public function getOidPublicServerKey() { + return \OCA\UserOIDC\Vendor\Firebase\JWT\JWK::parseKeySet([ 'keys' => [[ + 'kty' => 'RSA', + 'e' => 'AQAB', + 'use' => 'sig', + 'kid' => '0123456789', + 'alg' => 'RS256', + 'n' => '6WCdDo8KuksEFaFlzvmsaoYhfOoMt5XgnX98dx-F1OUz53SG0lQlFt-xkwra5B4GZ-13lki0qCa2CjA1aLa9kEvDdYhz_0Uc5qOy5haDj8Jn547s6gFyaLzJ0RN5i5eKeDMHcjeEC0_NjiB2UNUFJJ61b2nXIlUvp_vBfKCv4A-8C3mLSbCKJQhX84QRDgt_Abz0MXj_ga72Ka2cwVLo4OFQAK5m57Qfu9ZvseMcgoinyhIQ18b98SkWinn3DM0W1KXLkWLk0S3XEMxLV1M7-9RLo4fgEGOpX1xmmM6KbsC5SxXvRUO7tjU-o35fcewDwXYHnRbxqhRkEFfWb7b8nQ' + ]]]); + } + + public function getOidTestCode() { + return '66844608'; + } + + public function getOidTestState() { + return '4VSL5T274MJEMLZI1810HUFDA07CEPXZ'; + } + + public function setUp(): void { + parent::setUp(); + + $this->app = new App(Application::APP_ID); + $this->realOidClaims = [ + 'sub' => 'jgyros', + 'urn:custom.com:displayname' => 'Jonny G', + 'urn:custom.com:email' => 'jonny.gyros@x.y', + 'urn:custom.com:mainEmail' => 'jonny.gyuris@x.y.de', + 'iss' => "https:\/\/accounts.login00.custom.de", + 'urn:custom.com:feat1' => '0', + 'urn:custom.com:uid' => '081500000001234', + 'urn:custom.com:feat2' => '1', + 'urn:custom.com:ext2' => '0', + 'urn:custom.com:feat3' => '1', + 'acr' => 'urn:custom:names:idm:THO:1.0:ac:classes:passid:00', + 'urn:custom.com:feat4' => '0', + 'urn:custom.com:ext4' => '0', + 'auth_time' => time(), + 'exp' => time() + 7200, + 'iat' => time(), + 'urn:custom.com:session_token' => 'ad0fff71-e013-11ec-9e17-39677d2c891c', + 'nonce' => 'CVMI8I3JZPALSL5DIM6I1PDP8SDSEN4K', + 'aud' => ['USER_NC_OPENID_TEST'] ]; + } + + protected function createSignToken(array $claims) : string { + // The algorithm manager with the HS256 algorithm. + $algorithmManager = new AlgorithmManager([ + new RS256(), + ]); + + // use a different key for an invalid signature + $jwk = new JWK($this->getOidPrivateServerKey()); + $jwsBuilder = new JWSBuilder($algorithmManager); + $jws = $jwsBuilder->create() // We want to create a new JWS + ->withPayload(json_encode($claims)) // We set the payload + ->addSignature($jwk, ['alg' => 'RS256', 'kid' => '0123456789']) // We add a signature with a simple protected header + ->build(); + + $serializer = new CompactSerializer(); + return $serializer->serialize($jws, 0); + } +} diff --git a/tests/unit/MagentaCloud/BearerSettingsTest.php b/tests/unit/MagentaCloud/BearerSettingsTest.php new file mode 100644 index 00000000..eb142675 --- /dev/null +++ b/tests/unit/MagentaCloud/BearerSettingsTest.php @@ -0,0 +1,420 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + +use OCA\UserOIDC\AppInfo\Application; +use OCA\UserOIDC\Command\UpsertProvider; + +use OCA\UserOIDC\Db\Provider; + +use OCA\UserOIDC\Db\ProviderMapper; +use OCA\UserOIDC\Service\ProviderService; +use OCP\IConfig; + +use OCP\IRequest; + +use OCP\Security\ICrypto; +use PHPUnit\Framework\TestCase; + + +use Symfony\Component\Console\Tester\CommandTester; + +class BearerSettingsTest extends TestCase { + /** + * @var ProviderService + */ + private $provider; + + /** + * @var IConfig; + */ + private $config; + + public function setUp(): void { + parent::setUp(); + + $app = new \OCP\AppFramework\App(Application::APP_ID); + $this->requestMock = $this->createMock(IRequest::class); + + $this->config = $this->createMock(IConfig::class); + $this->providerMapper = $this->createMock(ProviderMapper::class); + $providers = [ + new \OCA\UserOIDC\Db\Provider(), + ]; + $providers[0]->setId(1); + $providers[0]->setIdentifier('Fraesbook'); + + $this->providerMapper->expects(self::any()) + ->method('getProviders') + ->willReturn($providers); + + $this->providerService = $this->getMockBuilder(ProviderService::class) + ->setConstructorArgs([ $this->config, $this->providerMapper]) + ->onlyMethods(['getProviderByIdentifier']) + ->getMock(); + $this->crypto = $app->getContainer()->get(ICrypto::class); + } + + protected function mockCreateUpdate( + string $providername, + ?string $clientid, + ?string $clientsecret, + ?string $discovery, + string $scope, + ?string $bearersecret, + array $options, + int $id = 2, + ) { + $provider = $this->getMockBuilder(Provider::class) + ->addMethods(['getIdentifier', 'getId']) + ->getMock(); + $provider->expects($this->any()) + ->method('getIdentifier') + ->willReturn($providername); + $provider->expects($this->any()) + ->method('getId') + ->willReturn($id); + + $this->providerMapper->expects($this->once()) + ->method('createOrUpdateProvider') + ->with( + $this->equalTo($providername), + $this->equalTo($clientid), + $this->anything(), + $this->equalTo($discovery), + $this->equalTo($scope), + $this->anything() + ) + ->willReturnCallback(function ($id, $clientid, $secret, $discovery, $scope, $bsecret) use ($clientsecret, $bearersecret, $provider) { + if ($secret !== null) { + $this->assertEquals($clientsecret, $this->crypto->decrypt($secret)); + } else { + $this->assertNull($secret); + } + if ($bsecret !== null) { + $this->assertEquals($bearersecret, \Base64Url\Base64Url::decode($this->crypto->decrypt($bsecret))); + } else { + $this->assertNull($bsecret); + } + return $provider; + }); + + + $this->config->expects($this->any()) + ->method('setAppValue') + ->with($this->equalTo(Application::APP_ID), $this->anything(), $this->anything()) + ->willReturnCallback(function ($appid, $key, $value) use ($options) { + if (array_key_exists($key, $options)) { + $this->assertEquals($options[$key], $value); + } + return ''; + }); + } + + + public function testCommandAddProvider() { + $this->providerService->expects($this->once()) + ->method('getProviderByIdentifier') + ->with($this->equalTo('Telekom')) + ->willReturn(null); + + $this->mockCreateUpdate('Telekom', + '10TVL0SAM30000004901NEXTMAGENTACLOUDTEST', + 'clientsecret***', + 'https://accounts.login00.idm.ver.sul.t-online.de/.well-known/openid-configuration', + 'openid email profile', + 'bearersecret***', + [ + 'provider-2-' . ProviderService::SETTING_UNIQUE_UID => '0', + 'provider-2-' . ProviderService::SETTING_MAPPING_DISPLAYNAME => 'urn:telekom.com:displayname', + 'provider-2-' . ProviderService::SETTING_MAPPING_EMAIL => 'urn:telekom.com:mainEmail', + 'provider-2-' . ProviderService::SETTING_MAPPING_QUOTA => 'quota', + 'provider-2-' . ProviderService::SETTING_MAPPING_UID => 'sub' + ]); + + $command = new UpsertProvider($this->providerService, $this->providerMapper, $this->crypto); + $commandTester = new CommandTester($command); + + $commandTester->execute([ + 'identifier' => 'Telekom', + '--clientid' => '10TVL0SAM30000004901NEXTMAGENTACLOUDTEST', + '--clientsecret' => 'clientsecret***', + '--bearersecret' => 'bearersecret***', + '--discoveryuri' => 'https://accounts.login00.idm.ver.sul.t-online.de/.well-known/openid-configuration', + '--scope' => 'openid email profile', + '--unique-uid' => '0', + '--mapping-display-name' => 'urn:telekom.com:displayname', + '--mapping-email' => 'urn:telekom.com:mainEmail', + '--mapping-quota' => 'quota', + '--mapping-uid' => 'sub', + ]); + + + //$output = $commandTester->getOutput(); + //$this->assertContains('done', $output); + } + + protected function mockProvider(string $providername, + string $clientid, + string $clientsecret, + string $discovery, + string $scope, + string $bearersecret, + int $id = 2) : Provider { + $provider = $this->getMockBuilder(Provider::class) + ->addMethods(['getIdentifier', 'getClientId', 'getClientSecret', 'getBearerSecret', 'getDiscoveryEndpoint']) + ->setMethods(['getScope', 'getId']) + ->getMock(); + $provider->expects($this->any()) + ->method('getIdentifier') + ->willReturn($providername); + $provider->expects($this->any()) + ->method('getId') + ->willReturn(2); + $provider->expects($this->any()) + ->method('getClientId') + ->willReturn($clientid); + $provider->expects($this->any()) + ->method('getClientSecret') + ->willReturn($clientsecret); + $provider->expects($this->any()) + ->method('getBearerSecret') + ->willReturn(\Base64Url\Base64Url::encode($bearersecret)); + $provider->expects($this->any()) + ->method('getDiscoveryEndpoint') + ->willReturn($discovery); + $provider->expects($this->any()) + ->method('getScope') + ->willReturn($scope); + + return $provider; + } + + public function testCommandUpdateFull() { + $provider = $this->getMockBuilder(Provider::class) + ->addMethods(['getIdentifier', 'getClientId', 'getClientSecret', 'getBearerSecret', 'getDiscoveryEndpoint']) + ->setMethods(['getScope']) + ->getMock(); + $provider->expects($this->any()) + ->method('getIdentifier') + ->willReturn('Telekom'); + $provider->expects($this->never())->method('getClientId'); + $provider->expects($this->never())->method('getClientSecret'); + $provider->expects($this->never())->method('getBearerSecret'); + $provider->expects($this->never())->method('getDiscoveryEndpoint'); + $provider->expects($this->never())->method('getScope'); + + $this->providerService->expects($this->once()) + ->method('getProviderByIdentifier') + ->with($this->equalTo('Telekom')) + ->willReturn(null); + $this->mockCreateUpdate('Telekom', + '10TVL0SAM30000004902NEXTMAGENTACLOUDTEST', + 'client*secret***', + 'https://accounts.login00.idm.ver.sul.t-online.de/.well-unknown/openid-configuration', + 'openid profile', + 'bearer*secret***', + [ + 'provider-2-' . ProviderService::SETTING_UNIQUE_UID => '1', + 'provider-2-' . ProviderService::SETTING_MAPPING_DISPLAYNAME => 'urn:telekom.com:displaykrame', + 'provider-2-' . ProviderService::SETTING_MAPPING_EMAIL => 'urn:telekom.com:mainDemail', + 'provider-2-' . ProviderService::SETTING_MAPPING_QUOTA => 'quotas', + 'provider-2-' . ProviderService::SETTING_MAPPING_UID => 'flop' + ]); + + $command = new UpsertProvider($this->providerService, $this->providerMapper, $this->crypto); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'identifier' => 'Telekom', + '--clientid' => '10TVL0SAM30000004902NEXTMAGENTACLOUDTEST', + '--clientsecret' => 'client*secret***', + '--bearersecret' => 'bearer*secret***', + '--discoveryuri' => 'https://accounts.login00.idm.ver.sul.t-online.de/.well-unknown/openid-configuration', + '--scope' => 'openid profile', + '--mapping-display-name' => 'urn:telekom.com:displaykrame', + '--mapping-email' => 'urn:telekom.com:mainDemail', + '--mapping-quota' => 'quotas', + '--mapping-uid' => 'flop', + '--unique-uid' => '1' + ]); + } + + public function testCommandUpdateSingleClientId() { + $provider = $this->mockProvider('Telekom', '10TVL0SAM30000004901NEXTMAGENTACLOUDTEST', 'clientsecret***', + 'https://accounts.login00.idm.ver.sul.t-online.de/.well-known/openid-configuration', + 'openid email profile', 'bearersecret***'); + $this->providerService->expects($this->once()) + ->method('getProviderByIdentifier') + ->with($this->equalTo('Telekom')) + ->willReturn($provider); + $this->mockCreateUpdate( + 'Telekom', + '10TVL0SAM30000004903NEXTMAGENTACLOUDTEST', + null, + null, + 'openid email profile', + null, + []); + + $command = new UpsertProvider($this->providerService, $this->providerMapper, $this->crypto); + $commandTester = new CommandTester($command); + + $commandTester->execute([ + 'identifier' => 'Telekom', + '--clientid' => '10TVL0SAM30000004903NEXTMAGENTACLOUDTEST', + ]); + } + + + public function testCommandUpdateSingleClientSecret() { + $provider = $this->mockProvider('Telekom', '10TVL0SAM30000004901NEXTMAGENTACLOUDTEST', 'clientsecret***', + 'https://accounts.login00.idm.ver.sul.t-online.de/.well-known/openid-configuration', + 'openid email profile', 'bearersecret***'); + $this->providerService->expects($this->once()) + ->method('getProviderByIdentifier') + ->with($this->equalTo('Telekom')) + ->willReturn($provider); + $this->mockCreateUpdate( + 'Telekom', + null, + '***clientsecret***', + null, + 'openid email profile', + null, + []); + + $command = new UpsertProvider($this->providerService, $this->providerMapper, $this->crypto); + $commandTester = new CommandTester($command); + + $commandTester->execute([ + 'identifier' => 'Telekom', + '--clientsecret' => '***clientsecret***', + ]); + } + + public function testCommandUpdateSingleBearerSecret() { + $provider = $this->mockProvider('Telekom', '10TVL0SAM30000004901NEXTMAGENTACLOUDTEST', 'clientsecret***', + 'https://accounts.login00.idm.ver.sul.t-online.de/.well-known/openid-configuration', + 'openid email profile', 'bearersecret***'); + $this->providerService->expects($this->once()) + ->method('getProviderByIdentifier') + ->with($this->equalTo('Telekom')) + ->willReturn($provider); + $this->mockCreateUpdate( + 'Telekom', + null, + null, + null, + 'openid email profile', + '***bearersecret***', + []); + + + $command = new UpsertProvider($this->providerService, $this->providerMapper, $this->crypto); + $commandTester = new CommandTester($command); + + $commandTester->execute([ + 'identifier' => 'Telekom', + '--bearersecret' => '***bearersecret***', + ]); + } + + public function testCommandUpdateSingleDiscoveryEndpoint() { + $provider = $this->mockProvider('Telekom', '10TVL0SAM30000004901NEXTMAGENTACLOUDTEST', 'clientsecret***', + 'https://accounts.login00.idm.ver.sul.t-online.de/.well-known/openid-configuration', + 'openid email profile', 'bearersecret***'); + $this->providerService->expects($this->once()) + ->method('getProviderByIdentifier') + ->with($this->equalTo('Telekom')) + ->willReturn($provider); + $this->mockCreateUpdate( + 'Telekom', + null, + null, + 'https://accounts.login00.idm.ver.sul.t-online.de/.well-unknown/openid-configuration', + 'openid email profile', + null, []); + + $command = new UpsertProvider($this->providerService, $this->providerMapper, $this->crypto); + $commandTester = new CommandTester($command); + + $commandTester->execute([ + 'identifier' => 'Telekom', + '--discoveryuri' => 'https://accounts.login00.idm.ver.sul.t-online.de/.well-unknown/openid-configuration', + ]); + } + + public function testCommandUpdateSingleScope() { + $provider = $this->mockProvider('Telekom', '10TVL0SAM30000004901NEXTMAGENTACLOUDTEST', 'clientsecret***', + 'https://accounts.login00.idm.ver.sul.t-online.de/.well-known/openid-configuration', + 'openid email profile', 'bearersecret***'); + $this->providerService->expects($this->once()) + ->method('getProviderByIdentifier') + ->with($this->equalTo('Telekom')) + ->willReturn($provider); + $this->mockCreateUpdate( + 'Telekom', + null, + null, + null, + 'openid profile', + '***bearersecret***', + []); + + + $command = new UpsertProvider($this->providerService, $this->providerMapper, $this->crypto); + $commandTester = new CommandTester($command); + + $commandTester->execute([ + 'identifier' => 'Telekom', + '--scope' => 'openid profile', + ]); + } + + public function testCommandUpdateSingleUniqueUid() { + $provider = $this->mockProvider('Telekom', '10TVL0SAM30000004901NEXTMAGENTACLOUDTEST', 'clientsecret***', + 'https://accounts.login00.idm.ver.sul.t-online.de/.well-known/openid-configuration', + 'openid email profile', 'bearersecret***'); + $this->providerService->expects($this->once()) + ->method('getProviderByIdentifier') + ->with($this->equalTo('Telekom')) + ->willReturn($provider); + $this->mockCreateUpdate( + 'Telekom', + null, + null, + null, + 'openid email profile', + null, + ['provider-2-' . ProviderService::SETTING_UNIQUE_UID => '1']); + + $command = new UpsertProvider($this->providerService, $this->providerMapper, $this->crypto); + $commandTester = new CommandTester($command); + + $commandTester->execute([ + 'identifier' => 'Telekom', + '--unique-uid' => '1', + ]); + } +} diff --git a/tests/unit/MagentaCloud/BearerTokenServiceTest.php b/tests/unit/MagentaCloud/BearerTokenServiceTest.php new file mode 100644 index 00000000..2c6ab49c --- /dev/null +++ b/tests/unit/MagentaCloud/BearerTokenServiceTest.php @@ -0,0 +1,103 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + + +use OCA\UserOIDC\MagentaBearer\SignatureException; +use OCA\UserOIDC\MagentaBearer\TokenService; + +use PHPUnit\Framework\TestCase; + +class BearerTokenServiceTest extends TestCase { + public const EXPIRED_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdHMwMC5pZG0udmVyLnN1bC50LW9ubGluZS5kZSIsInVybjp0ZWxla29tLmNvbTppZG06YXQ6c3ViamVjdFR5cGUiOnsiZm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6bmFtZWlkLWZvcm1hdDphbmlkIiwicmVhbG0iOiJ2ZXIuc3VsLnQtb25saW5lLmRlIn0sImFjciI6InVybjp0ZWxla29tOm5hbWVzOmlkbTpUSE86MS4wOmFjOmNsYXNzZXM6cHdkIiwic3ViIjoiMTIwMDQ5MDEwMDAwMDAwMDA3MjEwMjA3IiwiaWF0IjoxNjM1NTgxODAyLCJuYmYiOjE2MzU1ODE4MDIsImV4cCI6MTYzNTU4OTAwMiwidXJuOnRlbGVrb20uY29tOmlkbTphdDphdXRoTlN0YXRlbWVudHMiOnsidXJuOnRlbGVrb206bmFtZXM6aWRtOlRITzoxLjA6YWM6Y2xhc3Nlczpwd2QiOnsiYXV0aGVudGljYXRpbmdBdXRob3JpdHkiOm51bGwsImF1dGhOSW5zdGFudCI6MTYzNTU4MTUzNX19LCJhdWQiOlsiaHR0cDovL2F1dGgubWFnZW50YWNsb3VkLmRlIl0sImp0aSI6IlNUUy0xZTIyYTA2Zi03OTBjLTQwZmItYWQxZC02ZGUyZGRjZjI0MzEiLCJ1cm46dGVsZWtvbS5jb206aWRtOmF0OmF0dHJpYnV0ZXMiOlt7Im5hbWUiOiJjbGllbnRfaWQiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIxMFRWTDBTQU0zMDAwMDAwNDkwMU5FWFRNQUdFTlRBQ0xPVUQwMDAwIn0seyJuYW1lIjoiZGlzcGxheW5hbWUiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiJubWNsb3VkMDFAdmVyLnN1bC50LW9ubGluZS5kZSJ9LHsibmFtZSI6ImVtYWlsIiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoibm1jbG91ZDAxQHZlci5zdWwudC1vbmxpbmUuZGUifSx7Im5hbWUiOiJhbmlkIiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoiMTIwMDQ5MDEwMDAwMDAwMDA3MjEwMjA3In0seyJuYW1lIjoiZDU1NiIsIm5hbWVGb3JtYXQiOiJ1cm46Y29tOnRlbGVrb206aWRtOjEuMDphdHRybmFtZS1mb3JtYXQ6ZmllbGQiLCJ2YWx1ZSI6IjAifSx7Im5hbWUiOiJkb210IiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoidmVyLnN1bC50LW9ubGluZS5kZSJ9LHsibmFtZSI6ImYwNDgiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIxIn0seyJuYW1lIjoiZjA0OSIsIm5hbWVGb3JtYXQiOiJ1cm46Y29tOnRlbGVrb206aWRtOjEuMDphdHRybmFtZS1mb3JtYXQ6ZmllbGQiLCJ2YWx1ZSI6IjEifSx7Im5hbWUiOiJmMDUxIiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoiMCJ9LHsibmFtZSI6ImY0NjAiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIwIn0seyJuYW1lIjoiZjQ2NyIsIm5hbWVGb3JtYXQiOiJ1cm46Y29tOnRlbGVrb206aWRtOjEuMDphdHRybmFtZS1mb3JtYXQ6ZmllbGQiLCJ2YWx1ZSI6IjAifSx7Im5hbWUiOiJmNDY4IiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoiMCJ9LHsibmFtZSI6ImY0NjkiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIwIn0seyJuYW1lIjoiZjQ3MSIsIm5hbWVGb3JtYXQiOiJ1cm46Y29tOnRlbGVrb206aWRtOjEuMDphdHRybmFtZS1mb3JtYXQ6ZmllbGQiLCJ2YWx1ZSI6IjAifSx7Im5hbWUiOiJmNTU2IiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoiMSJ9LHsibmFtZSI6ImY3MzQiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIwIn0seyJuYW1lIjoibWFpbkVtYWlsIiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoibm1jbG91ZDAxQHZlci5zdWwudC1vbmxpbmUuZGUifSx7Im5hbWUiOiJzNTU2IiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoiMCJ9LHsibmFtZSI6InVzdGEiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIxIn1dLCJ1cm46dGVsZWtvbS5jb206aWRtOmF0OnZlcnNpb24iOiIxLjAifQ.5zbr7Uvx2KmU8uR412jHhptWEjykJ_n2awBRcQL8fLE'; + public const INVALID_SIGN_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdHMwMC5pZG0udmVyLnN1bC50LW9ubGluZS5kZSIsInVybjp0ZWxla29tLmNvbTppZG06YXQ6c3ViamVjdFR5cGUiOnsiZm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6bmFtZWlkLWZvcm1hdDphbmlkIiwicmVhbG0iOiJ2ZXIuc3VsLnQtb25saW5lLmRlIn0sImFjciI6InVybjp0ZWxla29tOm5hbWVzOmlkbTpUSE86MS4wOmFjOmNsYXNzZXM6cHdkIiwic3ViIjoiMTIwMDQ5MDEwMDAwMDAwMDA3MjEwMjA3IiwiaWF0IjoxNjM1NTgxODAyLCJuYmYiOjE2MzU1ODE4MDIsImV4cCI6MTYzNTU4OTAwMiwidXJuOnRlbGVrb20uY29tOmlkbTphdDphdXRoTlN0YXRlbWVudHMiOnsidXJuOnRlbGVrb206bmFtZXM6aWRtOlRITzoxLjA6YWM6Y2xhc3Nlczpwd2QiOnsiYXV0aGVudGljYXRpbmdBdXRob3JpdHkiOm51bGwsImF1dGhOSW5zdGFudCI6MTYzNTU4MTUzNX19LCJhdWQiOlsiaHR0cDovL2F1dGgubWFnZW50YWNsb3VkLmRlIl0sImp0aSI6IlNUUy0xZTIyYTA2Zi03OTBjLTQwZmItYWQxZC02ZGUyZGRjZjI0MzEiLCJ1cm46dGVsZWtvbS5jb206aWRtOmF0OmF0dHJpYnV0ZXMiOlt7Im5hbWUiOiJjbGllbnRfaWQiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIxMFRWTDBTQU0zMDAwMDAwNDkwMU5FWFRNQUdFTlRBQ0xPVUQwMDAwIn0seyJuYW1lIjoiZGlzcGxheW5hbWUiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiJubWNsb3VkMDFAdmVyLnN1bC50LW9ubGluZS5kZSJ9LHsibmFtZSI6ImVtYWlsIiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoibm1jbG91ZDAxQHZlci5zdWwudC1vbmxpbmUuZGUifSx7Im5hbWUiOiJhbmlkIiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoiMTIwMDQ5MDEwMDAwMDAwMDA3MjEwMjA3In0seyJuYW1lIjoiZDU1NiIsIm5hbWVGb3JtYXQiOiJ1cm46Y29tOnRlbGVrb206aWRtOjEuMDphdHRybmFtZS1mb3JtYXQ6ZmllbGQiLCJ2YWx1ZSI6IjAifSx7Im5hbWUiOiJkb210IiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoidmVyLnN1bC50LW9ubGluZS5kZSJ9LHsibmFtZSI6ImYwNDgiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIxIn0seyJuYW1lIjoiZjA0OSIsIm5hbWVGb3JtYXQiOiJ1cm46Y29tOnRlbGVrb206aWRtOjEuMDphdHRybmFtZS1mb3JtYXQ6ZmllbGQiLCJ2YWx1ZSI6IjEifSx7Im5hbWUiOiJmMDUxIiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoiMCJ9LHsibmFtZSI6ImY0NjAiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIwIn0seyJuYW1lIjoiZjQ2NyIsIm5hbWVGb3JtYXQiOiJ1cm46Y29tOnRlbGVrb206aWRtOjEuMDphdHRybmFtZS1mb3JtYXQ6ZmllbGQiLCJ2YWx1ZSI6IjAifSx7Im5hbWUiOiJmNDY4IiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoiMCJ9LHsibmFtZSI6ImY0NjkiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIwIn0seyJuYW1lIjoiZjQ3MSIsIm5hbWVGb3JtYXQiOiJ1cm46Y29tOnRlbGVrb206aWRtOjEuMDphdHRybmFtZS1mb3JtYXQ6ZmllbGQiLCJ2YWx1ZSI6IjAifSx7Im5hbWUiOiJmNTU2IiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoiMSJ9LHsibmFtZSI6ImY3MzQiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIwIn0seyJuYW1lIjoibWFpbkVtYWlsIiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoibm1jbG91ZDAxQHZlci5zdWwudC1vbmxpbmUuZGUifSx7Im5hbWUiOiJzNTU2IiwibmFtZUZvcm1hdCI6InVybjpjb206dGVsZWtvbTppZG06MS4wOmF0dHJuYW1lLWZvcm1hdDpmaWVsZCIsInZhbHVlIjoiMCJ9LHsibmFtZSI6InVzdGEiLCJuYW1lRm9ybWF0IjoidXJuOmNvbTp0ZWxla29tOmlkbToxLjA6YXR0cm5hbWUtZm9ybWF0OmZpZWxkIiwidmFsdWUiOiIxIn1dLCJ1cm46dGVsZWtvbS5jb206aWRtOmF0OnZlcnNpb24iOiIxLjAifQ.5zbr7Uvx2KmU8uR412jHhptWEjykJ_n2awBRcQL9fLE'; + public const ENCRPYT1_SIGN_TOKEN = 'eyJwMnMiOiI4VzhYY21iaHJPSSIsInAyYyI6MTAwMCwiY3R5IjoiSldUIiwiZW5jIjoiQTI1NkNCQy1IUzUxMiIsImFsZyI6IlBCRVMyLUhTNTEyK0EyNTZLVyJ9.5bA_ctLbQOnMojJW3MPo83AIvCAu3MpmaaD7j2GzqBv5_-D4w69ONqcPEsc6LYMG9B-rw3HDXng4Mqye4KqpW70ECpf9HXV6.6zl4Zqp4wbcO_AqqmpA3sQ.y7dHcwxXveYkuh4UaqHhE4nvP_avZsxaf7aAbnJdDHHKbBKvEKKqHkPg593i14ypWuRHd2i9Opsuyppfxx9Hw7C7N7LJ8UCTYMihHqlJkHecB08xgJ3ciE0L2Qtvg9hfxQbHNVV4p1_KL3ubAXt9ovwDCOJvN6PXyixUDtYYF1D_Km7Ze1ptUNbwS2H4vf-MKHwwrm5uhTvXOppGNO-0tYnIMOZ8BkiTtrrlO6IQbRcC4EMw74PzbFsQXY9u1xsNZ9IOrzbBl_EyPBLr5ool1BGlvNog4XFsHLgxUa5cjIcZVRMgZSLWdToTiXYFAWdO6fbQrRWT8ERRDWjiDxJEaPlfI_61G5NzJN2NKnSAY7fR8i3Rfs_JoF1TtpR5dGU28Lk1vcLjKYBLqp2hjW97QsANVgmalkkJMUpiAvNN48ZSCK9T3vTfiH7unFRNWvTKvZXyHIkYQPZ0-b3Z9s5oLMx93Snvcq9jQVKA1dWU_bEUIOnwP65ADU_FIkYB8gsZXp5Za3HrK63u03Lij6rwkJpEPbwcnxhBkMhtKOOwQVZm1ZBf_lVyn39MFXmLN_gDD052vFpxl1NnG0KEg8XJQ_usE9e64q7W6IG4gRm9NYG6rdeik6Dm45K8fA4oUiyjdgHjveR6GW8uXQR-tWXf3IC-_2jws2PJ31acdoEbDU30XlVeCqENW-ylPJ10rP28XxboQVJMRrzMiEzu39IH3c02czHh81U09TREVsO2S8CCQcahboaplDg9kpr1UZpsRrjg40bEtdm2cKubTbczGiXiF7sI0qE-kHm0aiK5c6mO8fHETMCmvh2vhxcYo_T6q7VklbwiZVbn47z-oriEDyPlLrB_PzYR6fNRbtObttj0CHRgf-NI69RU2pAGxujSi2lEhNkG-CAFNfASKm8uSUCg8UPr7v38c5vr4IuYC1gYjxgebXIh0EFX4G8jZM6ljPSzmMFDyErWJQ5OrtJjuKrUa96Yp3oOZTemtCwc--mrDXmpwVlaBMCuuJDz6zucxwSeVK0mP0t56zHeK59jxz0OfV62TrcVeZaLqSl3o-pVsY5KrLxL1qf2QIry-uy_c1zi9AuZnSbH3t1RvmyG5-QIh5WSPOLXG9ivuHKAdQTvBnchXWfkUVkoPYuPFyBydlPAhpRQyBLHboqdT6lIdoQ5lBRI8vsGb9wQVSQx08hbpEFOPMe-SJqzjZp36sUurJrgj_ethbIWkTSe_HPkcvBv8X0kyvhnyTKYJoroE5HDM0dtgFW8xK8NmOZOuREzJW5fpqzJML8iY0p1IX3bvGrCeVMEJtM0T6KSJFdPHBAzkWNNMBUc2jhuxa6B2cSaMz60bwSCw8n5NWz8wkXUFJJkHKEnK8tFbtOQXHeGG48k7Wl6kgrQkAFAHZqQt9gRDdmGcYAYHVK7cESjABV9LWQIQYy0eyveU0sWE5sYXKCwsk8rLiKt5GmZlRQ0rOltuFXRTu_EZYuqR0DCRXrjQWVN1zLTy0LMqAvDR-PJcFtekbT9CXLEW6M6GHzJhYfNyMc_cPitG8QwS5EWGzJjQIiNsJBRyV7cPlHeMhKzDtEk3DR3l-qQJa9-54RQB-kStJjB0AAZ21ku7eBS6orT0lljj935eghlHxAzyr1fvlDjIpHc--ob_7DOPc9sBGqcwdYoZ28zD1d02rpJujOwTe4zgll4vffJ_aFP8hm19pmroCwFsZPWIK6GN_cllJaxnllkJ_9c-7eBj1rKkNX0DLyNwKoMYttugeQFWAxaaqWhoOpQXnRHaVt5hTzoexi5C2j_aVBUAzyMPZtvuYgY1uc8zeKt5X8rAy3Y7WqYeOy8Q6IezVyTE6p0kzYgzUT1Vg2XZEr7dBgNkv8ySfYQNG5d8_PtvBHX-SOy25rtes7oUHHgZx0AkpomhNGSwfrW4dyIWCa6j5qUexqs3TPip_FAJwdW38OnyfPQ5SHLTt8D6OCOLN70MdbPpeoFkGnx1oj1Xjx_UW8mtueWAkxidv6Lamf_D5j8sJvkksne8Nos2YvGNkaGZwQK8YfjvPP-VVdukLMqoloovOuvgxLVLSvnDYcRRjfwAdiKwFNGdMbdV5LwfAzVAlncyWPJso3Lk9fPYd88YW8e6o7xiboiushcbDQU0ZN_Zh9YGk-8R4VnvAuI3yWxLrBB8NFUwKYkNBupVWrxRHJbJEebsLv9r_PZstBHHfMFpcQYX05NYfQiezhQ9l-aseC9Ay4FLbcxyXkIiPEBfiwZESqQbYoL3OeBQYzsV8AFe4GVdUUwPCuPjKR52UlkPiUJthxGkLFfcEPbqfX_lByN5YZRMSruOt6yKysbBIw0gcC6n7wuA_URaFNSPfyHe6nqAtveh1YjZpwZszAERyk2ziFXKFYFppdjMPvxF37uWoH_BEpv9Bs7yaxPRK7pfniS105RBsDFS093-3sUYM6W7IrmPfKAe71OtdWtQQqQKOAX3WGFShCIKyz-aOJWJPRG35Q2DOGu0nehFetGVsSnt-ehmru-Zuv4IanlF0_3SjQ7l7l6gg3Sfyy6sN8SVvxTtw4jLkaAM6cpmVMQVP8uQeJ9IFSHyq1kFceQcguh5tbwMknJzcMNzmZ9zEOG4ifyk9zmeulX9Rtf3lIXIOU-1lEs5bVm42eg1IKpxaY8PeTrT4qvPIyVkOprpKGIAcGyD0tP11vvDCvbltEWBo72gdbtD9tUdUPK0XRD_TgEPy2YU6I6BsKBStd40Fk6nOCGrq-mjYmH6OK3JUF3EVV7E0fEg7BgnYPLxcla0l7H6LpY4sqmFwapDqknjhgbqK0dyZDGWEPJ7Ph_5K6BazKuV_1bf6ZFOuRbm72cmT6vAJM8BhihAdTQt92QbTPikjLS2he5AfSV1ieDgLT26dsLNuLkyExyBqUGkrFoojh4fvW9K-wDKtgvQwCYZYABlC9JY72gtpaV2OV2UrB4aXuJX6n1NNXaSzpPqSupAIGK3Gaw39yrzBgBjTYAe0nnRu10BO7-gNRvKGIMCBTa7c-c0o0eNGe81xv1w8_-6auoKZYS8rzXQ8T6XLUjC1mRZD_cGxnfEra2G96-Cqm9WZO5hVX5fpXZhybz7neyGKlUKZG_An-jGmc9j_m03-5EEOfKAXJNlmOT1IynNVudtzTTrh8O5Dp4nD6fKsyOrg-6yRePCiP4FeItLCH6uVLWWdR65WZzklQuPrBELg58OzIsaBuKCKNjODSA4dGVE4JurhmgnnSmaqz2z6s0Zd1gXERebk_1WEmkWd03jO7dXMk3hOM9zV9BrZALOAll3GsvCqgh9kfouX-3ZNSNO7Lah6ecLD_zK228ap6r1MeY2VK-PiHUEnH58jh2HuutZB1Ge0GVvsYBue_r0FjGVNh6a9XYwIaf1Um2Z81WgHpWHZ-pLVZlkbN1vxgqLNBpjDy6UWpPJzOUv829C31WID92Wa6XPsfq6sIvYRUEx03DE2sbXKjUNX2t8InuLCgC6_wmq-GOoZ5vLKt1KHMicJUM9YFZYYKd-7c25X6DLplAnP-Hw_URgRINQdD8kOWzZ_70SiEq0om6OWniva6czSiwrcml_UBDA5Xr8pNtSWqtNbHh1LJzJenVIZl9gPLRs_o-OxB9gylqk7HwQZgKPCbvccYyh162Iy_Kg2j07hnDuoiUyZ93o9x_3Asf8Ms_E_ov6CqpFgKICX6rEE0oOgFO_pKvwtNH8fF-uNkVGKQwNYX6S33SlWh_pULYLSl-YrXVP0hLLmGlunnOGXUIVTXjQcc6AheR8Dmg9jDIefpgHMH6hegAnoZL0_AVuG-yd9LSRSh2qH_rABtJHTOx-0qQ6yYnrzHcMuvatCwDuIePK5DcxBj8KhKq9F4y_i5Ym9drIskRvAzwygZuIIuT3uyXl5nI6YE_jd6F9w4PZ7SkOs9JvfCnt-Wm7UKI6dxLnCRoTarUwop1wDZ77-rRwYoo5zYwF73BragZBZuWNB8ImLlktcAyCBF6P2_F2j4jvnQNLShYZ5HsJKsJNljjIiKYEAeJ2ScT2tjPSfMsdssWQPPByDgwnWtGpx2z6JTFGLUHaj_WbQe3hciyl7jGM2U1JrA610-Jb0X_OiGslZuYBasmPkEXFbDhZy_QZ4Pjs4RddBqrS15-H4FphxsB4knYHtfAzvJno80QmR69zvIfBSIScEx48foHjbeObNpW51IGbg2-yhssa9YtLpjpafnc1-yJ5xj6tJWYZcpskhgADRQvoxF8Xa7BE8o0D9-I7r2Yp0wMfYrbX8NCTBUWczxBZt2juBIERwgjHZzphIGVXNJ6ARm9F12UMf2OwUEk56J6SiSfB1ho7EDdARwj6Nfkm1LjpYLDhii-IRVJUN8tphw6SHVJBbMucYsXsL8viafUwdh7MbBwLKOPgZM4H9BqWFePgEglf7nzrALd2WV40tOai-sm4e4UCKh9bQ1qNw-uHQLP81NNzMA.bMWJdVmxAg2RZm7NE9wTz4H4LwjDb21tFV8hGtTKGFI'; + public const ENCRPYT2_SIGN_TOKEN = 'eyJwMnMiOiI2UWMyblpRYkxyRSIsInAyYyI6MTAwMCwiY3R5IjoiSldUIiwiZW5jIjoiQTI1NkNCQy1IUzUxMiIsImFsZyI6IlBCRVMyLUhTNTEyK0EyNTZLVyJ9.WqxhY5Qk2uYhlwqtH7JLb3l8QLxjo9Keq4p0iT_Xy4gIxbnQvIlkOYyf9b7QcBiOlStPm9-RMvt-MmgV_dibTvmrDtEq9CFy.J1NxAp-hkYrh4SHjvErdhw.cb9NQdiQDAoAJKn0jGm_nK9UzuxGcAdaycTwYv1IkYOPg4nteGXUoEZH0Yzh6KJh95vNBbVVhpKnQaYmYfnR7WzcufJJ9XpvsHGP2_KV_kItWTijClXun8Eyg5DDY5lK0Z656-f_walGoZnUxCGTxmra_3yOGbKMKaEq-lxBuf1lPdmi3IpGm1H4IOiiHLpu0ZjEUt_S-L4mOLmnc4TR17sEgB260e7-8np_GqwN5vns7ug3-M_AGFJUyDqKV6hRGu3qn7lLJynldNhIEitT759O-PWyCEff353WZv9d9PwdOQDE8N_pHRgm2JVDGeagDRzygEqhSMwct5o_IWJWNQs6zllnNYGyaB8B9fu9UhFq2dutIS24JwERnlNUcy_RXGq8CGEmnctEX_Has3fXUHanBqBGPJW7cZ2sei5O64YkndCMvePAWaScyd3j9QGvYHrBsriecxk565wjU905OHyXBax_UupbPeCKivDYOWou-HESV_xMHJhFqLJKDiCT7g-33OgXpLPpkzNi4a66KCYXngkYeCUhoGV_W7sVYHDe7GPwz99V7-6YuFnCI4qAvZkUQuzUtDILzrP8PBvsQ6DikYWkqcp9ypAMdej3OTh0TNZdu1ROEYFQB5eNv6I5N-V1ifD9WO1ch5ijYJ1xX-4FbUGt9d6Y-voPzR-6LK5NKhLs9xhNVv26mUZ3HNH78-qoZxXTrHtiarrkE7qc60-oh9BH0XZKsSZcCLpxukX8ibFl4-adYsFhFRAzU-XZT_LdXsBmX3U4DRGC-maM4Bqretaj5DdtjQrPuSETVNO8cMb8rf4UdLBKdXYpp2uqtbIhu327BDCtSptQ5uiZjwlxkMzYFBWCx0W6KN9PPvXhrKpPUzZ_rN98JdSZ0AtyQbR0Oqos7jF5OXs2HDEtOa9EQora3hVGiaOpy-u-AruQ-5SDl0Yh_1n3O2sI0HMO1fMvPEwq6GbetLg3a_xHNvPV6gTZv4-vhVG7b53m526pvx86pYqRp1XKCzQwBTsVUSDAW4mIqtu1R2Yilnb0Ep6wHic0n710YRco4H_tkRr7nYd526WxXYe_neEdD2du-3m3eakZ1IKOdcqiSNvPXyNTDpAPl6U9ap7VsqzksfO3Xo06RwbMv9HcLf9hqWYS6HYRvVVLcShTF9yVMc-XWIZaS0tL_M4jcD7SJsFhLo5p021j68vYoZFcniqFKRwC7ChoBv2Vh4L-eu9o9qlLkUm56g5QhZWQr7OuLxtoKpfoNuRcsg6cNl2TPDl1Hd36s6LtpHneEDaBuRY-s-46pa_gdYWN5i_LHUO807ZE6OmvFBLXy9qY38L5zoXm1FUtP6Td1kXnsiYiluSz8PFelqmkqYALRc1byfvjtjnZBAyMQ9P4xTP3_mEaPKw18O3NYtANYTJjvPvzxuDi7h_slKryJqtvkUcUUd1g45Qibj-jIW9WZdEEWjmDFQ9PA5CB32CfLIq0BqPCuwpFSHiukQ9ZKIpkw0aaDDS25MtwTc3zieiFxfvLSh3Y0lZrcStogk8f4FZecIM7Gi2joGBKZEHg5l30C4JlrZhnxVoIW197XVDDcXl_tRUFaxKAjyRCqQRMcFbapH1pTdA5Hqbwwron6GPnfZbSQoZmSNUuVbphK4DMMDlaMi9nNmIc5ckcR93m0io7fQkcMzAMjwcmMdWqy9boEva8ZEagmRlFoF0ryTsWNCe839dYtZAYuvDzh2JlG6lkisH5Begrx2sK6krKg0hKuvl2oy-eL0iPHff8AxGyrXp8VKF_afikaZyepIavfyW4AXg_1eSh_CP_WVxIvUpCUwdamoowYP0bvTNn_5dwywWD4lgF6FiQygBPCs7A3mQRUsvTyja0sS2LI7yRMWKiJ98mLt3aLNWUCwc4T37Uprk3yQjwFWZXBLsb40sCy_pOwuBzRsabANLbQ9KBThoeLp4FTrCXAK8VdVgUvZyzW8cuOix_EuV32V0HTYQpfbOVlOriAvT2_u2p7QQD8NjgTwy2XEI6zoIXOiXCZJ1ijowCKdmck95nOr4C50_bOFhQTInt3FAlxaAnKRWpdZ5IQ5xDqCsIY4T5gMAbm5SmHmDbwvGTsC7Ugsd6yattg907pmpFF_S9oR6hxKoxV9QfWdVcpXBgvYAc8k0zia1RyCUChAr76AMwHSpmuqGobhBU0J0Cpz11WA9QCQJMNcIezw07Dj-cf-XHbrVxGuXnJy7D1HrKddj8naPuTiRXw4UreJo4yBYQJxu2u1NXZfWyqgVdNi5hyH8F6l2fkMUD4Zmesplmp-NGsCxWV4KCioZXtZit_bpViBeqGQ1GTzvmYCgftqrZ_lJRCAbYjkZ_jZKfnysErxpp5fBpJYgBIUMLaKo4X47edQdygqZGC-58DFO5PTeCRSDcILYUgddTLG4D4lXg2l4SFGdidct-WSNHQhbTpjZeZZ4cX-fgZ2MzjcMf60rNRvp_AkvEqfdXEmnbWhL0820szKWaSPaJXqyapAaW4L4CYJGaIS8q9o8sZNQaMcAOzBVBdFS84AhnnIJeNQHxyUiVhR966KXewj7qKgxP9_bIkRLp3CoWvN8YsJXpg2NjryJNgTeswBdxTJiNeYM1v15rRMDBkpkLB-MfuGRjBnPiNB_KpTMfHdPi_J1d3wyOYT3bk27BIl43Mpubmz-2kyyyCZko9C7QvELaEgGNpKDgMfv6viqHw3BpLvBcMIsLxaT1arAHagSMyPu5KDKuAfD93Xl1ydEnkkSN11zmTks7rk7S3oc3kMPNyKrjhzXFMShgpjHBmJvSBimZgBUG9R4VmjSEjffvw55PZakAzA4f0TfN2hEgFdH1ABZoqGDrJXFRmjZY9hlGREwZ5DzV61144OShZ4yIagF2Efd7T4JBcu_9NLFg74M_CNCIJdJRerdOUhk2i7v2jcVT-N19Uo3CB6oQ9YfOecRqcl9jbX-r1GjnpYjdzEtYuuce03XNLVH5R__WgUssTYvupvCcPa8C9ZASSUQKVnM0r3-dD52E2JEBON123Z69OkUfx9QXqgJirT2mfwlpbarZ5Fk9DgE9J5M960DjUdRi31iP-KC8TDLJpoMyCvKLYKVdrqFwDx2xJRTOhpN02TWpkqMGXUech3QUndyEORq_FVgY3Xl1RXd9XFKK9Bv403lPIqt0421IFazWA6UAB5La0nf1iPrbkF9x-4uUBnLrEV20mlXWNh8X5FqVErjmqhQoAZwON1F-wRBju_bS8JZDpSYnzE6_8_550b-d9ZXtY1FD5b0uuF-6sU0gjiZmR9NuSc5JN-g_M8IrU8wCeuBovKobuvmUh1v2PXjJJ5iG1sjs3U3UnoaMdKMaUiMGf_yZLRTZFaMGpFfynJfksYUETaQ58hpjG4hIoNEBkMiC41O0SlsqyBAJ2R249K9BByULup504LdwVdRkyhoDRulZ7pEHxZbU9jKMvjPq9bfvfzHqPnSXzzFuQpO_HOsJZlk0Z1f8FtPAFCwCCclpRdvXOursYEnTIcpYLx4mZrr0wWUsmsxCYZHbEXEdtwTm9tJEsOMcLzNic2C9FZgQuE56Hyh5cLTOsEA3C8eoZBhVfxpVExJTSFal63_1wMvymXdXbLiDlGHTAnnPhL5RtaD6aFprNVEiK56vd7cyao7P_g2pciVkJ6EpvjdYCKOI_9uQGZTrdU2HEM__5l_b6ejIXh1EzcttCdqUOb3dUNEGfdMCMJdIYfYttlVmEGALlXTnvy4kH-XgaiGZiKxAU0ZKvCysqbd8GtXbSyHQ5AS8qHx3pxR8_Mws6rV55O5uycGYYCYWTfeZ0-8UBEKFAnSWi8MvIilsurAmYj8r9etUOLZ9ah-lKjedmURU3jQfYFa7r_-dcTJbRipzY5cNUKE6hJkdn0i9s3mz-fT-j0LcKEGyLfij55zUkKFQrkwyj3X6f1X_YcTkxXoSOZ23_zLisQIecOSgaPTyIiKQr7G_cwnoUBLNgUH_GB66uirM5Q2hhBD8DIToJYybaSD8XwScGtp3PirfVmP2ZbkFdh1021Mx65u3lh5Lpy9NVfyZ0m4GWGFRwPjqL7AcEtoPGi-PqEh61NrXNlKwsT_IMc3_D1hOb1CAYaNX3QjGYfgncc3tSpz--ZJvq7eDGNj6Z63qudum-pm7Sfgu9eBcknlw_TKjp67Ttnzt6UECZD47DFAgcAAnCs9nEthOU6B-J9hLYaLy2z3vGFyBPfaEH-B-YHRlriIaM5vCTHEmV3gY2iJxFA1DD7VDj3vx6Qw9uMLbcfl4eVXQRNs-HiW-ML6yUv0_zxIspC8HTX1l62vMi2_ofue9lDtjKfxVV-2inRhjae1fhViHb7DcwArnw2XaZYlTC0Oe_neA2pY4DuTAAuMikfQbaf50sU-gszAD_Xmmh6WvDHr4FdrTtf8ew3I1YCmP5lguc_w0QuC-fAAsVvz7bsIcSMFSFWGB71H9dAGSQ53bmboqUrRL-kUjLEF2hF9FqnVGmsx6lH9eMn9tWCApwm1zQNNMM5c.a1VctJwTCcJQ9LC1xGoyKn_2743jHhGpU5G8ucFR2ts'; + + /** + * @var ProviderService + */ + private $provider; + + /** + * @var tokenService + */ + private $tokenService; + + public function setUp(): void { + parent::setUp(); + $this->tokenService = \OC::$server->get(TokenService::class); + $this->access_secret = \Base64Url\Base64Url::encode('JQ17C99A-DAF8-4E27-FBW4-GV23B043C993'); + } + + public function testDecodeAndValidSignature() { + $testtoken = self::EXPIRED_TOKEN; + + $serializerManager = new \Jose\Component\Signature\Serializer\JWSSerializerManager([ + new \Jose\Component\Signature\Serializer\CompactSerializer() + ]); + + $decodedToken = $this->tokenService->decryptToken($testtoken, $this->access_secret); + $this->tokenService->verifySignature($decodedToken, $this->access_secret); + $claims = $this->tokenService->decode($decodedToken); + $this->assertNotNull($claims->exp); + $this->assertNotNull($claims->aud); + } + + public function decryptDecodeAndValidate(string $testtoken) { + $serializerManager = new \Jose\Component\Signature\Serializer\JWSSerializerManager([ + new \Jose\Component\Signature\Serializer\CompactSerializer() + ]); + + $decodedToken = $this->tokenService->decryptToken($testtoken, $this->access_secret); + $this->tokenService->verifySignature($decodedToken, $this->access_secret); + $claims = $this->tokenService->decode($decodedToken); + $this->assertNotNull($claims->exp); + $this->assertNotNull($claims->aud); + return $claims; + } + + public function testDecryptDecodeAndValidSignature1() { + //this is not a good unittest style: $this->expectNotToPerformAssertions(); + $claims = $this->decryptDecodeAndValidate(self::ENCRPYT1_SIGN_TOKEN); + $this->assertEquals('10TESTSAM30000004901VOLKERKRIEGEL0000000', $claims->{'urn:telekom.com:client_id'}); + } + + public function testDecryptDecodeAndValidSignature2() { + //this is not a good unittest style: $this->expectNotToPerformAssertions(); + $claims = $this->decryptDecodeAndValidate(self::ENCRPYT2_SIGN_TOKEN); + } + + public function testDecodeAndInvalidSignature() { + $this->expectException(SignatureException::class); + $testtoken = self::INVALID_SIGN_TOKEN; + + $serializerManager = new \Jose\Component\Signature\Serializer\JWSSerializerManager([ + new \Jose\Component\Signature\Serializer\CompactSerializer() + ]); + + $decodedToken = $this->tokenService->decryptToken($testtoken, $this->access_secret); + $this->tokenService->verifySignature($decodedToken, $this->access_secret); + } +} diff --git a/tests/unit/MagentaCloud/BearerTokenTestCase.php b/tests/unit/MagentaCloud/BearerTokenTestCase.php new file mode 100644 index 00000000..16a6615f --- /dev/null +++ b/tests/unit/MagentaCloud/BearerTokenTestCase.php @@ -0,0 +1,225 @@ +realExampleClaims; + } + + /** + * Test bearer secret + */ + public function getTestBearerSecret() { + return \Base64Url\Base64Url::encode('JQ17C99A-DAF8-4E27-FBW4-GV23B043C993'); + } + + + public function setUp(): void { + parent::setUp(); + + $this->app = new App(Application::APP_ID); + + $this->tokenService = $this->app->getContainer()->get(TokenService::class); + $this->realExampleClaims = [ + 'iss' => 'sts00.idm.ver.sul.t-online.de', + 'urn:telekom.com:idm:at:subjectType' => [ + 'format' => 'urn:com:telekom:idm:1.0:nameid-format:anid', + 'realm' => 'ver.sul.t-online.de' + ], + 'acr' => 'urn:telekom:names:idm:THO:1.0:ac:classes:pwd', + 'sub' => '1200490100000000100XXXXX', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 7200, + 'urn:telekom.com:idm:at:authNStatements' => [ + 'urn:telekom:names:idm:THO:1.0:ac:classes:pwd' => [ + 'authenticatingAuthority' => null, + 'authNInstant' => time() ] + ], + 'aud' => ['http://auth.magentacloud.de'], + 'jti' => 'STS-1e22a06f-790c-40fb-ad1d-6de2ddcf2431', + 'urn:telekom.com:idm:at:attributes' => [ + [ 'name' => 'client_id', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '10TVL0SAM30000004901NEXTMAGENTACLOUDTEST'], + [ 'name' => 'displayname', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => 'nmc01@ver.sul.t-online.de'], + [ 'name' => 'email', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => 'nmc01@ver.sul.t-online.de'], + [ 'name' => 'anid', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '1200490100000000100XXXXX'], + [ 'name' => 'd556', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '0'], + [ 'name' => 'domt', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => 'ver.sul.t-online.de'], + [ 'name' => 'f048', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '1'], + [ 'name' => 'f049', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '1'], + [ 'name' => 'f051', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '0'], + [ 'name' => 'f460', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '0'], + [ 'name' => 'f467', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '0'], + [ 'name' => 'f468', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '0'], + [ 'name' => 'f469', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '0'], + [ 'name' => 'f471', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '0'], + [ 'name' => 'f556', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '1'], + [ 'name' => 'f734', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '0'], + [ 'name' => 'mainEmail', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => 'nmc01@ver.sul.t-online.de'], + [ 'name' => 's556', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '0'], + [ 'name' => 'usta', + 'nameFormat' => 'urn:com:telekom:idm:1.0:attrname-format:field', + 'value' => '1']], + 'urn:telekom.com:idm:at:version' => '1.0']; + } + + protected function signToken(array $claims, string $signKey, bool $invalidate = false) : JWS { + // The algorithm manager with the HS256 algorithm. + $algorithmManager = new AlgorithmManager([ + new HS256(), + ]); + + if (!$invalidate) { + $jwk = new JWK([ + 'kty' => 'oct', + 'k' => $signKey]); + } else { + // use a different key for an invalid signature + $jwk = new JWK([ + 'kty' => 'oct', + 'k' => 'BnWHlEdffC0hfKSxrh01g7/M3djHIiOU6jNwJChYWP8=']); + } + // We instantiate our JWS Builder. + $jwsBuilder = new JWSBuilder($algorithmManager); + + $jws = $jwsBuilder->create() // We want to create a new JWS + ->withPayload(json_encode($claims)) // We set the payload + ->addSignature($jwk, ['alg' => 'HS256']) // We add a signature with a simple protected header + ->build(); + + return $jws; + } + + protected function setupSignedToken(array $claims, string $signKey) { + $serializer = new \Jose\Component\Signature\Serializer\CompactSerializer(); + return $serializer->serialize($this->signToken($claims, $signKey), 0); + } + + protected function setupEncryptedToken(JWS $token, string $decryptKey) { + // The key encryption algorithm manager with the A256KW algorithm. + $keyEncryptionAlgorithmManager = new AlgorithmManager([ + new PBES2HS512A256KW(), + new RSAOAEP256(), + new ECDHESA256KW() + ]); + // The content encryption algorithm manager with the A256CBC-HS256 algorithm. + $contentEncryptionAlgorithmManager = new AlgorithmManager([ + new A256CBCHS512(), + ]); + // The compression method manager with the DEF (Deflate) method. + $compressionMethodManager = new CompressionMethodManager([ + new Deflate(), + ]); + $signSerializer = new \Jose\Component\Signature\Serializer\CompactSerializer(); + + $jwk = new JWK([ + 'kty' => 'oct', + 'k' => $decryptKey]); + + // We instantiate our JWE Builder. + $jweBuilder = new JWEBuilder( + $keyEncryptionAlgorithmManager, + $contentEncryptionAlgorithmManager, + $compressionMethodManager + ); + + $jwe = $jweBuilder + ->create() // We want to create a new JWE + ->withPayload($signSerializer->serialize($token, 0)) // We set the payload + ->withSharedProtectedHeader([ + 'alg' => 'PBES2-HS512+A256KW', // Key Encryption Algorithm + 'enc' => 'A256CBC-HS512', // Content Encryption Algorithm + 'zip' => 'DEF' // We enable the compression (just for the example). + ]) + ->addRecipient($jwk) + ->build(); // We build it + + $encryptionSerializer = new \Jose\Component\Encryption\Serializer\CompactSerializer(); // The serializer + return $encryptionSerializer->serialize($jwe, 0); + } + + + protected function setupSignEncryptToken(array $claims, string $secret, bool $invalidate = false) { + return $this->setupEncryptedToken($this->signToken($claims, $secret, $invalidate), $secret); + } +} diff --git a/tests/unit/MagentaCloud/HeaderBearerTokenTest.php b/tests/unit/MagentaCloud/HeaderBearerTokenTest.php new file mode 100644 index 00000000..842b9466 --- /dev/null +++ b/tests/unit/MagentaCloud/HeaderBearerTokenTest.php @@ -0,0 +1,236 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + +use OCA\UserOIDC\AppInfo\Application; +use OCA\UserOIDC\BaseTest\BearerTokenTestCase; + +use OCA\UserOIDC\Db\Provider; +use OCA\UserOIDC\Db\ProviderMapper; +use OCA\UserOIDC\Db\UserMapper; +use OCA\UserOIDC\MagentaBearer\MBackend; +use OCA\UserOIDC\MagentaBearer\TokenService; +use OCA\UserOIDC\Service\DiscoveryService; +use OCA\UserOIDC\Service\ProviderService; + +use OCA\UserOIDC\Service\ProvisioningEventService; +use OCP\EventDispatcher\IEventDispatcher; + +use OCP\IConfig; + +//use OCA\UserOIDC\Db\User; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; + +use OCP\Security\ICrypto; +use Psr\Log\LoggerInterface; + +class HeaderBearerTokenTest extends BearerTokenTestCase { + + /** + * @var ProviderService + */ + private $provider; + + /** + * @var MBackend + */ + private $backend; + + /** + * @var IConfig; + */ + private $config; + + public function setUp(): void { + parent::setUp(); + + $app = new \OCP\AppFramework\App(Application::APP_ID); + $this->requestMock = $this->createMock(IRequest::class); + + $this->config = $this->createMock(IConfig::class); + $this->config->expects(self::any()) + ->method('getAppValue') + ->willReturnMap([ + [Application::APP_ID, 'provider-2-' . ProviderService::SETTING_MAPPING_UID, 'sub', 'uid'], + [Application::APP_ID, 'provider-2-' . ProviderService::SETTING_MAPPING_DISPLAYNAME, 'urn:telekom.com:displayname', 'dn'], + [Application::APP_ID, 'provider-2-' . ProviderService::SETTING_MAPPING_EMAIL, 'urn:telekom.com:mainEmail', 'mail'], + [Application::APP_ID, 'provider-2-' . ProviderService::SETTING_MAPPING_QUOTA, 'quota', '1g'], + [Application::APP_ID, 'provider-2-' . ProviderService::SETTING_UNIQUE_UID, '0', '0'], + ]); + + $crypto = $app->getContainer()->get(ICrypto::class); + $this->b64BearerToken = $this->getTestBearerSecret(); + $encryptedB64BearerToken = $crypto->encrypt($this->getTestBearerSecret()); + + $this->providerMapper = $this->createMock(ProviderMapper::class); + $provider1 = $this->getMockBuilder(Provider::class) + ->addMethods(['getId', 'getIdentifier', 'getClientId', 'getClientSecret', + 'getBearerSecret'])->getMock(); + $provider1->expects(self::any())->method('getId')->willReturn(1); + $provider1->expects(self::any())->method('getIdentifier')->willReturn('Fraesbook'); + $provider1->expects(self::any())->method('getClientId')->willReturn('FraesRein1'); + $provider1->expects(self::any())->method('getClientSecret')->willReturn('client****'); + $provider1->expects(self::any())->method('getBearerSecret')->willReturn('xx***'); + + $provider2 = $this->getMockBuilder(Provider::class) + ->addMethods(['getId', 'getIdentifier', 'getClientId', 'getClientSecret', + 'getBearerSecret', 'getDiscoveryEndpoint'])->getMock(); + $provider2->expects(self::any())->method('getId')->willReturn(2); + $provider2->expects(self::any())->method('getIdentifier')->willReturn('Telekom'); + $provider2->expects(self::any())->method('getClientId')->willReturn('10TVL0SAM30000004901NEXTMAGENTACLOUDTEST'); + $provider2->expects(self::any())->method('getClientSecret')->willReturn('client****'); + $provider2->expects(self::any())->method('getBearerSecret')->willReturn($encryptedB64BearerToken); + $provider2->expects(self::any())->method('getDiscoveryEndpoint')->willReturn('https://accounts.login00.idm.ver.sul.t-online.de/.well-known/openid-configuration'); + + $this->providerMapper->expects(self::any()) + ->method('getProviders') + ->willReturn([ $provider1, $provider2 ]); + + $this->providerService = $this->createMock(ProviderService::class); + $this->providerService->expects($this->any()) + ->method('getSetting') + ->with($this->anything(), $this->logicalOr($this->equalTo(ProviderService::SETTING_CHECK_BEARER), + $this->equalTo(ProviderService::SETTING_MAPPING_UID))) + ->willReturnCallback(function ($id, $field, $default) :string { + if ($field === ProviderService::SETTING_MAPPING_UID) { + return 'sub'; + } elseif ($field === ProviderService::SETTING_CHECK_BEARER) { + return '1'; + } else { + return ''; + } + }); + + + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('1200490100000000100XXXXX'); + $user->expects($this->any()) + ->method('getDisplayName') + ->willReturn('nmc01'); + $user->expects($this->any()) + ->method('getEMailAddress') + ->willReturn('nmc01@ver.sul.t-online.de'); + + $userManager = $this->createMock(IUserManager::class); + $userManager->expects($this->any()) + ->method('get') + ->willReturn($user); + + $provisioningService = $this->createMock(ProvisioningEventService::class); + $provisioningService->expects($this->any()) + ->method('provisionUser') + ->willReturn($user); + + $this->backend = new MBackend($app->getContainer()->get(IConfig::class), + $app->getContainer()->get(UserMapper::class), + $app->getContainer()->get(LoggerInterface::class), + $this->requestMock, + $app->getContainer()->get(ISession::class), + $app->getContainer()->get(IURLGenerator::class), + $app->getContainer()->get(IEventDispatcher::class), + $app->getContainer()->get(DiscoveryService::class), + $this->providerMapper, + $this->providerService, + $userManager, + $crypto, + $app->getContainer()->get(TokenService::class), + $provisioningService); + } + + public function testValidSignature() { + $testtoken = $this->setupSignedToken($this->getRealExampleClaims(), $this->b64BearerToken); + $this->requestMock->expects($this->any()) + ->method('getHeader') + ->with($this->equalTo(Application::OIDC_API_REQ_HEADER)) + ->willReturn('Bearer ' . $testtoken); + + $this->assertTrue($this->backend->isSessionActive()); + $this->assertEquals('1200490100000000100XXXXX', $this->backend->getCurrentUserId()); + } + + public function testInvalidSignature() { + $testtoken = $this->setupSignedToken($this->getRealExampleClaims(), $this->b64BearerToken); + $invalidSignToken = mb_substr($testtoken, 0, -1); // shorten sign to invalidate + $this->requestMock->expects($this->any()) + ->method('getHeader') + ->with($this->equalTo(Application::OIDC_API_REQ_HEADER)) + ->willReturn('Bearer ' . $invalidSignToken); + + $this->assertTrue($this->backend->isSessionActive()); + $this->assertEquals('', $this->backend->getCurrentUserId()); + } + + public function testEncryptedValidSignature() { + $testtoken = $this->setupSignEncryptToken($this->getRealExampleClaims(), $this->b64BearerToken); + $this->requestMock->expects($this->any()) + ->method('getHeader') + ->with($this->equalTo(Application::OIDC_API_REQ_HEADER)) + ->willReturn('Bearer ' . $testtoken); + + $this->assertTrue($this->backend->isSessionActive()); + $this->assertEquals('1200490100000000100XXXXX', $this->backend->getCurrentUserId()); + } + + public function testEncryptedInvalidSignature() { + $invalidEncToken = $this->setupSignEncryptToken($this->getRealExampleClaims(), + $this->b64BearerToken, true); + $this->requestMock->expects($this->any()) + ->method('getHeader') + ->with($this->equalTo(Application::OIDC_API_REQ_HEADER)) + ->willReturn('Bearer ' . $invalidEncToken); + + $this->assertTrue($this->backend->isSessionActive()); + $this->assertEquals('', $this->backend->getCurrentUserId()); + } + + public const ENCRPYT1_SIGN_TOKEN = 'eyJwMnMiOiI4VzhYY21iaHJPSSIsInAyYyI6MTAwMCwiY3R5IjoiSldUIiwiZW5jIjoiQTI1NkNCQy1IUzUxMiIsImFsZyI6IlBCRVMyLUhTNTEyK0EyNTZLVyJ9.5bA_ctLbQOnMojJW3MPo83AIvCAu3MpmaaD7j2GzqBv5_-D4w69ONqcPEsc6LYMG9B-rw3HDXng4Mqye4KqpW70ECpf9HXV6.6zl4Zqp4wbcO_AqqmpA3sQ.y7dHcwxXveYkuh4UaqHhE4nvP_avZsxaf7aAbnJdDHHKbBKvEKKqHkPg593i14ypWuRHd2i9Opsuyppfxx9Hw7C7N7LJ8UCTYMihHqlJkHecB08xgJ3ciE0L2Qtvg9hfxQbHNVV4p1_KL3ubAXt9ovwDCOJvN6PXyixUDtYYF1D_Km7Ze1ptUNbwS2H4vf-MKHwwrm5uhTvXOppGNO-0tYnIMOZ8BkiTtrrlO6IQbRcC4EMw74PzbFsQXY9u1xsNZ9IOrzbBl_EyPBLr5ool1BGlvNog4XFsHLgxUa5cjIcZVRMgZSLWdToTiXYFAWdO6fbQrRWT8ERRDWjiDxJEaPlfI_61G5NzJN2NKnSAY7fR8i3Rfs_JoF1TtpR5dGU28Lk1vcLjKYBLqp2hjW97QsANVgmalkkJMUpiAvNN48ZSCK9T3vTfiH7unFRNWvTKvZXyHIkYQPZ0-b3Z9s5oLMx93Snvcq9jQVKA1dWU_bEUIOnwP65ADU_FIkYB8gsZXp5Za3HrK63u03Lij6rwkJpEPbwcnxhBkMhtKOOwQVZm1ZBf_lVyn39MFXmLN_gDD052vFpxl1NnG0KEg8XJQ_usE9e64q7W6IG4gRm9NYG6rdeik6Dm45K8fA4oUiyjdgHjveR6GW8uXQR-tWXf3IC-_2jws2PJ31acdoEbDU30XlVeCqENW-ylPJ10rP28XxboQVJMRrzMiEzu39IH3c02czHh81U09TREVsO2S8CCQcahboaplDg9kpr1UZpsRrjg40bEtdm2cKubTbczGiXiF7sI0qE-kHm0aiK5c6mO8fHETMCmvh2vhxcYo_T6q7VklbwiZVbn47z-oriEDyPlLrB_PzYR6fNRbtObttj0CHRgf-NI69RU2pAGxujSi2lEhNkG-CAFNfASKm8uSUCg8UPr7v38c5vr4IuYC1gYjxgebXIh0EFX4G8jZM6ljPSzmMFDyErWJQ5OrtJjuKrUa96Yp3oOZTemtCwc--mrDXmpwVlaBMCuuJDz6zucxwSeVK0mP0t56zHeK59jxz0OfV62TrcVeZaLqSl3o-pVsY5KrLxL1qf2QIry-uy_c1zi9AuZnSbH3t1RvmyG5-QIh5WSPOLXG9ivuHKAdQTvBnchXWfkUVkoPYuPFyBydlPAhpRQyBLHboqdT6lIdoQ5lBRI8vsGb9wQVSQx08hbpEFOPMe-SJqzjZp36sUurJrgj_ethbIWkTSe_HPkcvBv8X0kyvhnyTKYJoroE5HDM0dtgFW8xK8NmOZOuREzJW5fpqzJML8iY0p1IX3bvGrCeVMEJtM0T6KSJFdPHBAzkWNNMBUc2jhuxa6B2cSaMz60bwSCw8n5NWz8wkXUFJJkHKEnK8tFbtOQXHeGG48k7Wl6kgrQkAFAHZqQt9gRDdmGcYAYHVK7cESjABV9LWQIQYy0eyveU0sWE5sYXKCwsk8rLiKt5GmZlRQ0rOltuFXRTu_EZYuqR0DCRXrjQWVN1zLTy0LMqAvDR-PJcFtekbT9CXLEW6M6GHzJhYfNyMc_cPitG8QwS5EWGzJjQIiNsJBRyV7cPlHeMhKzDtEk3DR3l-qQJa9-54RQB-kStJjB0AAZ21ku7eBS6orT0lljj935eghlHxAzyr1fvlDjIpHc--ob_7DOPc9sBGqcwdYoZ28zD1d02rpJujOwTe4zgll4vffJ_aFP8hm19pmroCwFsZPWIK6GN_cllJaxnllkJ_9c-7eBj1rKkNX0DLyNwKoMYttugeQFWAxaaqWhoOpQXnRHaVt5hTzoexi5C2j_aVBUAzyMPZtvuYgY1uc8zeKt5X8rAy3Y7WqYeOy8Q6IezVyTE6p0kzYgzUT1Vg2XZEr7dBgNkv8ySfYQNG5d8_PtvBHX-SOy25rtes7oUHHgZx0AkpomhNGSwfrW4dyIWCa6j5qUexqs3TPip_FAJwdW38OnyfPQ5SHLTt8D6OCOLN70MdbPpeoFkGnx1oj1Xjx_UW8mtueWAkxidv6Lamf_D5j8sJvkksne8Nos2YvGNkaGZwQK8YfjvPP-VVdukLMqoloovOuvgxLVLSvnDYcRRjfwAdiKwFNGdMbdV5LwfAzVAlncyWPJso3Lk9fPYd88YW8e6o7xiboiushcbDQU0ZN_Zh9YGk-8R4VnvAuI3yWxLrBB8NFUwKYkNBupVWrxRHJbJEebsLv9r_PZstBHHfMFpcQYX05NYfQiezhQ9l-aseC9Ay4FLbcxyXkIiPEBfiwZESqQbYoL3OeBQYzsV8AFe4GVdUUwPCuPjKR52UlkPiUJthxGkLFfcEPbqfX_lByN5YZRMSruOt6yKysbBIw0gcC6n7wuA_URaFNSPfyHe6nqAtveh1YjZpwZszAERyk2ziFXKFYFppdjMPvxF37uWoH_BEpv9Bs7yaxPRK7pfniS105RBsDFS093-3sUYM6W7IrmPfKAe71OtdWtQQqQKOAX3WGFShCIKyz-aOJWJPRG35Q2DOGu0nehFetGVsSnt-ehmru-Zuv4IanlF0_3SjQ7l7l6gg3Sfyy6sN8SVvxTtw4jLkaAM6cpmVMQVP8uQeJ9IFSHyq1kFceQcguh5tbwMknJzcMNzmZ9zEOG4ifyk9zmeulX9Rtf3lIXIOU-1lEs5bVm42eg1IKpxaY8PeTrT4qvPIyVkOprpKGIAcGyD0tP11vvDCvbltEWBo72gdbtD9tUdUPK0XRD_TgEPy2YU6I6BsKBStd40Fk6nOCGrq-mjYmH6OK3JUF3EVV7E0fEg7BgnYPLxcla0l7H6LpY4sqmFwapDqknjhgbqK0dyZDGWEPJ7Ph_5K6BazKuV_1bf6ZFOuRbm72cmT6vAJM8BhihAdTQt92QbTPikjLS2he5AfSV1ieDgLT26dsLNuLkyExyBqUGkrFoojh4fvW9K-wDKtgvQwCYZYABlC9JY72gtpaV2OV2UrB4aXuJX6n1NNXaSzpPqSupAIGK3Gaw39yrzBgBjTYAe0nnRu10BO7-gNRvKGIMCBTa7c-c0o0eNGe81xv1w8_-6auoKZYS8rzXQ8T6XLUjC1mRZD_cGxnfEra2G96-Cqm9WZO5hVX5fpXZhybz7neyGKlUKZG_An-jGmc9j_m03-5EEOfKAXJNlmOT1IynNVudtzTTrh8O5Dp4nD6fKsyOrg-6yRePCiP4FeItLCH6uVLWWdR65WZzklQuPrBELg58OzIsaBuKCKNjODSA4dGVE4JurhmgnnSmaqz2z6s0Zd1gXERebk_1WEmkWd03jO7dXMk3hOM9zV9BrZALOAll3GsvCqgh9kfouX-3ZNSNO7Lah6ecLD_zK228ap6r1MeY2VK-PiHUEnH58jh2HuutZB1Ge0GVvsYBue_r0FjGVNh6a9XYwIaf1Um2Z81WgHpWHZ-pLVZlkbN1vxgqLNBpjDy6UWpPJzOUv829C31WID92Wa6XPsfq6sIvYRUEx03DE2sbXKjUNX2t8InuLCgC6_wmq-GOoZ5vLKt1KHMicJUM9YFZYYKd-7c25X6DLplAnP-Hw_URgRINQdD8kOWzZ_70SiEq0om6OWniva6czSiwrcml_UBDA5Xr8pNtSWqtNbHh1LJzJenVIZl9gPLRs_o-OxB9gylqk7HwQZgKPCbvccYyh162Iy_Kg2j07hnDuoiUyZ93o9x_3Asf8Ms_E_ov6CqpFgKICX6rEE0oOgFO_pKvwtNH8fF-uNkVGKQwNYX6S33SlWh_pULYLSl-YrXVP0hLLmGlunnOGXUIVTXjQcc6AheR8Dmg9jDIefpgHMH6hegAnoZL0_AVuG-yd9LSRSh2qH_rABtJHTOx-0qQ6yYnrzHcMuvatCwDuIePK5DcxBj8KhKq9F4y_i5Ym9drIskRvAzwygZuIIuT3uyXl5nI6YE_jd6F9w4PZ7SkOs9JvfCnt-Wm7UKI6dxLnCRoTarUwop1wDZ77-rRwYoo5zYwF73BragZBZuWNB8ImLlktcAyCBF6P2_F2j4jvnQNLShYZ5HsJKsJNljjIiKYEAeJ2ScT2tjPSfMsdssWQPPByDgwnWtGpx2z6JTFGLUHaj_WbQe3hciyl7jGM2U1JrA610-Jb0X_OiGslZuYBasmPkEXFbDhZy_QZ4Pjs4RddBqrS15-H4FphxsB4knYHtfAzvJno80QmR69zvIfBSIScEx48foHjbeObNpW51IGbg2-yhssa9YtLpjpafnc1-yJ5xj6tJWYZcpskhgADRQvoxF8Xa7BE8o0D9-I7r2Yp0wMfYrbX8NCTBUWczxBZt2juBIERwgjHZzphIGVXNJ6ARm9F12UMf2OwUEk56J6SiSfB1ho7EDdARwj6Nfkm1LjpYLDhii-IRVJUN8tphw6SHVJBbMucYsXsL8viafUwdh7MbBwLKOPgZM4H9BqWFePgEglf7nzrALd2WV40tOai-sm4e4UCKh9bQ1qNw-uHQLP81NNzMA.bMWJdVmxAg2RZm7NE9wTz4H4LwjDb21tFV8hGtTKGFI'; + + public function testEncryptedRealSignature1() { + $this->requestMock->expects($this->any()) + ->method('getHeader') + ->with($this->equalTo(Application::OIDC_API_REQ_HEADER)) + ->willReturn('Bearer ' . self::ENCRPYT1_SIGN_TOKEN); + + $this->assertTrue($this->backend->isSessionActive()); + $this->assertEquals('', $this->backend->getCurrentUserId()); + } + + public const ENCRPYT2_SIGN_TOKEN = 'eyJwMnMiOiJWSTRQS0ZCeVRyUSIsInAyYyI6MTAwMCwiY3R5IjoiSldUIiwiZW5jIjoiQTI1NkNCQy1IUzUxMiIsImFsZyI6IlBCRVMyLUhTNTEyK0EyNTZLVyJ9.YQlaJwr-og6DNQhCkszfsts2z2NLuWsP5czCbMQdyhqjBuhutAvdZlqkFD6el4OeupoXXkTb7XkNyNZVq5S-rfUNGptv27J9.mNCv0KWUDXJoVLxkyppGqg.BdjbqWD14kmuJfLhVMWInuDjTh5O_qxjF9n9rD3viGH1WXZvQtiPT9U2ZKN17jLyzhLXtmvPP_bGZZPrGc5p68WoAteCSxzwJRGcF0hzO6gBhgvx_CcddG0jWcfaXgsFbOeLBpZMKR3w8_6I6shxDcrm0vwL_xeSOd_m4me_VVPQGkaOPKrMy4Ywlh-H7DTquI4NgC1vqt-B7Mpowj82PifFSgEDVrFPkNsustl4PE_2IiL5s_YAPme-OKq50wXzjcjsKAWEbgfsTk5iPoEJNaNWPyWUKiQ8Zp3w6qQgsiY7EGKB5D_-cgbkpq7GmASTiV0FbWHlKleQmHlZ0yJe-WMn0Ai_feVrNwsDM1X5QJ0YMyk5otef-s_64vnLCyo4VbLexO3d67dUqut03xdb9c2SLrupLzpONAJ-nNJ2vNbfr2EBZiSHYjttsmRXlAXgRhiJZIdUGDxBJO-ydEaR22VtPK8pdX9s2Sv8t609xeNQA9hjxCT6IRtEv7vJ0sODV-LSJetO3RKYdBOzNUUvz5VHDE6ogLWNF5blvQ8JoImJd8XP4rNmasassb1NHOPFr4lO7r4ZIn4vmb_idBjzWO2940o48vO5MoRT9gN9rUZDhTwK2enuKdek10PmsVIII5Q18DwvDZhRfM1ZbqZRdkKpnkVb-nWqXChHcSgFcR-TXZGmh3WaH6OJWKpckBAoQ1OHZDl2h_lIfCJ7-eOHR2i3tpXEp6URi31iABcsUZniv8hxB1XYORu9Bl63BQ_t6ns3L-wlMb-LAcvk_sruyObIAuhiZzCyJGxaugje0znGMd3vSXi4U-oqnuGKlKu_1-o7-qB-f1Pkfl6UCk5mS6Vnq-P78FN0iIGaeT8FwsrX-uAFpO8HH4YYEeE8yTi0CQShXVYPiisAQIQFg6QBjy5zEXUZnMBfG-iQ4lfxBJg2sGZ7-HAZpYB2RXDVXAUi4fqI8A1RdHpQofqFGyQZtfVPviOhfNw9Xx79GXb7Cw7viaHFFeyocbyk-55bqjRKpWPP758oxsmP7LZn7yVbMRciCiGDB0LNA1_vJ-7qi9oIUFGdoEW0r3y9I8Su3TH2H2P7HjVaIojOwY4z5_EuADg3lzoSACPvR_I7_r5zMqm7g89HDOo7b-_wh46JVpORbCemQwvJQehN6MUJTbBv_rLKCJ_wjNNMF9sa29yUvoUmEFvlLLy2e2p_r-4AnfGP5P1givxxh12pS_c64XZ1SLqaALTARRwkv1HCnufTNmit80-5rRghgAANf4KXcppXDoMqKW-mrI1Q_ckrkVb7vJuEHaPB1cka5MLIpQ9dFz2iwAEZcFDXXpx2u_ySSDSzRItgazuSOk7DMJzTER_aMTOP2IwzVPoGK8K3RT7wS0lNGfalepX-BAcAbZz4md2PAgHPcfKt1czhdBO5DO9mhKLSSHNA2cc4MmE4_3Ir3BfQCL7mQExvy5mESVr05eTIvLBAzae6SimwzkAUz3o6sxU0neTfxyM47zwQYutOvyC5MCHcA00HdLcyRG9PaE3Bsu5n1WJpIY8i217eFvBZXTIBM-b9vS2_lfC_nNC9DB4N33B2DFEkH02uk9L8vOY90vunGKX-qLXahFOWV_WrFxi_jKzav1FIGV0FcK8QPU8UC9tF9cbxKE1DyLu_G1I9XHP8KO7y9bKOGNv1sRDSUiGZX1_COPM6cifpJsEhOLsucmGsybKg2C77cXhuou9OSen89Devr2ZzWtSZOg1HQdAJuFVkQhjAKcygW49mKqvXsUytRkWEN1mOPsuIJgmt3t4-bxvxeH9qITjy7gR8KYCY5sgdeaIhiEmc2hVp1cBo_HMQNo1E1ew0l8K5X1gavEbUd3RCcRBEtsekwTsfGFoQ6rivH_F5PwAlhMde9jN-I3fnPZMlPnTQEBpb3RdcPV8YNJ7RzRVbQJktdDqb_be1L3BYzKuK8hnv4aEu4Y0wYLRkBxYNIW70X06bIeyCC7B07xn5yLrUaC0MS4UxO9gSPEdauj1OBP7Z_va7zNIbOr4CI68QLfUwtoWpYLPag1exLADeQO3Cdd1qX9LU2trhNVNsw_NapqVkguAI3A3YTuaCQpt68kKGhsugiJ7DsxHuWoNzou4hejBQAvJ1Lm-N38DFKB47gDrwraafpRAezpCyclpaQeYbMK_rz12YCbl35PkFqDefL7B4EESJyk_Wzqpl6Y3AU81rrXK2aVaO0iuVuunWc492tullX_TQ4rtcX_URyZBKz9eF6dxwMJM5UNTtnz7uq-oOmxL3o80XSLpSbfHM4p9elkZGsXfsgpPj0DQJ7EAneLGRqncdLC-6d_ry2E5HwtcC8iWS51CFttDoVyatDDdEWOB7WxD0wy63uc8XK58PPc_ped8W53bid3jB2E5Bg0_c63KQ83U7fezzMtFhUzLIc83FzsG9D4hAPGvZowj3IOAh-E1FlvjvHThse_iH2lIoA1sC9WHpUFx3RkalAaN76fAWP-3xO-bckk9AR3XX1pPxYnx0kOq0a4GR9G7y_ylBt6zGZ0E8TUg8VHS5i834V_rh15R3o8pHncq8b7kwAA--EWCuiLP8B7gTgMqS58r9G89PfZa7u9Wf4NkjoBvZbKzfbnZmPzXkuSLPyC4VBcAp9hZSzdTTd67zLYikGij5dSZ3TRFFG6MSvGDBYvs2P9KaixhcJbY6a7ULGbeBpB29rnq4OEXoGMjOoyG171ZzIeuXAvZnk_ujhEWlCFvznvfQu8H5mTjtFb17I9BJ4YS5gT3E5UwHEH_bAaJI8KtRjfbhKkv09cxaYqRjCMoPlLEPnwDxc2Ousux5SHOjgIqWp9z1acIUzLqkbK3euZNL1YpCNRJTMn4qDPhel5gyY9IjoqgEhfQFJ4ckp2_DLGcFZj3Wwwh-WGmkduvTr2TE_kIA-SmXcqwyGdLse3n7JUHVxcumvXgr5oxe2I_h6UQGSPLxz-KwKxeIUAARQhM9f2mjBcnJ3hkaJj-ciuAjof-WBVCZJsjlccogXhXtxLbjz8ZSntQuaLdjb-ci2wMANhPWnWh9R2KqnREhp-PTllAG4Bj-BWmpzTTRy7tZGkFKoL1xiZMCFA_5egS9V1lqwz62BVOVZ7AeZ5NK8hjGnzSgq6E3bhLoTDupPJLUl3f7fC16PqHQjb049Srme9lK13s8oR79g9UUufW-jQloUhA5fRql45ArveLSTSgg-nUCk22Dso1-Cjk7BIqsEFmeBcyhQoqpjCiuKT6iiVTuEnQXAJ8WEi_hJKTXJ2NxEOdaCG1VaZNycggvX4urmkD53HLpXABitdYpBqJvu-DkO-K8OZA0v8tThBZx4zrIY5EMUPi9YikMrWOqeJtXhA6ZYpeUjK8FHM-sAb3i377lw0CarC8XDzzeNCHRJvaksZdhviuBqNjWXQ_VtU6xEqXsXc8FSftvK2SoSiW19qgiQkrUMJxSy6A_daXT0b7FucBACN1O3YDQ2-x6juM1uMjLico4I1OeFP0RsbUazYVdW0wL6CXiC81ygyTk_XE85xyWwNyiooBuJc377qapNcbUVAYca6R5YVHLVsVLjr3h_BlO1KWv064dypH1faO8cYatSwXp5ttcUg8xoI6E_q0N3IUepfTleZBiCRncoFyKcOT7xUlqojhkC4YirwgtV5Pv3hp6MQ9hjibUeX8mNLFepE1tDFyzZmMXM2kr0Q99WVINbRqv8vGjt82wuZScuJiBy8P6BV-FJLAXsECrAtauSQlDP7YTWsibeqQ3_LEDRd4G9BMj7RorJg6Z0jFloIVfzQOHkZCEZITbh8ifrDrnpMO84l-__kRVImb1rW6I-1KdTubMAaZbAYPhpiYWmC5FJfmyyCSA7uuqeP7RWSm3fZeJK-YinLKH6dUHgwchPQ1godY97ywznP5YuM9pmve75iaNcd3ILuljGx8eBj2Ig7lkPK00JId6FfDwfg9h9cgAKfqueZRBPEN0D3grwZkplG7-_6B1ZhmwjRHaFY88L4EUVnqNh9F73190G-oOuM8Ztw0ItfLU-EvshvMLZ_4W-FUN8B_okqAGH0F088j5ZADxS7HdWMq0DNDIaXpDgPjPhLT7mng20O7BWfG8nTSMEqTBGfvpgoeTL5LjBuDESG4H7FhxGXlfum8asCs8WgdhZ0Zh-SRV8bcLTcpOSEuutdCOK0DxMjs30MTijfLDfpHQP9_fWuG__3n-9g-7Rs6OIaU9jwJ2yWarC-CfPX7yzZcgcsAbT_UEHqRZXQU5vhepV5tmvM5RTv9k7a16b6xIEJIBNLaDRw7LZaauowiaF40vrMNZNGnqqTED_bqMcnfYXvp2R0QFZihNgey1rh2ndhYcSmXSC0F4Wm6r4T6q9VfW_T4Y7NGb31a001Mq_edR2xa_uSBETzybCsHNUq5bD_F3Qj4JUivq2nyh-UAbxP71MdlGE8RN5RYL7b5j25o1oyw5tSYbndIjfp_oVHkdWtnYJsH6T131lUwM0-DwMWWtLParbukDjDjy08aTEDR0vW6LaJJ9bh1_Po-XR6sG4lAeTcJo7XjptIWQCbkSrV6gD7GXOOJgF2qVlvM02ARNLl6DNo3Y7ar_H4LkZ3aAkkV1Yy7-vnVpIEx-UoSnilNRQN_rp6icTwNilt1UnuuLutxKISHRMDP3Pv9vEATDQy-z.w6KkNgIIeh8SPlMtA6l7dbywsDAKFLkTmrVc65q-BL8'; + + public function testEncryptedRealSignature2() { + $this->requestMock->expects($this->any()) + ->method('getHeader') + ->with($this->equalTo(Application::OIDC_API_REQ_HEADER)) + ->willReturn('Bearer ' . self::ENCRPYT2_SIGN_TOKEN); + + $this->assertTrue($this->backend->isSessionActive()); + $this->assertEquals('', $this->backend->getCurrentUserId()); + } +} diff --git a/tests/unit/MagentaCloud/ProvisioningEventServiceTest.php b/tests/unit/MagentaCloud/ProvisioningEventServiceTest.php new file mode 100644 index 00000000..85ac4d2a --- /dev/null +++ b/tests/unit/MagentaCloud/ProvisioningEventServiceTest.php @@ -0,0 +1,481 @@ + + * + * @license GNU AGPL version 3 or any later version + * + */ + +declare(strict_types=1); + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\Authentication\Token\IProvider; +use OC\Security\Crypto; +use OCA\UserOIDC\AppInfo\Application; +use OCA\UserOIDC\BaseTest\OpenidTokenTestCase; +use OCA\UserOIDC\Controller\LoginController; +use OCA\UserOIDC\Db\Provider; +use OCA\UserOIDC\Db\ProviderMapper; +use OCA\UserOIDC\Db\SessionMapper; +use OCA\UserOIDC\Db\UserMapper; +use OCA\UserOIDC\Event\AttributeMappedEvent; +use OCA\UserOIDC\Event\UserAccountChangeEvent; +use OCA\UserOIDC\Service\DiscoveryService; +use OCA\UserOIDC\Service\LdapService; +use OCA\UserOIDC\Service\LocalIdService; +use OCA\UserOIDC\Service\ProviderService; +use OCA\UserOIDC\Service\ProvisioningEventService; +use OCP\Accounts\IAccountManager; +use OCP\AppFramework\App; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\IResponse; +use OCP\IAvatarManager; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IL10N; // deprecated! +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; + + +use OCP\Security\ISecureRandom; + +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +class ProvisioningEventServiceTest extends OpenidTokenTestCase { + /** + * Set up needed system and app configurations + */ + protected function getConfigSetup() :MockObject { + $config = $this->getMockForAbstractClass(IConfig::class); + + $config->expects($this->any()) + ->method('getSystemValue') + ->with($this->logicalOr($this->equalTo('user_oidc'), $this->equalTo('secret'))) + ->willReturn($this->returnCallback(function ($key, $default) { + if ($key == 'user_oidc') { + return [ + 'auto_provisioning' => true, + ]; + } elseif ($key == 'secret') { + return 'Streng_geheim'; + } + })); + return $config; + } + + /** + * Prepare a proper session as if the handshake with an + * OpenID authenticator entity has already been done. + */ + protected function getOidSessionSetup() :MockObject { + $session = $this->getMockForAbstractClass(ISession::class); + + $session->expects($this->any()) + ->method('get') + ->willReturn($this->returnCallback(function ($key) { + $values = [ + 'oidc.state' => $this->getOidTestState(), + 'oidc.providerid' => $this->getProviderId(), + 'oidc.nonce' => $this->getOidNonce(), + 'oidc.redirect' => 'https://welcome.to.magenta' + ]; + + return $values[$key] ? $values[$key] : 'some_' . $key; + })); + $this->sessionMapper = $this->getMockBuilder(SessionMapper::class) + ->setConstructorArgs([ $this->getMockForAbstractClass(IDBConnection::class) ]) + ->getMock(); + $this->sessionMapper->expects($this->any()) + ->method('createSession'); + + return $session; + } + + /** + * Prepare a proper session as if the handshake with an + * OpenID authenticator entity has already been done. + */ + protected function getProviderSetup() :MockObject { + $provider = $this->getMockBuilder(Provider::class) + ->addMethods(['getClientId', 'getClientSecret']) + ->getMock(); + $provider->expects($this->any()) + ->method('getClientId') + ->willReturn($this->getOidClientId()); + $provider->expects($this->once()) + ->method('getClientSecret') + ->willReturn($this->crypto->encrypt($this->getOidClientSecret())); + $this->providerMapper->expects($this->once()) + ->method('getProvider') + ->with($this->equalTo($this->getProviderId())) + ->willReturn($provider); + + return $provider; + } + + + /** + * Prepare a proper mapping configuration for the provider + */ + protected function getProviderServiceSetup() :MockObject { + $providerService = $this->getMockBuilder(ProviderService::class) + ->setConstructorArgs([ $this->config, $this->providerMapper]) + ->getMock(); + $providerService->expects($this->any()) + ->method('getSetting') + ->with($this->equalTo($this->getProviderId()), $this->logicalOr( + $this->equalTo(ProviderService::SETTING_MAPPING_UID), + $this->equalTo(ProviderService::SETTING_MAPPING_DISPLAYNAME), + $this->equalTo(ProviderService::SETTING_MAPPING_QUOTA), + $this->equalTo(ProviderService::SETTING_MAPPING_EMAIL), + $this->anything())) + ->will($this->returnCallback(function ($providerid, $key, $default):string { + $values = [ + ProviderService::SETTING_MAPPING_UID => 'sub', + ProviderService::SETTING_MAPPING_DISPLAYNAME => 'urn:custom.com:displayname', + ProviderService::SETTING_MAPPING_QUOTA => 'urn:custom.com:f556', + ProviderService::SETTING_MAPPING_EMAIL => 'urn:custom.com:mainEmail' + ]; + return $values[$key]; + })); + return $providerService; + } + + /** + * Prepare a proper session as if the handshake with an + * OpenID authenticator entity has already been done. + */ + protected function getUserManagerSetup() :MockObject { + $userManager = $this->getMockForAbstractClass(IUserManager::class); + $this->user = $this->getMockForAbstractClass(IUser::class); + $this->user->expects($this->any()) + ->method('canChangeAvatar') + ->willReturn(false); + + return $userManager; + } + + + /** + * This is the standard execution sequence until provisoning + * is triggered in LoginController, set up with an artificial + * yet valid OpenID token. + */ + public function setUp(): void { + parent::setUp(); + + $this->app = new App(Application::APP_ID); + $this->config = $this->getConfigSetup(); + $this->crypto = $this->getMockBuilder(Crypto::class) + ->setConstructorArgs([ $this->config ]) + ->getMock(); + + $this->request = $this->getMockForAbstractClass(IRequest::class); + $this->request->expects($this->once()) + ->method('getServerProtocol') + ->willReturn('https'); + $this->providerMapper = $this->getMockBuilder(ProviderMapper::class) + ->setConstructorArgs([ $this->getMockForAbstractClass(IDBConnection::class) ]) + ->getMock(); + $this->provider = $this->getProviderSetup(); + $this->providerService = $this->getProviderServiceSetup(); + $this->localIdService = $this->getMockBuilder(LocalIdService::class) + ->setConstructorArgs([ $this->providerService, + $this->providerMapper]) + ->getMock(); + $this->userMapper = $this->getMockBuilder(UserMapper::class) + ->setConstructorArgs([ $this->getMockForAbstractClass(IDBConnection::class), + $this->localIdService ]) + ->getMock(); + $this->discoveryService = $this->getMockBuilder(DiscoveryService::class) + ->setConstructorArgs([ $this->app->getContainer()->get(LoggerInterface::class), + $this->getMockForAbstractClass(IClientService::class), + $this->providerService, + $this->app->getContainer()->get(ICacheFactory::class) ]) + ->getMock(); + $this->discoveryService->expects($this->once()) + ->method('obtainDiscovery') + ->willReturn([ 'token_endpoint' => 'https://whatever.to.discover/token', + 'issuer' => 'https:\/\/accounts.login00.custom.de' ]); + $this->discoveryService->expects($this->once()) + ->method('obtainJWK') + ->willReturn($this->getOidPublicServerKey()); + $this->session = $this->getOidSessionSetup(); + $this->client = $this->getMockForAbstractClass(IClient::class); + $this->response = $this->getMockForAbstractClass(IResponse::class); + //$this->usersession = $this->getMockForAbstractClass(IUserSession::class); + $this->usersession = $this->getMockBuilder(IUserSession::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'setUser', + 'login', + 'logout', + 'getUser', + 'isLoggedIn', + 'getImpersonatingUserID', + 'setImpersonatingUserID', + 'setVolatileActiveUser' // Diese Methode hinzufügen, falls sie gebraucht wird. + ]) + ->addMethods([ + 'completeLogin', + 'createSessionToken', + 'createRememberMeToken' + ]) + ->getMock(); + + $this->usermanager = $this->getUserManagerSetup(); + $this->groupmanager = $this->getMockForAbstractClass(IGroupManager::class); + $this->dispatcher = $this->app->getContainer()->get(IEventDispatcher::class); + + $this->provisioningService = new ProvisioningEventService( + $this->app->getContainer()->get(LocalIdService::class), + $this->providerService, + $this->userMapper, + $this->usermanager, + $this->groupmanager, + $this->dispatcher, + $this->app->getContainer()->get(LoggerInterface::class), + $this->app->getContainer()->get(IAccountManager::class), + $this->app->getContainer()->get(IClientService::class), + $this->app->getContainer()->get(IAvatarManager::class), + $this->app->getContainer()->get(IConfig::class)); + // here is where the token magic comes in + $this->token = [ 'id_token' => + $this->createSignToken($this->getRealOidClaims(), + $this->getOidServerKey())]; + $this->tokenResponse = $this->getMockForAbstractClass(IResponse::class); + $this->tokenResponse->expects($this->once()) + ->method('getBody') + ->willReturn(json_encode($this->token)); + + // mock token retrieval + $this->client = $this->getMockForAbstractClass(IClient::class); + $this->client->expects($this->once()) + ->method('post') + ->with($this->equalTo('https://whatever.to.discover/token'), $this->arrayHasKey('body')) + ->willReturn($this->tokenResponse); + $this->clientService = $this->getMockForAbstractClass(IClientService::class); + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($this->client); + $this->registrationContext = + $this->app->getContainer()->get(Coordinator::class)->getRegistrationContext(); + $this->loginController = new LoginController($this->request, + $this->providerMapper, + $this->providerService, + $this->discoveryService, + $this->app->getContainer()->get(LdapService::class), + $this->app->getContainer()->get(ISecureRandom::class), + $this->session, + $this->clientService, + $this->app->getContainer()->get(IUrlGenerator::class), + $this->usersession, + $this->usermanager, + $this->app->getContainer()->get(ITimeFactory::class), + $this->dispatcher, + $this->config, + $this->app->getContainer()->get(IProvider::class), + $this->sessionMapper, + $this->provisioningService, + $this->app->getContainer()->get(IL10N::class), + $this->app->getContainer()->get(LoggerInterface::class), + $this->crypto); + + $this->attributeListener = null; + $this->accountListener = null; + } + + /** + * Seems like the event dispatcher requires explicit unregistering + */ + public function tearDown(): void { + parent::tearDown(); + if ($this->accountListener != null) { + $this->dispatcher->removeListener(UserAccountChangeEvent::class, $this->accountListener); + } + if ($this->attributeListener != null) { + $this->dispatcher->removeListener(AttributeMappedEvent::class, $this->attributeListener); + } + } + + protected function mockAssertLoginSuccess() { + $this->usermanager->expects($this->once()) + ->method('get') + ->willReturn($this->user); + $this->session->expects($this->exactly(2)) + ->method('set') + ->withConsecutive([$this->equalTo('oidc.id_token'), $this->anything()], + [$this->equalTo('last-password-confirm'), $this->anything()]); + $this->usersession->expects($this->once()) + ->method('setUser') + ->with($this->equalTo($this->user)); + $this->usersession->expects($this->once()) + ->method('completeLogin') + ->with($this->anything(), $this->anything()); + $this->usersession->expects($this->once()) + ->method('createSessionToken'); + $this->usersession->expects($this->once()) + ->method('createRememberMeToken'); + } + + protected function assertLoginRedirect($result) { + $this->assertInstanceOf(RedirectResponse::class, + $result, 'LoginController->code() did not end with success redirect: Status: ' . + strval($result->getStatus() . ' ' . json_encode($result->getThrottleMetadata()))); + } + + protected function assertLogin403($result) { + $this->assertInstanceOf(TemplateResponse::class, + $result, 'LoginController->code() did not end with 403 Forbidden: Actual status: ' . + strval($result->getStatus() . ' ' . json_encode($result->getThrottleMetadata()))); + } + + /** + * Test with the default mapping, no mapping by attribute events + * provisioning with successful result. + */ + public function testNoMap_AccessOk() { + $this->mockAssertLoginSuccess(); + $this->accountListener = function (Event $event) :void { + $this->assertInstanceOf(UserAccountChangeEvent::class, $event); + $this->assertEquals('jgyros', $event->getUid()); + $this->assertEquals('Jonny G', $event->getDisplayname()); + $this->assertEquals('jonny.gyuris@x.y.de', $event->getMainEmail()); + $this->assertNull($event->getQuota()); + $event->setResult(true, 'ok', null); + }; + + $this->dispatcher->addListener(UserAccountChangeEvent::class, $this->accountListener); + $result = $this->loginController->code($this->getOidTestState(), $this->getOidTestCode(), ''); + + $this->assertLoginRedirect($result); + $this->assertEquals('https://welcome.to.magenta', $result->getRedirectURL()); + } + + /** + * For multiple reasons, uid should com directly from a token + * field, usually sub. Thus, uid is not remapped by event, even + * if you try with a listener. + */ + public function testUidNoMapEvent_AccessOk() { + $this->mockAssertLoginSuccess(); + $this->attributeListener = function (Event $event): void { + if ($event instanceof AttributeMappedEvent && + $event->getAttribute() == ProviderService::SETTING_MAPPING_UID) { + $this->fail('UID event mapping not supported'); + } + }; + $this->accountListener = function (Event $event) :void { + $this->assertInstanceOf(UserAccountChangeEvent::class, $event); + $this->assertEquals('jgyros', $event->getUid()); + $this->assertEquals('Jonny G', $event->getDisplayname()); + $this->assertEquals('jonny.gyuris@x.y.de', $event->getMainEmail()); + $this->assertNull($event->getQuota()); + $event->setResult(true, 'ok', 'https://welcome.to.darkside'); + }; + + $this->dispatcher->addListener(AttributeMappedEvent::class, $this->attributeListener); + $this->dispatcher->addListener(UserAccountChangeEvent::class, $this->accountListener); + $result = $this->loginController->code($this->getOidTestState(), $this->getOidTestCode(), ''); + + $this->assertLoginRedirect($result); + $this->assertEquals('https://welcome.to.magenta', $result->getRedirectURL()); + } + + + + /** + * Test displayname set by event scheduling and negative result + */ + public function testDisplaynameMapEvent_NOk_NoRedirect() { + $this->attributeListener = function (Event $event): void { + if ($event instanceof AttributeMappedEvent && + $event->getAttribute() == ProviderService::SETTING_MAPPING_DISPLAYNAME) { + $event->setValue('Lisa, Mona'); + } + }; + $this->accountListener = function (Event $event) :void { + $this->assertInstanceOf(UserAccountChangeEvent::class, $event); + $this->assertEquals('jgyros', $event->getUid()); + $this->assertEquals('Lisa, Mona', $event->getDisplayname()); + $this->assertEquals('jonny.gyuris@x.y.de', $event->getMainEmail()); + $this->assertNull($event->getQuota()); + $event->setResult(false, 'not an original', null); + }; + $this->dispatcher->addListener(AttributeMappedEvent::class, $this->attributeListener); + $this->dispatcher->addListener(UserAccountChangeEvent::class, $this->accountListener); + $result = $this->loginController->code($this->getOidTestState(), $this->getOidTestCode(), ''); + + $this->assertLogin403($result); + } + + public function testMainEmailMap_Nok_Redirect() { + $this->attributeListener = function (Event $event): void { + if ($event instanceof AttributeMappedEvent && + $event->getAttribute() == ProviderService::SETTING_MAPPING_EMAIL) { + //$defaultUID = $event->getValue(); + $event->setValue('mona.lisa@louvre.fr'); + } + }; + + $this->accountListener = function (Event $event) :void { + $this->assertInstanceOf(UserAccountChangeEvent::class, $event); + $this->assertEquals('jgyros', $event->getUid()); + $this->assertEquals('Jonny G', $event->getDisplayname()); + $this->assertEquals('mona.lisa@louvre.fr', $event->getMainEmail()); + $this->assertNull($event->getQuota()); + $event->setResult(false, 'under restoration', 'https://welcome.to.louvre'); + }; + $this->dispatcher->addListener(AttributeMappedEvent::class, $this->attributeListener); + $this->dispatcher->addListener(UserAccountChangeEvent::class, $this->accountListener); + $result = $this->loginController->code($this->getOidTestState(), $this->getOidTestCode(), ''); + + $this->assertLoginRedirect($result); + $this->assertEquals('https://welcome.to.louvre', $result->getRedirectURL()); + } + + public function testDisplaynameUidQuotaMapped_AccessOK() { + $this->mockAssertLoginSuccess(); + $this->attributeListener = function (Event $event): void { + if ($event instanceof AttributeMappedEvent) { + if ($event->getAttribute() == ProviderService::SETTING_MAPPING_UID) { + $this->fail('UID event mapping not supported'); + } elseif ($event->getAttribute() == ProviderService::SETTING_MAPPING_DISPLAYNAME) { + $event->setValue('Lisa, Mona'); + } elseif ($event->getAttribute() == ProviderService::SETTING_MAPPING_QUOTA) { + $event->setValue('5 TB'); + } + } + }; + $this->accountListener = function (Event $event) :void { + $this->assertInstanceOf(UserAccountChangeEvent::class, $event); + $this->assertEquals('jgyros', $event->getUid()); + $this->assertEquals('Lisa, Mona', $event->getDisplayname()); + $this->assertEquals('jonny.gyuris@x.y.de', $event->getMainEmail()); + $this->assertEquals('5 TB', $event->getQuota()); + $event->setResult(true, 'ok', 'https://welcome.to.louvre'); + }; + + $this->dispatcher->addListener(AttributeMappedEvent::class, $this->attributeListener); + $this->dispatcher->addListener(UserAccountChangeEvent::class, $this->accountListener); + $result = $this->loginController->code($this->getOidTestState(), $this->getOidTestCode(), ''); + + $this->assertLoginRedirect($result); + $this->assertEquals('https://welcome.to.magenta', $result->getRedirectURL()); + } +} diff --git a/tests/unit/MagentaCloud/RegistrationsTest.php b/tests/unit/MagentaCloud/RegistrationsTest.php new file mode 100644 index 00000000..eb714da9 --- /dev/null +++ b/tests/unit/MagentaCloud/RegistrationsTest.php @@ -0,0 +1,33 @@ + + * + * @license GNU AGPL version 3 or any later version + * + */ + +declare(strict_types=1); + +use OC\AppFramework\Bootstrap\Coordinator; +use OCA\UserOIDC\AppInfo\Application; +use OCA\UserOIDC\Service\ProvisioningEventService; +use OCA\UserOIDC\Service\ProvisioningService; + +use PHPUnit\Framework\TestCase; + +class RegistrationsTest extends TestCase { + public function setUp() :void { + parent::setUp(); + + $this->app = new Application(); + $coordinator = \OC::$server->get(Coordinator::class); + $this->app->register($coordinator->getRegistrationContext()->for('user_oidc')); + } + + public function testRegistration() :void { + $provisioningService = $this->app->getContainer()->get(ProvisioningService::class); + $this->assertInstanceOf(ProvisioningEventService::class, $provisioningService); + } +} diff --git a/tests/unit/MagentaCloud/SamBearerTokenTest.php b/tests/unit/MagentaCloud/SamBearerTokenTest.php new file mode 100644 index 00000000..0c87a33f --- /dev/null +++ b/tests/unit/MagentaCloud/SamBearerTokenTest.php @@ -0,0 +1,85 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + +use OCA\UserOIDC\BaseTest\BearerTokenTestCase; + + +use OCA\UserOIDC\MagentaBearer\InvalidTokenException; +use OCA\UserOIDC\MagentaBearer\SignatureException; + +class SamBearerTokenTest extends BearerTokenTestCase { + + /** + * @var ProviderService + */ + private $provider; + + + public function setUp(): void { + parent::setUp(); + } + + public function testValidSignature() { + $this->expectNotToPerformAssertions(); + $testtoken = $this->setupSignedToken($this->getRealExampleClaims(), $this->getTestBearerSecret()); + //fwrite(STDERR, '[' . $testtoken . ']'); + $bearerToken = $this->tokenService->decryptToken($testtoken, $this->getTestBearerSecret()); + $this->tokenService->verifySignature($bearerToken, $this->getTestBearerSecret()); + $claims = $this->tokenService->decode($bearerToken); + $this->tokenService->verifyClaims($claims, ['http://auth.magentacloud.de']); + } + + public function testInvalidSignature() { + $this->expectException(SignatureException::class); + $testtoken = $this->setupSignedToken($this->getRealExampleClaims(), $this->getTestBearerSecret()); + $invalidSignToken = mb_substr($testtoken, 0, -1); // shorten sign to invalidate + // fwrite(STDERR, '[' . $testtoken . ']'); + $bearerToken = $this->tokenService->decryptToken($invalidSignToken, $this->getTestBearerSecret()); + $this->tokenService->verifySignature($bearerToken, $this->getTestBearerSecret()); + $claims = $this->tokenService->decode($bearerToken); + $this->tokenService->verifyClaims($claims, ['http://auth.magentacloud.de']); + } + + public function testEncryptedValidSignature() { + $this->expectNotToPerformAssertions(); + $testtoken = $this->setupSignEncryptToken($this->getRealExampleClaims(), $this->getTestBearerSecret()); + //fwrite(STDERR, '[' . $testtoken . ']'); + $bearerToken = $this->tokenService->decryptToken($testtoken, $this->getTestBearerSecret()); + $this->tokenService->verifySignature($bearerToken, $this->getTestBearerSecret()); + $claims = $this->tokenService->decode($bearerToken); + $this->tokenService->verifyClaims($claims, ['http://auth.magentacloud.de']); + } + + public function testEncryptedInvalidEncryption() { + $this->expectException(InvalidTokenException::class); + $testtoken = $this->setupSignEncryptToken($this->getRealExampleClaims(), $this->getTestBearerSecret()); + $invalidEncryption = mb_substr($testtoken, 0, -1); // shorten sign to invalidate + //fwrite(STDERR, '[' . $testtoken . ']'); + $bearerToken = $this->tokenService->decryptToken($invalidEncryption, $this->getTestBearerSecret()); + $this->tokenService->verifySignature($bearerToken, $this->getTestBearerSecret()); + $claims = $this->tokenService->decode($bearerToken); + $this->tokenService->verifyClaims($claims, ['http://auth.magentacloud.de']); + } +}