From 56fb1cba9601fdbc19a6796cf4eddf1e85b5fb08 Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Mon, 10 Jul 2023 17:09:32 +0200
Subject: [PATCH 01/33] Override upstream Readme in .github
---
.github/README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 56 insertions(+)
create mode 100644 .github/README.md
diff --git a/.github/README.md b/.github/README.md
new file mode 100644
index 00000000..76c7b0ac
--- /dev/null
+++ b/.github/README.md
@@ -0,0 +1,56 @@
+# user_oidc: Customisation of OpenID 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/README.md)
+
+The app is extended by the following features
+
+## 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
+
+## 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)
+
+# update web-token dependencies in composer.lock
+composer update web-token
+```
+It is recommended to leave the version management for all other libraries to upstream
+and only update web-token with the dedicated `composer update web-token`.
+
+
+### Configuring an additional Bearer preshared secret with provider
+TODO
+
+### Testing Bearer secrets
+TODO
\ No newline at end of file
From 0d4fb92a291d481cf1291de42bc36c48965c8ea6 Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Mon, 10 Jul 2023 17:18:04 +0200
Subject: [PATCH 02/33] Correct some wordings
---
.github/README.md | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/.github/README.md b/.github/README.md
index 76c7b0ac..cc522ede 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -1,9 +1,11 @@
-# user_oidc: Customisation of OpenID app for MagentaCLOUD
+# 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/README.md)
+see [upstream configuration hints for basic setup](https://github.com/nextcloud/user_oidc/blob/main/README.md)
-The app is extended by the following features
+The app is extended by the following features:
## Event-based provisioning (upstream contribution candidate)
The mechanism allows to implement custom puser provisioning logic in a separate Nextcloud app by
From cf631b60067cdd8d4c33de20f4b28f894a28f2f0 Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Tue, 11 Jul 2023 18:06:01 +0200
Subject: [PATCH 03/33] Add automatic phpunit run after assembly
---
.github/workflows/nmc-custom-phpunit.yml | 39 ++++++++++++++++++++
.github/workflows/nmc-custom-versions.yml | 43 +++++++++++++++++++++++
2 files changed, 82 insertions(+)
create mode 100644 .github/workflows/nmc-custom-phpunit.yml
create mode 100644 .github/workflows/nmc-custom-versions.yml
diff --git a/.github/workflows/nmc-custom-phpunit.yml b/.github/workflows/nmc-custom-phpunit.yml
new file mode 100644
index 00000000..ab230b4b
--- /dev/null
+++ b/.github/workflows/nmc-custom-phpunit.yml
@@ -0,0 +1,39 @@
+###
+# 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 phpunit (customisation change)
+
+###
+# The automated unittets cycles are started as soon as a new
+# customisation branch is pushed
+on:
+ push:
+ branches:
+ - customisation-*master
+ - customisation-*nmcstable/**
+
+jobs:
+ build-custom:
+ strategy:
+ fail-fast: false
+ matrix:
+ phpversion: ['8.0', '8.1']
+ database: ['mysql']
+ custombase: [ "main", "nmcstable/25.0.6" ]
+ uses: nextmcloud/.github/.github/workflows/nmc-custom-app-phpunit.yml@master
+ 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-versions.yml b/.github/workflows/nmc-custom-versions.yml
new file mode 100644
index 00000000..63579131
--- /dev/null
+++ b/.github/workflows/nmc-custom-versions.yml
@@ -0,0 +1,43 @@
+###
+# 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:
+ build-custom:
+ strategy:
+ fail-fast: false
+ matrix:
+ custombase: [ "main", "nmcstable/25.0.6" ]
+ 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
From 38ef829285f314bebf9f400342c9a96a4cb3d71d Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Tue, 11 Jul 2023 18:19:30 +0200
Subject: [PATCH 04/33] Correct trunk name for customisation branch
---
.github/workflows/nmc-custom-phpunit.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/nmc-custom-phpunit.yml b/.github/workflows/nmc-custom-phpunit.yml
index ab230b4b..76e04740 100644
--- a/.github/workflows/nmc-custom-phpunit.yml
+++ b/.github/workflows/nmc-custom-phpunit.yml
@@ -18,8 +18,8 @@ name: MCLOUD phpunit (customisation change)
on:
push:
branches:
- - customisation-*master
- - customisation-*nmcstable/**
+ - customisation-*-main
+ - customisation-*-nmcstable/**
jobs:
build-custom:
From b46a89714236dc11c451ace1a7b803774dc64a78 Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Tue, 11 Jul 2023 18:37:51 +0200
Subject: [PATCH 05/33] Debug scheduling on push
---
.github/workflows/nmc-custom-phpunit.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/nmc-custom-phpunit.yml b/.github/workflows/nmc-custom-phpunit.yml
index 76e04740..1d5426f3 100644
--- a/.github/workflows/nmc-custom-phpunit.yml
+++ b/.github/workflows/nmc-custom-phpunit.yml
@@ -18,8 +18,8 @@ name: MCLOUD phpunit (customisation change)
on:
push:
branches:
- - customisation-*-main
- - customisation-*-nmcstable/**
+ - 'customisation*-main'
+ - 'customisation*-nmcstable/25.0.6'
jobs:
build-custom:
From b93100d243d7ff0b16929d816934b8c0352107e1 Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Wed, 12 Jul 2023 08:50:24 +0200
Subject: [PATCH 06/33] Include phpunit in versions assembling
---
.github/workflows/nmc-custom-phpunit.yml | 39 -----------------------
.github/workflows/nmc-custom-versions.yml | 20 +++++++++++-
2 files changed, 19 insertions(+), 40 deletions(-)
delete mode 100644 .github/workflows/nmc-custom-phpunit.yml
diff --git a/.github/workflows/nmc-custom-phpunit.yml b/.github/workflows/nmc-custom-phpunit.yml
deleted file mode 100644
index 1d5426f3..00000000
--- a/.github/workflows/nmc-custom-phpunit.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-###
-# 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 phpunit (customisation change)
-
-###
-# The automated unittets cycles are started as soon as a new
-# customisation branch is pushed
-on:
- push:
- branches:
- - 'customisation*-main'
- - 'customisation*-nmcstable/25.0.6'
-
-jobs:
- build-custom:
- strategy:
- fail-fast: false
- matrix:
- phpversion: ['8.0', '8.1']
- database: ['mysql']
- custombase: [ "main", "nmcstable/25.0.6" ]
- uses: nextmcloud/.github/.github/workflows/nmc-custom-app-phpunit.yml@master
- 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-versions.yml b/.github/workflows/nmc-custom-versions.yml
index 63579131..6b0c3eb6 100644
--- a/.github/workflows/nmc-custom-versions.yml
+++ b/.github/workflows/nmc-custom-versions.yml
@@ -30,7 +30,8 @@ on:
# - stable/**
jobs:
- build-custom:
+
+ assemble:
strategy:
fail-fast: false
matrix:
@@ -41,3 +42,20 @@ jobs:
stable: ${{ matrix.custombase }}
result: ${{ 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", "nmcstable/25.0.6" ]
+ uses: nextmcloud/.github/.github/workflows/nmc-custom-app-phpunit.yml@master
+ need: assemble
+ 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
From e5d81f9fc6e8c77caf6a6479de6fbaab2a1f6450 Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Wed, 12 Jul 2023 09:08:29 +0200
Subject: [PATCH 07/33] Fix syntax
---
.github/workflows/nmc-custom-versions.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/nmc-custom-versions.yml b/.github/workflows/nmc-custom-versions.yml
index 6b0c3eb6..ff976790 100644
--- a/.github/workflows/nmc-custom-versions.yml
+++ b/.github/workflows/nmc-custom-versions.yml
@@ -57,5 +57,5 @@ jobs:
appname: 'user_oidc'
server-branch: ${{ matrix.custombase }}
phpversion: ${{ matrix.phpversion }}
- database: ${{ matrix.database }}
+ database: ${{ matrix.database }}
secrets: inherit
\ No newline at end of file
From 93e7995592302747a87c85cf1eb67ea9bb46d741 Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Wed, 12 Jul 2023 09:16:58 +0200
Subject: [PATCH 08/33] Fix needs syntax
---
.github/workflows/nmc-custom-versions.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/nmc-custom-versions.yml b/.github/workflows/nmc-custom-versions.yml
index ff976790..7fa8a832 100644
--- a/.github/workflows/nmc-custom-versions.yml
+++ b/.github/workflows/nmc-custom-versions.yml
@@ -51,7 +51,7 @@ jobs:
database: ['mysql']
custombase: [ "main", "nmcstable/25.0.6" ]
uses: nextmcloud/.github/.github/workflows/nmc-custom-app-phpunit.yml@master
- need: assemble
+ needs: assemble
with:
assembly: ${{ format('customisation-{0}-{1}', github.actor, matrix.custombase) }}
appname: 'user_oidc'
From dc53689e901bb3d1f95c2c22d77e52f851cfb843 Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Wed, 12 Jul 2023 12:32:57 +0200
Subject: [PATCH 09/33] Move readme to central setup
---
.github/README.md | 58 -----------------------------------------------
1 file changed, 58 deletions(-)
delete mode 100644 .github/README.md
diff --git a/.github/README.md b/.github/README.md
deleted file mode 100644
index cc522ede..00000000
--- a/.github/README.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# 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)
-
-The app is extended by the following features:
-
-## 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
-
-## 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)
-
-# update web-token dependencies in composer.lock
-composer update web-token
-```
-It is recommended to leave the version management for all other libraries to upstream
-and only update web-token with the dedicated `composer update web-token`.
-
-
-### Configuring an additional Bearer preshared secret with provider
-TODO
-
-### Testing Bearer secrets
-TODO
\ No newline at end of file
From 3fc8495d557cc18cfd9c37f163cfe62d6e685e7c Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Sat, 19 Aug 2023 08:54:14 +0200
Subject: [PATCH 10/33] Refactor for working fast-fail precheck
---
.github/workflows/nmc-custom-release.yml | 58 ++++++++++++++++++++++++
1 file changed, 58 insertions(+)
create mode 100644 .github/workflows/nmc-custom-release.yml
diff --git a/.github/workflows/nmc-custom-release.yml b/.github/workflows/nmc-custom-release.yml
new file mode 100644
index 00000000..5962a538
--- /dev/null
+++ b/.github/workflows/nmc-custom-release.yml
@@ -0,0 +1,58 @@
+###
+# 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
+ build-custom:
+ uses: nextmcloud/.github/.github/workflows/nmc-custom-app-build.yml@master
+ needs: [ check-custom, assemble-custom ]
+ 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
From d41baa1ebeb195c524d818332a4d5f050401a092 Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Fri, 1 Sep 2023 12:37:19 +0200
Subject: [PATCH 11/33] Remove obsolete stable versions base in matrix
---
.github/workflows/nmc-custom-versions.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/nmc-custom-versions.yml b/.github/workflows/nmc-custom-versions.yml
index 7fa8a832..6d262e27 100644
--- a/.github/workflows/nmc-custom-versions.yml
+++ b/.github/workflows/nmc-custom-versions.yml
@@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- custombase: [ "main", "nmcstable/25.0.6" ]
+ custombase: [ "main" ]
uses: nextmcloud/.github/.github/workflows/nmc-custom-assembly.yml@master
with:
trunk: "main"
From d525678a033b18c30214c9b963681e604ddf6d93 Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Fri, 1 Sep 2023 19:19:41 +0200
Subject: [PATCH 12/33] Add required composer dependencies programmatically
---
...release.yml => nmc-custom-app-release.yml} | 14 +++-
...rsions.yml => nmc-custom-app-versions.yml} | 15 +++-
.../workflows/nmc-custom-oidc-composer.yml | 82 +++++++++++++++++++
3 files changed, 108 insertions(+), 3 deletions(-)
rename .github/workflows/{nmc-custom-release.yml => nmc-custom-app-release.yml} (83%)
rename .github/workflows/{nmc-custom-versions.yml => nmc-custom-app-versions.yml} (82%)
create mode 100644 .github/workflows/nmc-custom-oidc-composer.yml
diff --git a/.github/workflows/nmc-custom-release.yml b/.github/workflows/nmc-custom-app-release.yml
similarity index 83%
rename from .github/workflows/nmc-custom-release.yml
rename to .github/workflows/nmc-custom-app-release.yml
index 5962a538..b34abf18 100644
--- a/.github/workflows/nmc-custom-release.yml
+++ b/.github/workflows/nmc-custom-app-release.yml
@@ -47,9 +47,21 @@ jobs:
stable: ${{ inputs.branch }}
result: ${{ format('customisation-{0}-{1}', inputs.branch, inputs.increment ) }}
secrets: inherit
+
+ composerdep:
+ strategy:
+ fail-fast: false
+ matrix:
+ custombase: [ "main" ]
+ uses: ./.github/workflows/nmc-custom-oidc-composer.yml
+ needs: assemble-custom
+ with:
+ assembly: ${{ format('customisation-{0}-{1}', github.actor, matrix.custombase) }}
+ secrets: inherit
+
build-custom:
uses: nextmcloud/.github/.github/workflows/nmc-custom-app-build.yml@master
- needs: [ check-custom, assemble-custom ]
+ needs: [ check-custom, composerdep ]
with:
appname: ${{ needs.check-custom.outputs.appname }}
assembly: ${{ format('customisation-{0}-{1}', inputs.branch , inputs.increment ) }}
diff --git a/.github/workflows/nmc-custom-versions.yml b/.github/workflows/nmc-custom-app-versions.yml
similarity index 82%
rename from .github/workflows/nmc-custom-versions.yml
rename to .github/workflows/nmc-custom-app-versions.yml
index 6d262e27..e18f1941 100644
--- a/.github/workflows/nmc-custom-versions.yml
+++ b/.github/workflows/nmc-custom-app-versions.yml
@@ -43,15 +43,26 @@ jobs:
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", "nmcstable/25.0.6" ]
+ custombase: [ "main" ]
uses: nextmcloud/.github/.github/workflows/nmc-custom-app-phpunit.yml@master
- needs: assemble
+ needs: composerdep
with:
assembly: ${{ format('customisation-{0}-{1}', github.actor, matrix.custombase) }}
appname: 'user_oidc'
diff --git a/.github/workflows/nmc-custom-oidc-composer.yml b/.github/workflows/nmc-custom-oidc-composer.yml
new file mode 100644
index 00000000..474b3257
--- /dev/null
+++ b/.github/workflows/nmc-custom-oidc-composer.yml
@@ -0,0 +1,82 @@
+###
+# SPDX-License-Identifier: AGPL-3.0
+#
+# Author: Bernd Rederlechner
Date: Fri, 1 Sep 2023 19:33:57 +0200
Subject: [PATCH 13/33] Remove commit push blocker
---
.github/workflows/nmc-custom-oidc-composer.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.github/workflows/nmc-custom-oidc-composer.yml b/.github/workflows/nmc-custom-oidc-composer.yml
index 474b3257..ca247fbd 100644
--- a/.github/workflows/nmc-custom-oidc-composer.yml
+++ b/.github/workflows/nmc-custom-oidc-composer.yml
@@ -45,7 +45,6 @@ jobs:
# set user in case commits are needed
git config user.name $BUILD_USER
git config user.email $BUILD_EMAIL
- git remote set-url origin http://no.such.host
# install php dependencies
- name: Set up php ${{ env.PHP_VERSION }}
From 77aa3687458a34318750c2cf93145f54ef694d4c Mon Sep 17 00:00:00 2001
From: "Bernd.Rederlechner@t-systems.com"
Date: Mon, 4 Sep 2023 08:49:39 +0200
Subject: [PATCH 14/33] Fix assembly branch name for dependency check
---
.github/workflows/nmc-custom-app-release.yml | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/.github/workflows/nmc-custom-app-release.yml b/.github/workflows/nmc-custom-app-release.yml
index b34abf18..64d287ce 100644
--- a/.github/workflows/nmc-custom-app-release.yml
+++ b/.github/workflows/nmc-custom-app-release.yml
@@ -51,12 +51,10 @@ jobs:
composerdep:
strategy:
fail-fast: false
- matrix:
- custombase: [ "main" ]
uses: ./.github/workflows/nmc-custom-oidc-composer.yml
needs: assemble-custom
with:
- assembly: ${{ format('customisation-{0}-{1}', github.actor, matrix.custombase) }}
+ assembly: ${{ format('customisation-{0}-{1}', inputs.branch, inputs.increment) }}
secrets: inherit
build-custom:
From 1b44600ebaba387923d429698c849042f010bb57 Mon Sep 17 00:00:00 2001
From: Mauro Mura
Date: Wed, 29 Oct 2025 07:57:31 +0100
Subject: [PATCH 15/33] Update nmc-custom-oidc-composer.yml
---
.github/workflows/nmc-custom-oidc-composer.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/nmc-custom-oidc-composer.yml b/.github/workflows/nmc-custom-oidc-composer.yml
index ca247fbd..d4f2c527 100644
--- a/.github/workflows/nmc-custom-oidc-composer.yml
+++ b/.github/workflows/nmc-custom-oidc-composer.yml
@@ -26,7 +26,7 @@ jobs:
BUILD_USER: ${{ github.actor }}
BUILD_EMAIL: ${{ github.actor }}@users.noreply.github.com
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN || secrets.GITHUB_TOKEN }}
- PHP_VERSION: ${{ vars.PHP_VERSION || '8.1' }}
+ PHP_VERSION: ${{ vars.PHP_VERSION || '8.2' }}
steps:
- name: Fetch custom assembly
id: checkout_custom
From 66b80f8e67b080f0113bef1d31d8f7fc967c96ab Mon Sep 17 00:00:00 2001
From: memurats
Date: Wed, 29 Oct 2025 16:12:00 +0100
Subject: [PATCH 16/33] return status ok
---
lib/Controller/LoginController.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/Controller/LoginController.php b/lib/Controller/LoginController.php
index da786660..225f4b79 100644
--- a/lib/Controller/LoginController.php
+++ b/lib/Controller/LoginController.php
@@ -897,7 +897,7 @@ private function getBackchannelLogoutErrorResponse(
'error' => $error,
'error_description' => $description,
],
- Http::STATUS_BAD_REQUEST,
+ Http::STATUS_OK,
);
}
From 2ae69f411de6bcefd2e4cc3055e886bdd595ab0e Mon Sep 17 00:00:00 2001
From: memurats
Date: Wed, 29 Oct 2025 16:14:33 +0100
Subject: [PATCH 17/33] added check and redirect
---
lib/Controller/LoginController.php | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/lib/Controller/LoginController.php b/lib/Controller/LoginController.php
index da786660..2e4664f1 100644
--- a/lib/Controller/LoginController.php
+++ b/lib/Controller/LoginController.php
@@ -321,6 +321,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([
@@ -901,6 +906,22 @@ private function getBackchannelLogoutErrorResponse(
);
}
+ 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
+ }
+
private function toCodeChallenge(string $data): string {
// Basically one big work around for the base64url decode being weird
$h = pack('H*', hash('sha256', $data));
From 9ddcb5216dc22ee24ea6363e7900dd913e3e9848 Mon Sep 17 00:00:00 2001
From: memurats
Date: Wed, 29 Oct 2025 17:03:47 +0100
Subject: [PATCH 18/33] added central customization
---
.github/README.md | 54 +++++++++++++++++++++++++
COPYING.DTAG | 5 +++
appinfo/routes.php | 5 ++-
lib/AppInfo/Application.php | 81 ++++++++++++++++++++++++++++++++++++-
tests/bootstrap.php | 5 +++
5 files changed, 147 insertions(+), 3 deletions(-)
create mode 100644 .github/README.md
create mode 100644 COPYING.DTAG
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/COPYING.DTAG b/COPYING.DTAG
new file mode 100644
index 00000000..85c2c3b9
--- /dev/null
+++ b/COPYING.DTAG
@@ -0,0 +1,5 @@
+Although this Nextcloud app code is free and available under the AGPL3 license, Deutsche Telekom
+(including T-Systems) fully reserves all rights to the Telekom brand. To prevent users from getting confused about
+the source of a digital product or experience, there are stringent restrictions on using the Telekom brand and design,
+even when built into code that we provide. For any customization other than explicitly for Telekom or T-Systems, you must
+replace the Deutsche Telekom and T-Systems brand elements contained in the provided sources.
diff --git a/appinfo/routes.php b/appinfo/routes.php
index a8c6bb52..58a0b9e4 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -16,9 +16,10 @@
['name' => '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/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 9842067f..7221eac3 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', '>=')) {
@@ -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/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();
From cf3fc5bcc79980674fc3f617d3f1cbf7ac7b87b2 Mon Sep 17 00:00:00 2001
From: memurats
Date: Wed, 29 Oct 2025 17:19:14 +0100
Subject: [PATCH 19/33] added event based provisioning
---
lib/AppInfo/Application.php | 2 +-
lib/Controller/LoginController.php | 19 +
lib/Event/UserAccountChangeEvent.php | 87 ++++
lib/Event/UserAccountChangeResult.php | 74 +++
lib/Service/ProvisioningDeniedException.php | 69 +++
lib/Service/ProvisioningEventService.php | 180 +++++++
.../unit/MagentaCloud/OpenidTokenTestCase.php | 141 +++++
.../ProvisioningEventServiceTest.php | 481 ++++++++++++++++++
tests/unit/MagentaCloud/RegistrationsTest.php | 33 ++
9 files changed, 1085 insertions(+), 1 deletion(-)
create mode 100644 lib/Event/UserAccountChangeEvent.php
create mode 100644 lib/Event/UserAccountChangeResult.php
create mode 100644 lib/Service/ProvisioningDeniedException.php
create mode 100644 lib/Service/ProvisioningEventService.php
create mode 100644 tests/unit/MagentaCloud/OpenidTokenTestCase.php
create mode 100644 tests/unit/MagentaCloud/ProvisioningEventServiceTest.php
create mode 100644 tests/unit/MagentaCloud/RegistrationsTest.php
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 9842067f..ec75d924 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -74,7 +74,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()) {
diff --git a/lib/Controller/LoginController.php b/lib/Controller/LoginController.php
index da786660..49dc2861 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;
@@ -554,6 +555,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
diff --git a/lib/Event/UserAccountChangeEvent.php b/lib/Event/UserAccountChangeEvent.php
new file mode 100644
index 00000000..c6b698aa
--- /dev/null
+++ b/lib/Event/UserAccountChangeEvent.php
@@ -0,0 +1,87 @@
+
+ *
+ * @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 {
+ private $uid;
+ private $displayname;
+ private $mainEmail;
+ private $quota;
+ private $claims;
+ 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');
+ }
+
+ /**
+ * @return get event username (uid)
+ */
+ public function getUid(): string {
+ return $this->uid;
+ }
+
+ /**
+ * @return get event displayname
+ */
+ public function getDisplayName(): ?string {
+ return $this->displayname;
+ }
+
+ /**
+ * @return get event main email
+ */
+ public function getMainEmail(): ?string {
+ return $this->mainEmail;
+ }
+
+ /**
+ * @return get event quota
+ */
+ public function getQuota(): ?string {
+ return $this->quota;
+ }
+
+ /**
+ * @return array the array of claim values associated with the event
+ */
+ public function getClaims(): object {
+ return $this->claims;
+ }
+
+ /**
+ * @return value for the logged in user attribute
+ */
+ public function getResult(): UserAccountChangeResult {
+ return $this->result;
+ }
+
+ 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..660e78f9
--- /dev/null
+++ b/lib/Event/UserAccountChangeResult.php
@@ -0,0 +1,74 @@
+
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ */
+
+declare(strict_types=1);
+
+namespace OCA\UserOIDC\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 UserAccountChangeResult {
+
+ /** @var bool */
+ private $accessAllowed;
+ /** @var string */
+ private $reason;
+ /** @var string */
+ private $redirectUrl;
+
+ public function __construct(bool $accessAllowed, string $reason = '', ?string $redirectUrl = null) {
+ $this->accessAllowed = $accessAllowed;
+ $this->redirectUrl = $redirectUrl;
+ $this->reason = $reason;
+ }
+
+ /**
+ * @return value for the logged in user attribute
+ */
+ public function isAccessAllowed(): bool {
+ return $this->accessAllowed;
+ }
+
+ public function setAccessAllowed(bool $accessAllowed): void {
+ $this->accessAllowed = $accessAllowed;
+ }
+
+ /**
+ * @return get optional alternate redirect address
+ */
+ public function getRedirectUrl(): ?string {
+ return $this->redirectUrl;
+ }
+
+ /**
+ * @return set optional alternate redirect address
+ */
+ public function setRedirectUrl(?string $redirectUrl): void {
+ $this->redirectUrl = $redirectUrl;
+ }
+
+ /**
+ * @return get decision reason
+ */
+ public function getReason(): string {
+ return $this->reason;
+ }
+
+ /**
+ * @return set decision reason
+ */
+ public function setReason(string $reason): void {
+ $this->reason = $reason;
+ }
+}
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..d544bd60
--- /dev/null
+++ b/lib/Service/ProvisioningEventService.php
@@ -0,0 +1,180 @@
+
+ *
+ * @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 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 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,
+ ) {
+ parent::__construct($idService,
+ $providerService,
+ $userMapper,
+ $userManager,
+ $groupManager,
+ $eventDispatcher,
+ $logger,
+ $accountManager,
+ $clientService,
+ $avatarManager,
+ $config,
+ $session,
+ );
+ $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/tests/unit/MagentaCloud/OpenidTokenTestCase.php b/tests/unit/MagentaCloud/OpenidTokenTestCase.php
new file mode 100644
index 00000000..c0955093
--- /dev/null
+++ b/tests/unit/MagentaCloud/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/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);
+ }
+}
From 9e9e6687da9d96cc1b58af6668a5b1045ca482d2 Mon Sep 17 00:00:00 2001
From: memurats
Date: Wed, 29 Oct 2025 17:26:02 +0100
Subject: [PATCH 20/33] added bearer token handling
---
lib/MagentaBearer/InvalidTokenException.php | 8 +
lib/MagentaBearer/MBackend.php | 199 +++++++++++++++
lib/MagentaBearer/SignatureException.php | 6 +
lib/MagentaBearer/TokenService.php | 203 +++++++++++++++
lib/User/AbstractOidcBackend.php | 220 ++++++++++++++++
.../MagentaCloud/BearerTokenServiceTest.php | 103 ++++++++
.../unit/MagentaCloud/BearerTokenTestCase.php | 225 +++++++++++++++++
.../MagentaCloud/HeaderBearerTokenTest.php | 236 ++++++++++++++++++
.../unit/MagentaCloud/SamBearerTokenTest.php | 85 +++++++
9 files changed, 1285 insertions(+)
create mode 100644 lib/MagentaBearer/InvalidTokenException.php
create mode 100644 lib/MagentaBearer/MBackend.php
create mode 100644 lib/MagentaBearer/SignatureException.php
create mode 100644 lib/MagentaBearer/TokenService.php
create mode 100644 lib/User/AbstractOidcBackend.php
create mode 100644 tests/unit/MagentaCloud/BearerTokenServiceTest.php
create mode 100644 tests/unit/MagentaCloud/BearerTokenTestCase.php
create mode 100644 tests/unit/MagentaCloud/HeaderBearerTokenTest.php
create mode 100644 tests/unit/MagentaCloud/SamBearerTokenTest.php
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/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/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/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']);
+ }
+}
From e2d9cf9f380bd3186eeeb59dccde525d5edb811a Mon Sep 17 00:00:00 2001
From: memurats
Date: Wed, 29 Oct 2025 18:02:38 +0100
Subject: [PATCH 21/33] added bearer token secret
---
lib/Command/UpsertProvider.php | 16 +-
lib/Controller/SettingsController.php | 13 +-
lib/Db/Provider.php | 5 +
lib/Db/ProviderMapper.php | 7 +-
.../Version00008Date20211114183344.php | 25 ++
.../Version010304Date20230902125945.php | 97 ++++
src/components/SettingsForm.vue | 9 +
.../unit/MagentaCloud/BearerSettingsTest.php | 420 ++++++++++++++++++
8 files changed, 587 insertions(+), 5 deletions(-)
create mode 100644 lib/Migration/Version00008Date20211114183344.php
create mode 100644 lib/Migration/Version010304Date20230902125945.php
create mode 100644 tests/unit/MagentaCloud/BearerSettingsTest.php
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/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/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/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/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',
+ ]);
+ }
+}
From 5a2ffd1c870b28f49f5f68b984175fff313a4e0a Mon Sep 17 00:00:00 2001
From: memurats
Date: Wed, 29 Oct 2025 20:13:20 +0100
Subject: [PATCH 22/33] backchannel logout fix
---
lib/Controller/LoginController.php | 66 ++++++++++++++++++++++--------
1 file changed, 50 insertions(+), 16 deletions(-)
diff --git a/lib/Controller/LoginController.php b/lib/Controller/LoginController.php
index da786660..99a01280 100644
--- a/lib/Controller/LoginController.php
+++ b/lib/Controller/LoginController.php
@@ -171,12 +171,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 +202,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
@@ -601,16 +615,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 +637,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(),
@@ -901,6 +919,22 @@ private function getBackchannelLogoutErrorResponse(
);
}
+ /**
+ * 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));
From 8c19fdc2b64f987e2c9623d11e3a9424bc040a37 Mon Sep 17 00:00:00 2001
From: memurats
Date: Wed, 29 Oct 2025 20:23:52 +0100
Subject: [PATCH 23/33] fixed merge error
---
lib/Controller/LoginController.php | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/lib/Controller/LoginController.php b/lib/Controller/LoginController.php
index 2e4664f1..487db098 100644
--- a/lib/Controller/LoginController.php
+++ b/lib/Controller/LoginController.php
@@ -906,6 +906,16 @@ private function getBackchannelLogoutErrorResponse(
);
}
+ private function toCodeChallenge(string $data): string {
+ // Basically one big work around for the base64url decode being weird
+ $h = pack('H*', hash('sha256', $data));
+ $s = base64_encode($h); // Regular base64 encoder
+ $s = explode('=', $s)[0]; // Remove any trailing '='s
+ $s = str_replace('+', '-', $s); // 62nd char of encoding
+ $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']);
@@ -921,14 +931,4 @@ private function isMobileDevice(): bool {
return false; // device is desktop
}
-
- private function toCodeChallenge(string $data): string {
- // Basically one big work around for the base64url decode being weird
- $h = pack('H*', hash('sha256', $data));
- $s = base64_encode($h); // Regular base64 encoder
- $s = explode('=', $s)[0]; // Remove any trailing '='s
- $s = str_replace('+', '-', $s); // 62nd char of encoding
- $s = str_replace('/', '_', $s); // 63rd char of encoding
- return $s;
- }
}
From 19f8f03968962d49ed87061e674c0450f068c0ba Mon Sep 17 00:00:00 2001
From: memurats
Date: Thu, 30 Oct 2025 08:29:04 +0100
Subject: [PATCH 24/33] added missing argument
---
lib/Service/ProvisioningEventService.php | 3 +++
1 file changed, 3 insertions(+)
diff --git a/lib/Service/ProvisioningEventService.php b/lib/Service/ProvisioningEventService.php
index d544bd60..c9695886 100644
--- a/lib/Service/ProvisioningEventService.php
+++ b/lib/Service/ProvisioningEventService.php
@@ -38,6 +38,7 @@
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;
@@ -70,6 +71,7 @@ public function __construct(
IAvatarManager $avatarManager,
IConfig $config,
ISession $session,
+ IFactory $l10nFactory,
) {
parent::__construct($idService,
$providerService,
@@ -83,6 +85,7 @@ public function __construct(
$avatarManager,
$config,
$session,
+ $l10nFactory,
);
$this->eventDispatcher = $eventDispatcher;
$this->logger = $logger;
From d6f09381a47b348faa9eefe002227ab2316eb76f Mon Sep 17 00:00:00 2001
From: memurats
Date: Thu, 30 Oct 2025 08:43:09 +0100
Subject: [PATCH 25/33] moved test class
---
tests/unit/{MagentaCloud => BaseTest}/OpenidTokenTestCase.php | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename tests/unit/{MagentaCloud => BaseTest}/OpenidTokenTestCase.php (100%)
diff --git a/tests/unit/MagentaCloud/OpenidTokenTestCase.php b/tests/unit/BaseTest/OpenidTokenTestCase.php
similarity index 100%
rename from tests/unit/MagentaCloud/OpenidTokenTestCase.php
rename to tests/unit/BaseTest/OpenidTokenTestCase.php
From dd2a2538f6140e12fb0c089da377f053a9b26332 Mon Sep 17 00:00:00 2001
From: memurats
Date: Thu, 30 Oct 2025 08:53:32 +0100
Subject: [PATCH 26/33] added exception
---
lib/Exception/AttributeValueException.php | 32 +++++++++++++++++++++++
lib/Service/ProvisioningEventService.php | 1 +
2 files changed, 33 insertions(+)
create mode 100644 lib/Exception/AttributeValueException.php
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/Service/ProvisioningEventService.php b/lib/Service/ProvisioningEventService.php
index c9695886..abcaf940 100644
--- a/lib/Service/ProvisioningEventService.php
+++ b/lib/Service/ProvisioningEventService.php
@@ -28,6 +28,7 @@
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;
From 3b4d492063b58115257d282bb8b51dbbfb36a049 Mon Sep 17 00:00:00 2001
From: memurats
Date: Thu, 30 Oct 2025 08:57:43 +0100
Subject: [PATCH 27/33] fixed psalm errors
---
lib/Event/UserAccountChangeResult.php | 39 +++++++++++++++++++--------
1 file changed, 28 insertions(+), 11 deletions(-)
diff --git a/lib/Event/UserAccountChangeResult.php b/lib/Event/UserAccountChangeResult.php
index 660e78f9..d8af8e49 100644
--- a/lib/Event/UserAccountChangeResult.php
+++ b/lib/Event/UserAccountChangeResult.php
@@ -5,7 +5,6 @@
* @author B. Rederlechner
*
* @license GNU AGPL version 3 or any later version
- *
*/
declare(strict_types=1);
@@ -13,18 +12,18 @@
namespace OCA\UserOIDC\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();
+ * 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 */
+
+ /** @var string|null */
private $redirectUrl;
public function __construct(bool $accessAllowed, string $reason = '', ?string $redirectUrl = null) {
@@ -34,39 +33,57 @@ public function __construct(bool $accessAllowed, string $reason = '', ?string $r
}
/**
- * @return value for the logged in user attribute
+ * 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;
}
/**
- * @return get optional alternate redirect address
+ * Returns the optional alternate redirect URL.
+ *
+ * @return string|null
*/
public function getRedirectUrl(): ?string {
return $this->redirectUrl;
}
/**
- * @return set optional alternate redirect address
+ * Sets the optional alternate redirect URL.
+ *
+ * @param string|null $redirectUrl
+ * @return void
*/
public function setRedirectUrl(?string $redirectUrl): void {
$this->redirectUrl = $redirectUrl;
}
/**
- * @return get decision reason
+ * Returns the decision reason.
+ *
+ * @return string
*/
public function getReason(): string {
return $this->reason;
}
/**
- * @return set decision reason
+ * Sets the decision reason.
+ *
+ * @param string $reason
+ * @return void
*/
public function setReason(string $reason): void {
$this->reason = $reason;
From d482982666bb5fba3ed3ef417a89d6016e7b1137 Mon Sep 17 00:00:00 2001
From: memurats
Date: Thu, 30 Oct 2025 09:03:02 +0100
Subject: [PATCH 28/33] fixed psalm errors
---
lib/Event/UserAccountChangeEvent.php | 56 ++++++++++++++++++++++-----
lib/Event/UserAccountChangeResult.php | 1 +
2 files changed, 48 insertions(+), 9 deletions(-)
diff --git a/lib/Event/UserAccountChangeEvent.php b/lib/Event/UserAccountChangeEvent.php
index c6b698aa..de9fb5e5 100644
--- a/lib/Event/UserAccountChangeEvent.php
+++ b/lib/Event/UserAccountChangeEvent.php
@@ -21,15 +21,33 @@
* 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;
- private $result;
+ /** @var UserAccountChangeResult */
+ private $result;
- public function __construct(string $uid, ?string $displayname, ?string $mainEmail, ?string $quota, object $claims, bool $accessAllowed = false) {
+ public function __construct(
+ string $uid,
+ ?string $displayname,
+ ?string $mainEmail,
+ ?string $quota,
+ object $claims,
+ bool $accessAllowed = false
+ ) {
parent::__construct();
$this->uid = $uid;
$this->displayname = $displayname;
@@ -40,48 +58,68 @@ public function __construct(string $uid, ?string $displayname, ?string $mainEmai
}
/**
- * @return get event username (uid)
+ * Get the user ID (UID) associated with the event.
+ *
+ * @return string
*/
public function getUid(): string {
return $this->uid;
}
/**
- * @return get event displayname
+ * Get the display name for the account.
+ *
+ * @return string|null
*/
public function getDisplayName(): ?string {
return $this->displayname;
}
/**
- * @return get event main email
+ * Get the primary email address.
+ *
+ * @return string|null
*/
public function getMainEmail(): ?string {
return $this->mainEmail;
}
/**
- * @return get event quota
+ * Get the quota assigned to the account.
+ *
+ * @return string|null
*/
public function getQuota(): ?string {
return $this->quota;
}
/**
- * @return array the array of claim values associated with the event
+ * Get the OIDC claims associated with the event.
+ *
+ * @return object
*/
public function getClaims(): object {
return $this->claims;
}
/**
- * @return value for the logged in user attribute
+ * Get the current result object.
+ *
+ * @return UserAccountChangeResult
*/
public function getResult(): UserAccountChangeResult {
return $this->result;
}
- public function setResult(bool $accessAllowed, string $reason = '', ?string $redirectUrl = null) : void {
+ /**
+ * 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
index d8af8e49..1b19d639 100644
--- a/lib/Event/UserAccountChangeResult.php
+++ b/lib/Event/UserAccountChangeResult.php
@@ -5,6 +5,7 @@
* @author B. Rederlechner
*
* @license GNU AGPL version 3 or any later version
+ *
*/
declare(strict_types=1);
From b3f0c47f57915e3c80a59f39d056bd0d6fa0428b Mon Sep 17 00:00:00 2001
From: memurats
Date: Thu, 30 Oct 2025 09:38:13 +0100
Subject: [PATCH 29/33] fixed errors
---
lib/MagentaBearer/TokenService.php | 340 ++++++++++++++++-------------
lib/User/AbstractOidcBackend.php | 1 -
2 files changed, 183 insertions(+), 158 deletions(-)
diff --git a/lib/MagentaBearer/TokenService.php b/lib/MagentaBearer/TokenService.php
index 3645580e..9e3fd7f3 100644
--- a/lib/MagentaBearer/TokenService.php
+++ b/lib/MagentaBearer/TokenService.php
@@ -24,180 +24,206 @@
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\Encryption\Serializer\JWESerializerManager;
+use Jose\Component\Encryption\Serializer\CompactSerializer as JWECompactSerializer;
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 Jose\Component\Signature\Serializer\JWSSerializerManager;
+use Jose\Component\Signature\Serializer\CompactSerializer as JWSCompactSerializer;
use OCP\AppFramework\Utility\ITimeFactory;
use Psr\Log\LoggerInterface;
+/**
+ * Token service for handling Magenta/SAM3 bearer tokens (decrypt/verify/claims).
+ */
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.');
- }
- }
+ /** @var LoggerInterface */
+ private $logger;
+
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ /** @var DiscoveryService|null */
+ private $discoveryService;
+
+ /** @var JWEDecrypter */
+ private $jweDecrypter;
+
+ /** @var JWESerializerManager */
+ private $encryptionSerializerManager;
+
+ /** @var JWSVerifier */
+ private $jwsVerifier;
+
+ /** @var JWSSerializerManager */
+ private $serializerManager;
+
+ public function __construct(LoggerInterface $logger, ITimeFactory $timeFactory, ?DiscoveryService $discoveryService = null) {
+ $this->logger = $logger;
+ $this->timeFactory = $timeFactory;
+ $this->discoveryService = $discoveryService;
+
+ // Key encryption algorithms manager
+ $keyEncryptionAlgorithmManager = new AlgorithmManager([
+ new PBES2HS512A256KW(),
+ new RSAOAEP256(),
+ new ECDHESA256KW(),
+ ]);
+
+ // Content encryption algorithm manager
+ $contentEncryptionAlgorithmManager = new AlgorithmManager([
+ new A256CBCHS512(),
+ ]);
+
+ // Compression manager
+ $compressionMethodManager = new CompressionMethodManager([
+ new Deflate(),
+ ]);
+
+ // Signature algorithms manager
+ $signatureAlgorithmManager = new AlgorithmManager([
+ new HS256(),
+ new HS384(),
+ new HS512(),
+ ]);
+
+ // JWE decrypter
+ $this->jweDecrypter = new JWEDecrypter(
+ $keyEncryptionAlgorithmManager,
+ $contentEncryptionAlgorithmManager,
+ $compressionMethodManager
+ );
+
+ // JWESerializerManager
+ $this->encryptionSerializerManager = new JWESerializerManager([
+ new JWECompactSerializer(),
+ ]);
+
+ // JWS verifier
+ $this->jwsVerifier = new JWSVerifier($signatureAlgorithmManager);
+
+ // JWSSerializerManager
+ $this->serializerManager = new JWSSerializerManager([
+ new JWSCompactSerializer(),
+ ]);
+ }
+
+ /**
+ * Implement JOSE decryption for SAM3 tokens
+ *
+ * @param string $rawToken
+ * @param string $decryptKey
+ * @return JWS
+ * @throws InvalidTokenException
+ */
+ public function decryptToken(string $rawToken, string $decryptKey) : JWS {
+ $numSegments = substr_count($rawToken, '.') + 1;
+ $this->logger->debug('Bearer access token(segments=' . $numSegments . ')=' . $rawToken);
+
+ if ($numSegments > 3) {
+ $clientSecret = new JWK([
+ 'kty' => 'oct',
+ 'k' => $decryptKey,
+ ]);
+
+ $jwe = $this->encryptionSerializerManager->unserialize($rawToken);
+
+ if ($this->jweDecrypter->decryptUsingKey($jwe, $clientSecret, 0)) {
+ return $this->serializerManager->unserialize($jwe->getPayload());
+ }
+
+ throw new InvalidTokenException('Unknown bearer encryption format');
+ }
+
+ return $this->serializerManager->unserialize($rawToken);
+ }
+
+ /**
+ * Decode the token payload into an object and remap claims
+ *
+ * @param JWS $decodedToken
+ * @return object
+ */
+ public function decode(JWS $decodedToken) : object {
+ $this->logger->debug('Telekom SAM3 access token: ' . $decodedToken->getPayload());
+ $samContent = json_decode($decodedToken->getPayload(), false);
+
+ $claimArray = $samContent->{'urn:telekom.com:idm:at:attributes'} ?? null;
+ if (is_array($claimArray)) {
+ foreach ($claimArray as $claimKeyValue) {
+ if (isset($claimKeyValue->name)) {
+ $samContent->{'urn:telekom.com:' . $claimKeyValue->name} = $claimKeyValue->value ?? null;
+ }
+ }
+ unset($samContent->{'urn:telekom.com:idm:at:attributes'});
+ }
+
+ $this->logger->debug('Adapted OpenID-like token; ' . json_encode($samContent));
+ return $samContent;
+ }
+
+ /**
+ * Verify the JWS signature using the given symmetric key
+ *
+ * @param JWS $decodedToken
+ * @param string $signKey
+ * @return void
+ * @throws SignatureException
+ */
+ public function verifySignature(JWS $decodedToken, string $signKey): void {
+ $accessSecret = new JWK([
+ 'kty' => 'oct',
+ 'k' => $signKey,
+ ]);
+
+ if (!$this->jwsVerifier->verifyWithKey($decodedToken, $accessSecret, 0)) {
+ throw new SignatureException('Invalid Signature');
+ }
+ }
+
+ /**
+ * Verify standard claims (nbf/iat/exp/aud)
+ *
+ * @param object $claims
+ * @param array $audiences
+ * @param int $leeway
+ * @return void
+ * @throws InvalidTokenException
+ */
+ public function verifyClaims(object $claims, array $audiences = [], int $leeway = 60): void {
+ $timestamp = $this->timeFactory->getTime();
+
+ if (isset($claims->nbf) && $claims->nbf > ($timestamp + $leeway)) {
+ throw new InvalidTokenException(
+ 'Cannot handle token prior to ' . \date(\DATE_ATOM, (int)$claims->nbf)
+ );
+ }
+
+ if (isset($claims->iat) && $claims->iat > ($timestamp + $leeway)) {
+ throw new InvalidTokenException(
+ 'Cannot handle token prior to ' . \date(\DATE_ATOM, (int)$claims->iat)
+ );
+ }
+
+ if (isset($claims->exp) && ($timestamp - $leeway) >= $claims->exp) {
+ throw new InvalidTokenException('Expired token');
+ }
+
+ if (empty(array_intersect((array)($claims->aud ?? []), $audiences))) {
+ throw new InvalidTokenException('No acceptable audience in token.');
+ }
+ }
}
diff --git a/lib/User/AbstractOidcBackend.php b/lib/User/AbstractOidcBackend.php
index d1e5de7b..7bfb95a0 100644
--- a/lib/User/AbstractOidcBackend.php
+++ b/lib/User/AbstractOidcBackend.php
@@ -190,7 +190,6 @@ public function getLogoutUrl(): string {
*
* @param string $userId
* @return bool
- * @throws NotFoundException
*/
protected function checkFirstLogin(string $userId): bool {
$user = $this->userManager->get($userId);
From bccd1a05a167d2b9f1cf4281f291dcc5faf65465 Mon Sep 17 00:00:00 2001
From: memurats
Date: Thu, 30 Oct 2025 09:47:42 +0100
Subject: [PATCH 30/33] revert tokenservice changes
---
lib/MagentaBearer/TokenService.php | 340 +++++++++++++----------------
1 file changed, 157 insertions(+), 183 deletions(-)
diff --git a/lib/MagentaBearer/TokenService.php b/lib/MagentaBearer/TokenService.php
index 9e3fd7f3..3645580e 100644
--- a/lib/MagentaBearer/TokenService.php
+++ b/lib/MagentaBearer/TokenService.php
@@ -24,206 +24,180 @@
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\Encryption\Serializer\JWESerializerManager;
-use Jose\Component\Encryption\Serializer\CompactSerializer as JWECompactSerializer;
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 Jose\Component\Signature\Serializer\JWSSerializerManager;
-use Jose\Component\Signature\Serializer\CompactSerializer as JWSCompactSerializer;
use OCP\AppFramework\Utility\ITimeFactory;
use Psr\Log\LoggerInterface;
-/**
- * Token service for handling Magenta/SAM3 bearer tokens (decrypt/verify/claims).
- */
class TokenService {
- /** @var LoggerInterface */
- private $logger;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var DiscoveryService|null */
- private $discoveryService;
-
- /** @var JWEDecrypter */
- private $jweDecrypter;
-
- /** @var JWESerializerManager */
- private $encryptionSerializerManager;
-
- /** @var JWSVerifier */
- private $jwsVerifier;
-
- /** @var JWSSerializerManager */
- private $serializerManager;
-
- public function __construct(LoggerInterface $logger, ITimeFactory $timeFactory, ?DiscoveryService $discoveryService = null) {
- $this->logger = $logger;
- $this->timeFactory = $timeFactory;
- $this->discoveryService = $discoveryService;
-
- // Key encryption algorithms manager
- $keyEncryptionAlgorithmManager = new AlgorithmManager([
- new PBES2HS512A256KW(),
- new RSAOAEP256(),
- new ECDHESA256KW(),
- ]);
-
- // Content encryption algorithm manager
- $contentEncryptionAlgorithmManager = new AlgorithmManager([
- new A256CBCHS512(),
- ]);
-
- // Compression manager
- $compressionMethodManager = new CompressionMethodManager([
- new Deflate(),
- ]);
-
- // Signature algorithms manager
- $signatureAlgorithmManager = new AlgorithmManager([
- new HS256(),
- new HS384(),
- new HS512(),
- ]);
-
- // JWE decrypter
- $this->jweDecrypter = new JWEDecrypter(
- $keyEncryptionAlgorithmManager,
- $contentEncryptionAlgorithmManager,
- $compressionMethodManager
- );
-
- // JWESerializerManager
- $this->encryptionSerializerManager = new JWESerializerManager([
- new JWECompactSerializer(),
- ]);
-
- // JWS verifier
- $this->jwsVerifier = new JWSVerifier($signatureAlgorithmManager);
-
- // JWSSerializerManager
- $this->serializerManager = new JWSSerializerManager([
- new JWSCompactSerializer(),
- ]);
- }
-
- /**
- * Implement JOSE decryption for SAM3 tokens
- *
- * @param string $rawToken
- * @param string $decryptKey
- * @return JWS
- * @throws InvalidTokenException
- */
- public function decryptToken(string $rawToken, string $decryptKey) : JWS {
- $numSegments = substr_count($rawToken, '.') + 1;
- $this->logger->debug('Bearer access token(segments=' . $numSegments . ')=' . $rawToken);
-
- if ($numSegments > 3) {
- $clientSecret = new JWK([
- 'kty' => 'oct',
- 'k' => $decryptKey,
- ]);
-
- $jwe = $this->encryptionSerializerManager->unserialize($rawToken);
-
- if ($this->jweDecrypter->decryptUsingKey($jwe, $clientSecret, 0)) {
- return $this->serializerManager->unserialize($jwe->getPayload());
- }
-
- throw new InvalidTokenException('Unknown bearer encryption format');
- }
-
- return $this->serializerManager->unserialize($rawToken);
- }
-
- /**
- * Decode the token payload into an object and remap claims
- *
- * @param JWS $decodedToken
- * @return object
- */
- public function decode(JWS $decodedToken) : object {
- $this->logger->debug('Telekom SAM3 access token: ' . $decodedToken->getPayload());
- $samContent = json_decode($decodedToken->getPayload(), false);
-
- $claimArray = $samContent->{'urn:telekom.com:idm:at:attributes'} ?? null;
- if (is_array($claimArray)) {
- foreach ($claimArray as $claimKeyValue) {
- if (isset($claimKeyValue->name)) {
- $samContent->{'urn:telekom.com:' . $claimKeyValue->name} = $claimKeyValue->value ?? null;
- }
- }
- unset($samContent->{'urn:telekom.com:idm:at:attributes'});
- }
-
- $this->logger->debug('Adapted OpenID-like token; ' . json_encode($samContent));
- return $samContent;
- }
-
- /**
- * Verify the JWS signature using the given symmetric key
- *
- * @param JWS $decodedToken
- * @param string $signKey
- * @return void
- * @throws SignatureException
- */
- public function verifySignature(JWS $decodedToken, string $signKey): void {
- $accessSecret = new JWK([
- 'kty' => 'oct',
- 'k' => $signKey,
- ]);
-
- if (!$this->jwsVerifier->verifyWithKey($decodedToken, $accessSecret, 0)) {
- throw new SignatureException('Invalid Signature');
- }
- }
-
- /**
- * Verify standard claims (nbf/iat/exp/aud)
- *
- * @param object $claims
- * @param array $audiences
- * @param int $leeway
- * @return void
- * @throws InvalidTokenException
- */
- public function verifyClaims(object $claims, array $audiences = [], int $leeway = 60): void {
- $timestamp = $this->timeFactory->getTime();
-
- if (isset($claims->nbf) && $claims->nbf > ($timestamp + $leeway)) {
- throw new InvalidTokenException(
- 'Cannot handle token prior to ' . \date(\DATE_ATOM, (int)$claims->nbf)
- );
- }
-
- if (isset($claims->iat) && $claims->iat > ($timestamp + $leeway)) {
- throw new InvalidTokenException(
- 'Cannot handle token prior to ' . \date(\DATE_ATOM, (int)$claims->iat)
- );
- }
-
- if (isset($claims->exp) && ($timestamp - $leeway) >= $claims->exp) {
- throw new InvalidTokenException('Expired token');
- }
-
- if (empty(array_intersect((array)($claims->aud ?? []), $audiences))) {
- throw new InvalidTokenException('No acceptable audience in token.');
- }
- }
+ /** @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.');
+ }
+ }
}
From 1424e83b977a4c74d972bcd9203335fd31aeddbd Mon Sep 17 00:00:00 2001
From: Mauro Mura
Date: Fri, 5 Dec 2025 10:51:51 +0100
Subject: [PATCH 31/33] Clean up LoginController by removing unused comments
Removed commented-out code related to login token storage.
---
lib/Controller/LoginController.php | 11 -----------
1 file changed, 11 deletions(-)
diff --git a/lib/Controller/LoginController.php b/lib/Controller/LoginController.php
index 99a01280..83ba4d30 100644
--- a/lib/Controller/LoginController.php
+++ b/lib/Controller/LoginController.php
@@ -615,17 +615,6 @@ 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) {
- // 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');
-
// remove code login session values
$this->session->remove(self::STATE);
$this->session->remove(self::NONCE);
From ba440fb36caa07ee4608d1983b259b08c1432a37 Mon Sep 17 00:00:00 2001
From: memurats
Date: Fri, 12 Dec 2025 14:23:11 +0100
Subject: [PATCH 32/33] fix errors
---
lib/MagentaBearer/TokenService.php | 149 +++++++++++++----------------
lib/Service/TokenService.php | 139 +++++++++++----------------
2 files changed, 122 insertions(+), 166 deletions(-)
diff --git a/lib/MagentaBearer/TokenService.php b/lib/MagentaBearer/TokenService.php
index 3645580e..f0762c46 100644
--- a/lib/MagentaBearer/TokenService.php
+++ b/lib/MagentaBearer/TokenService.php
@@ -5,151 +5,133 @@
* @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 DateTime;
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\Encryption\Serializer\CompactSerializer as JWECompactSerializer;
+use Jose\Component\Encryption\Serializer\JWESerializerManager;
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 Jose\Component\Signature\Serializer\CompactSerializer as JWSCompactSerializer;
+use Jose\Component\Signature\Serializer\JWSSerializerManager;
use OCP\AppFramework\Utility\ITimeFactory;
use Psr\Log\LoggerInterface;
class TokenService {
- /** @var LoggerInterface */
- private $logger;
+ private LoggerInterface $logger;
+ private ITimeFactory $timeFactory;
- /** @var ITimeFactory */
- private $timeFactory;
+ // ✅ FIX: vorher dynamisch gesetzt
+ private JWEDecrypter $jweDecrypter;
+ private JWESerializerManager $encryptionSerializerManager;
+ private JWSVerifier $jwsVerifier;
+ private JWSSerializerManager $serializerManager;
- /** @var DiscoveryService */
- private $discoveryService;
-
- public function __construct(LoggerInterface $logger,
- ITimeFactory $timeFactory) {
+ public function __construct(LoggerInterface $logger, ITimeFactory $timeFactory) {
$this->logger = $logger;
$this->timeFactory = $timeFactory;
-
- // The key encryption algorithm manager with the A256KW algorithm.
+
+ // Key encryption algorithms
$keyEncryptionAlgorithmManager = new AlgorithmManager([
new PBES2HS512A256KW(),
new RSAOAEP256(),
- new ECDHESA256KW() ]);
-
- // The content encryption algorithm manager with the A256CBC-HS256 algorithm.
+ new ECDHESA256KW(),
+ ]);
+
+ // Content encryption algorithms
$contentEncryptionAlgorithmManager = new AlgorithmManager([
new A256CBCHS512(),
]);
-
- // The compression method manager with the DEF (Deflate) method.
+
+ // Compression methods
$compressionMethodManager = new CompressionMethodManager([
new Deflate(),
]);
-
+
+ // Signature algorithms
$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(),
+ $this->encryptionSerializerManager = new JWESerializerManager([
+ new JWECompactSerializer(),
]);
+ $this->jwsVerifier = new JWSVerifier($signatureAlgorithmManager);
- // 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(),
+ $this->serializerManager = new JWSSerializerManager([
+ new JWSCompactSerializer(),
]);
}
-
+
/**
* Implement JOSE decryption for SAM3 tokens
*/
- public function decryptToken(string $rawToken, string $decryptKey) : JWS {
-
+ 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
+ // so use it for encrypted web tokens
$clientSecret = new JWK([
'kty' => 'oct',
- 'k' => $decryptKey
+ '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);
+
+ throw new InvalidTokenException('Unknown bearer encryption format');
}
+
+ 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 {
+ 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;
+ $claimArray = $samContent->{'urn:telekom.com:idm:at:attributes'} ?? null;
+ if (is_iterable($claimArray)) {
+ foreach ($claimArray as $claimKeyValue) {
+ if (isset($claimKeyValue->name, $claimKeyValue->value)) {
+ $samContent->{'urn:telekom.com:' . $claimKeyValue->name} = $claimKeyValue->value;
+ }
+ }
}
unset($samContent->{'urn:telekom.com:idm:at:attributes'});
@@ -157,46 +139,51 @@ public function decode(JWS $decodedToken) : object {
return $samContent;
}
-
- public function verifySignature(JWS $decodedToken, string $signKey) {
+ public function verifySignature(JWS $decodedToken, string $signKey): void {
$accessSecret = new JWK([
'kty' => 'oct',
- 'k' => $signKey
- ]); // TODO: take the additional access key secret from settings
+ 'k' => $signKey,
+ ]);
if (!$this->jwsVerifier->verifyWithKey($decodedToken, $accessSecret, 0)) {
throw new SignatureException('Invalid Signature');
}
}
- public function verifyClaims(object $claims, array $audiences = [], int $leeway = 60) {
+ /**
+ * @param object $claims decoded token payload
+ * @param string[] $audiences acceptable audiences
+ */
+ public function verifyClaims(object $claims, array $audiences = [], int $leeway = 60): void {
$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)
+ 'Cannot handle token prior to ' . date(DateTime::ISO8601, (int)$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)
+ 'Cannot handle token prior to ' . date(DateTime::ISO8601, (int)$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))) {
+ // aud kann String ODER Array sein -> normalisieren
+ $tokenAud = [];
+ if (isset($claims->aud)) {
+ if (is_array($claims->aud)) {
+ $tokenAud = $claims->aud;
+ } elseif (is_string($claims->aud)) {
+ $tokenAud = [$claims->aud];
+ }
+ }
+
+ if (!empty($audiences) && empty(array_intersect($tokenAud, $audiences))) {
throw new InvalidTokenException('No acceptable audience in token.');
}
}
diff --git a/lib/Service/TokenService.php b/lib/Service/TokenService.php
index 1cab6845..e6a41936 100644
--- a/lib/Service/TokenService.php
+++ b/lib/Service/TokenService.php
@@ -25,7 +25,6 @@
use OCP\Authentication\Exceptions\WipeTokenException;
use OCP\Authentication\Token\IToken;
use OCP\EventDispatcher\IEventDispatcher;
-use OCP\Http\Client\IClient;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IRequest;
@@ -46,8 +45,6 @@ class TokenService {
private const SESSION_TOKEN_KEY = Application::APP_ID . '-user-token';
- private IClient $client;
-
public function __construct(
public HttpClientHelper $clientService,
private ISession $session,
@@ -64,7 +61,8 @@ public function __construct(
private DiscoveryService $discoveryService,
private ProviderMapper $providerMapper,
) {
-
+ // nothing else needed
+ // If you really need an IClient instance, inject it explicitly here and store it as a promoted property.
}
public function storeToken(array $tokenData): Token {
@@ -75,11 +73,6 @@ public function storeToken(array $tokenData): Token {
}
/**
- * Get the token stored in the session
- * If it has expired: try to refresh it
- *
- * @param bool $refreshIfExpired
- * @return Token|null Return a token only if it is valid or has been successfully refreshed
* @throws \JsonException
*/
public function getToken(bool $refreshIfExpired = true): ?Token {
@@ -91,16 +84,24 @@ public function getToken(bool $refreshIfExpired = true): ?Token {
}
$token = new Token(json_decode($sessionData, true, 512, JSON_THROW_ON_ERROR));
+
// token is still valid
if (!$token->isExpired()) {
- $this->logger->debug('[TokenService] getToken: token is still valid, it expires in ' . strval($token->getExpiresInFromNow()) . ' and refresh expires in ' . strval($token->getRefreshExpiresInFromNow()));
+ $this->logger->debug(
+ '[TokenService] getToken: token is still valid, it expires in ' .
+ (string)$token->getExpiresInFromNow() .
+ ' and refresh expires in ' .
+ (string)$token->getRefreshExpiresInFromNow()
+ );
return $token;
}
- // token has expired
- // try to refresh the token if there is a refresh token and it is still valid
+ // token has expired -> try refresh
if ($refreshIfExpired && $token->getRefreshToken() !== null && !$token->refreshIsExpired()) {
- $this->logger->debug('[TokenService] getToken: token is expired and refresh token is still valid, refresh expires in ' . strval($token->getRefreshExpiresInFromNow()));
+ $this->logger->debug(
+ '[TokenService] getToken: token is expired and refresh token is still valid, refresh expires in ' .
+ (string)$token->getRefreshExpiresInFromNow()
+ );
return $this->refresh($token);
}
@@ -109,9 +110,6 @@ public function getToken(bool $refreshIfExpired = true): ?Token {
}
/**
- * Check to make sure the login token is still valid
- *
- * @return void
* @throws \JsonException
* @throws PreConditionNotMetException
*/
@@ -132,22 +130,20 @@ public function checkLoginToken(): void {
return;
}
- // Do not check the OIDC login token when not logged in via user_oidc (app password or direct login for example)
- // Inspired from https://github.com/nextcloud/server/pull/43942/files#diff-c5cef03f925f97933ff9b3eb10217d21ef6516342e5628762756f1ba0469ac84R81-R92
try {
$sessionId = $this->session->getId();
$sessionAuthToken = $this->tokenProvider->getToken($sessionId);
} catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException $e) {
- // States we do not deal with here.
$this->logger->debug('[TokenService] checkLoginToken: error getting the session auth token', ['exception' => $e]);
return;
}
+
$scope = $sessionAuthToken->getScopeAsArray();
- // since 30, we still support 29
+
if (defined(IToken::class . '::SCOPE_SKIP_PASSWORD_VALIDATION')
&& (
!isset($scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION])
- || $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === false
+ || $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === false
)
) {
$this->logger->debug('[TokenService] checkLoginToken: most likely not using user_oidc, the session auth token does not have the "skip pwd validation" scope');
@@ -157,36 +153,31 @@ public function checkLoginToken(): void {
$token = $this->getToken();
if ($token === null) {
$this->logger->debug('[TokenService] checkLoginToken: token is null');
- // if we don't have a token but we had one once,
- // it means the session (where we store the token) has died
- // so we need to reauthenticate
$this->logger->debug('[TokenService] checkLoginToken: token is null and user had_token_once -> logout');
$this->userSession->logout();
return;
- } elseif ($token->isExpired()) {
+ }
+ if ($token->isExpired()) {
$this->logger->debug('[TokenService] checkLoginToken: token is still expired -> reauthenticate');
- // if the token is not valid, it means we couldn't refresh it so we need to reauthenticate to get a fresh token
$this->reauthenticate($token->getProviderId());
return;
}
+
$this->logger->debug('[TokenService] checkLoginToken: all good');
}
- public function reauthenticate(int $providerId) {
- // Logout the user and redirect to the oidc login flow to gather a fresh token
+ public function reauthenticate(int $providerId): void {
$this->userSession->logout();
$redirectUrl = $this->urlGenerator->linkToRouteAbsolute(Application::APP_ID . '.login.login', [
'providerId' => $providerId,
'redirectUrl' => $this->request->getRequestUri(),
]);
- header('Location: ' . $redirectUrl);
$this->logger->debug('[TokenService] reauthenticate', ['redirectUrl' => $redirectUrl]);
+ header('Location: ' . $redirectUrl);
exit();
}
/**
- * @param Token $token
- * @return Token
* @throws \JsonException
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
@@ -201,9 +192,10 @@ public function refresh(Token $token): Token {
try {
$clientSecret = $this->crypto->decrypt($clientSecret);
} catch (\Exception $e) {
- $this->logger->error('[TokenService] Failed to decrypt oidc client secret to refresh the token');
+ $this->logger->error('[TokenService] Failed to decrypt oidc client secret to refresh the token', ['exception' => $e]);
}
}
+
$this->logger->debug('[TokenService] Refreshing the token: ' . $discovery['token_endpoint']);
$body = $this->clientService->post(
$discovery['token_endpoint'],
@@ -214,24 +206,16 @@ public function refresh(Token $token): Token {
'refresh_token' => $token->getRefreshToken(),
]
);
- $this->logger->debug('[TokenService] Token refresh request params', [
- 'client_id' => $oidcProvider->getClientId(),
- // 'client_secret' => $clientSecret,
- 'grant_type' => 'refresh_token',
- // 'refresh_token' => $token->getRefreshToken(),
- ]);
$bodyArray = json_decode(trim($body), true, 512, JSON_THROW_ON_ERROR);
$this->logger->debug('[TokenService] ---- Refresh token success');
- return $this->storeToken(
- array_merge(
- $bodyArray,
- ['provider_id' => $token->getProviderId()],
- )
- );
- } catch (\Exception $e) {
+
+ return $this->storeToken(array_merge(
+ $bodyArray,
+ ['provider_id' => $token->getProviderId()],
+ ));
+ } catch (\Throwable $e) {
$this->logger->error('[TokenService] Failed to refresh token ', ['exception' => $e]);
- // Failed to refresh, return old token which will be retried or otherwise timeout if expired
return $token;
}
}
@@ -245,10 +229,6 @@ public function decodeIdToken(Token $token): array {
}
/**
- * Exchange the login token for another audience (client ID)
- *
- * @param string $targetAudience
- * @return Token
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws TokenExchangeFailedException
@@ -262,6 +242,7 @@ public function getExchangedToken(string $targetAudience, array $extraScopes = [
0,
);
}
+
$this->logger->debug('[TokenService] Starting token exchange');
$loginToken = $this->getToken();
@@ -273,8 +254,10 @@ public function getExchangedToken(string $targetAudience, array $extraScopes = [
$this->logger->debug('[TokenService] Failed to exchange token, the login token is expired');
throw new TokenExchangeFailedException('Failed to exchange token, the login token is expired');
}
+
$oidcProvider = $this->providerMapper->getProvider($loginToken->getProviderId());
$discovery = $this->discoveryService->obtainDiscovery($oidcProvider);
+
$scope = $oidcProvider->getScope();
if (!empty($extraScopes)) {
$scope .= ' ' . implode(' ', $extraScopes);
@@ -286,55 +269,48 @@ public function getExchangedToken(string $targetAudience, array $extraScopes = [
try {
$clientSecret = $this->crypto->decrypt($clientSecret);
} catch (\Exception $e) {
- $this->logger->error('[TokenService] Token Exchange: Failed to decrypt oidc client secret');
+ $this->logger->error('[TokenService] Token Exchange: Failed to decrypt oidc client secret', ['exception' => $e]);
}
}
+
$this->logger->debug('[TokenService] Exchanging the token: ' . $discovery['token_endpoint']);
+
$tokenEndpointParams = [
'client_id' => $oidcProvider->getClientId(),
'client_secret' => $clientSecret,
'grant_type' => 'urn:ietf:params:oauth:grant-type:token-exchange',
'subject_token' => $loginToken->getAccessToken(),
'subject_token_type' => 'urn:ietf:params:oauth:token-type:access_token',
- // can also be
- // urn:ietf:params:oauth:token-type:access_token
- // or urn:ietf:params:oauth:token-type:id_token
- // this one will get us an access token and refresh token within the response
'requested_token_type' => 'urn:ietf:params:oauth:token-type:refresh_token',
'audience' => $targetAudience,
'scope' => $scope,
];
+
$oidcConfig = $this->config->getSystemValue('user_oidc', []);
if (isset($oidcConfig['prompt']) && is_string($oidcConfig['prompt'])) {
- // none, consent, login and internal for oauth2 passport server
$tokenEndpointParams['prompt'] = $oidcConfig['prompt'];
}
- // more in https://www.keycloak.org/securing-apps/token-exchange
+
$body = $this->clientService->post(
$discovery['token_endpoint'],
$tokenEndpointParams,
);
- $this->logger->debug('[TokenService] Token exchange request params', [
- 'client_id' => $oidcProvider->getClientId(),
- // 'client_secret' => $clientSecret,
- 'grant_type' => 'urn:ietf:params:oauth:grant-type:token-exchange',
- // 'subject_token' => $loginToken->getAccessToken(),
- 'subject_token_type' => 'urn:ietf:params:oauth:token-type:access_token',
- 'requested_token_type' => 'urn:ietf:params:oauth:token-type:refresh_token',
- 'audience' => $targetAudience,
- ]);
$bodyArray = json_decode(trim($body), true, 512, JSON_THROW_ON_ERROR);
- $this->logger->debug('[TokenService] Token exchange success: "' . trim($body) . '"');
+
$tokenData = array_merge(
$bodyArray,
['provider_id' => $loginToken->getProviderId()],
);
+
return new Token($tokenData);
} catch (ClientException|ServerException $e) {
$response = $e->getResponse();
$body = (string)$response->getBody();
- $this->logger->error('[TokenService] Failed to exchange token, client/server error in the exchange request', ['response_body' => $body, 'exception' => $e]);
+ $this->logger->error('[TokenService] Failed to exchange token, client/server error in the exchange request', [
+ 'response_body' => $body,
+ 'exception' => $e,
+ ]);
$parsedBody = json_decode(trim($body), true);
if (is_array($parsedBody) && isset($parsedBody['error'], $parsedBody['error_description'])) {
@@ -345,26 +321,19 @@ public function getExchangedToken(string $targetAudience, array $extraScopes = [
$parsedBody['error'],
$parsedBody['error_description'],
);
- } else {
- throw new TokenExchangeFailedException(
- 'Failed to exchange token, client/server error in the exchange request: ' . $body,
- 0,
- $e,
- );
}
- } catch (\Exception|\Throwable $e) {
+
+ throw new TokenExchangeFailedException(
+ 'Failed to exchange token, client/server error in the exchange request: ' . $body,
+ 0,
+ $e,
+ );
+ } catch (\Throwable $e) {
$this->logger->error('[TokenService] Failed to exchange token ', ['exception' => $e]);
throw new TokenExchangeFailedException('Failed to exchange token, error in the exchange request', 0, $e);
}
}
- /**
- * Try to get a token from the Oidc provider app for a user and a specific audience (client ID)
- *
- * @param string $userId
- * @param string $targetAudience
- * @return Token|null
- */
public function getTokenFromOidcProviderApp(string $userId, string $targetAudience, array $extraScopes = [], string $resource = ''): ?Token {
if (!class_exists(\OCA\OIDCIdentityProvider\AppInfo\Application::class)) {
$this->logger->warning('[TokenService] Failed to get token from Oidc provider app, oidc app is not installed');
@@ -383,12 +352,13 @@ public function getTokenFromOidcProviderApp(string $userId, string $targetAudien
$scope = implode(' ', $extraScopes);
$generationEvent = new \OCA\OIDCIdentityProvider\Event\TokenGenerationRequestEvent($targetAudience, $userId, $scope, $resource);
$this->eventDispatcher->dispatchTyped($generationEvent);
+
if ($generationEvent->getAccessToken() === null || $generationEvent->getIdToken() === null) {
$this->logger->debug('[TokenService] The Oidc provider app did not generate any access/id token');
return null;
}
- } catch (\Exception|\Throwable $e) {
- $this->logger->debug('[TokenService] The Oidc provider app failed to generate a token');
+ } catch (\Throwable $e) {
+ $this->logger->debug('[TokenService] The Oidc provider app failed to generate a token', ['exception' => $e]);
return null;
}
@@ -397,7 +367,6 @@ public function getTokenFromOidcProviderApp(string $userId, string $targetAudien
'id_token' => $generationEvent->getIdToken(),
'refresh_token' => $generationEvent->getRefreshToken(),
'expires_in' => $generationEvent->getExpiresIn(),
- // the getRefreshExpiresIn method will appear after oidc v1.4.0, see https://github.com/H2CK/oidc/pull/530
'refresh_expires_in' => method_exists($generationEvent, 'getRefreshExpiresIn')
? $generationEvent->getRefreshExpiresIn()
: $generationEvent->getExpiresIn(),
From 2d3cb70ca83b911e310ad4ef1089ddec32a6ebd5 Mon Sep 17 00:00:00 2001
From: memurats
Date: Fri, 12 Dec 2025 13:27:20 +0000
Subject: [PATCH 33/33] Add jwt-token composer library dependencies
---
composer.json | 12 +-
composer.lock | 1240 ++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 1249 insertions(+), 3 deletions(-)
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..db8dd2fd 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": [
@@ -2609,5 +3845,5 @@
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.9.0"
}