Repository: owncloud/ocis
Pull request #6007 (2023-04-04 14:01:41)

[full-ci] Implement backchannel logout
This PR adds an endpoint for the backchannel logout `/backchannel_logout`.
It can be triggered via a POST request with a special crafted logout token see: [https://openid.net/specs/openid-connect-backchannel-1_0.html](https://openid.net/specs/openid-connect-backchannel-1_0.html) section 2.4 for the structure.

*NOTE:* we have added a special http-handler wrapper to the proxy to intercept predefined routes in the proxy before they are passed on to the services! [https://github.com/dragonchaser/ocis/blob/backchannellogout/services/proxy/pkg/command/server.go#L200](https://github.com/dragonchaser/ocis/blob/backchannellogout/services/proxy/pkg/command/server.go#L200). In future this should be used for terminating stuff like oidc-well-knowns to avoid implementation as middlewares.

refs #3521 

update-docs[bot]: Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a [changelog](https://github.com/owncloud/ocis/blob/master/changelog/README.md) item based on your changes.

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6007)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list)


ownclouders: :boom: Acceptance test [<strong>localApiTests-apiContract-ocis</strong>](https://drone.owncloud.com/owncloud/ocis/21125/50/1) failed. Further test are cancelled...
<!-- id: pr-6007 -->

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6007)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6007)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [9 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL)

[![27.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '27.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list) [27.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6007)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [9 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL)

[![27.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '27.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list) [27.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6007)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [9 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL)

[![27.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '27.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list) [27.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6007)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [9 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL)

[![27.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '27.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list) [27.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6007)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [9 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL)

[![27.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '27.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list) [27.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=6007)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=6007&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL) [9 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=6007&resolved=false&types=CODE_SMELL)

[![27.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '27.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list) [27.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6007&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8bdb869e653..c2cf0ac7960 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,6 @@ The following sections list the changes for unreleased.
 * Bugfix - Fix so that PATCH requests for groups actually updates the group name: [#5949](https://github.com/owncloud/ocis/pull/5949)
 * Bugfix - Fix OIDC auth cache: [#5997](https://github.com/owncloud/ocis/pull/5997)
 * Bugfix - Fix Postprocessing events: [#5269](https://github.com/owncloud/ocis/pull/5269)
-* Bugfix - Fix Search reindexing performance regression: [#6085](https://github.com/owncloud/ocis/pull/6085)
 * Bugfix - Fix Search tag indexing: [#5405](https://github.com/owncloud/ocis/pull/5405)
 * Bugfix - Fix user type config for user provider: [#6027](https://github.com/owncloud/ocis/pull/6027)
 * Bugfix - Add missing CORS config: [#5987](https://github.com/owncloud/ocis/pull/5987)
diff --git a/changelog/unreleased/fix-search-reindexing-performance-regression.md b/changelog/unreleased/fix-search-reindexing-performance-regression.md
deleted file mode 100644
index 463891d0414..00000000000
--- a/changelog/unreleased/fix-search-reindexing-performance-regression.md
+++ /dev/null
@@ -1,6 +0,0 @@
-Bugfix: Fix Search reindexing performance regression
-
-We've fixed a regression in the search service reindexing step, causing the
-whole space to be reindexed instead of just the changed resources.
-
-https://github.com/owncloud/ocis/pull/6085
diff --git a/docs/services/general-info/_index.md b/docs/services/general-info/_index.md
index 8233f56a47c..f89b928d2b8 100644
--- a/docs/services/general-info/_index.md
+++ b/docs/services/general-info/_index.md
@@ -1,5 +1,5 @@
 ---
-title: General Information
+title: Genreal Information
 date: 2023-04-12T00:00:00+00:00
 weight: 10
 geekdocRepo: https://github.com/owncloud/ocis
diff --git a/go.mod b/go.mod
index 89ce1d9dd4f..5ab0d7b9637 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
 	github.com/armon/go-radix v1.0.0
 	github.com/bbalet/stopwords v1.0.0
 	github.com/blevesearch/bleve/v2 v2.3.7
+	github.com/coreos/go-oidc v2.2.1+incompatible
 	github.com/coreos/go-oidc/v3 v3.4.0
 	github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965
 	github.com/cs3org/reva/v2 v2.12.1-0.20230417084429-b3d96f9db80c
@@ -100,6 +101,7 @@ require (
 	google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f
 	google.golang.org/grpc v1.54.0
 	google.golang.org/protobuf v1.28.1
+	gopkg.in/square/go-jose.v2 v2.6.0
 	gopkg.in/yaml.v2 v2.4.0
 	gotest.tools/v3 v3.4.0
 	stash.kopano.io/kgol/oidc-go v0.3.4
@@ -153,7 +155,6 @@ require (
 	github.com/cilium/ebpf v0.7.0 // indirect
 	github.com/cloudflare/circl v1.2.0 // indirect
 	github.com/containerd/cgroups v1.0.4 // indirect
-	github.com/coreos/go-oidc v2.2.1+incompatible // indirect
 	github.com/coreos/go-semver v0.3.0 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
@@ -313,7 +314,6 @@ require (
 	golang.org/x/tools v0.7.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
-	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index c6eb35b38e6..2d8afefe80e 100644
--- a/go.sum
+++ b/go.sum
@@ -627,8 +627,12 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
 github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
 github.com/crewjam/saml v0.4.13 h1:TYHggH/hwP7eArqiXSJUvtOPNzQDyQ7vwmwEqlFWhMc=
 github.com/crewjam/saml v0.4.13/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
+github.com/cs3org/reva/v2 v2.12.1-0.20230404090709-bb973fae26ae h1:APfYubzIYqCTXtmX6cAm4c8wBYS3R/cZwomX8IlXLaI=
+github.com/cs3org/reva/v2 v2.12.1-0.20230404090709-bb973fae26ae/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
 github.com/cs3org/reva/v2 v2.12.1-0.20230417084429-b3d96f9db80c h1:H6OjKTaRowZfAU/Hwvv4W0pLFFH/KNbHaNVNw3ANoHU=
 github.com/cs3org/reva/v2 v2.12.1-0.20230417084429-b3d96f9db80c/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
+github.com/cs3org/reva/v2 v2.12.1-0.20230331184913-eca8953fb6e9 h1:zUMD0UvKVPbR3UaodnA0GXuBycxrIuaO8Kshgi3iKKI=
+github.com/cs3org/reva/v2 v2.12.1-0.20230331184913-eca8953fb6e9/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
 github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI=
 github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY=
 github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
diff --git a/ocis-pkg/keycloak/client.go b/ocis-pkg/keycloak/client.go
index 8b191d5dc02..f88f2b1421e 100644
--- a/ocis-pkg/keycloak/client.go
+++ b/ocis-pkg/keycloak/client.go
@@ -90,39 +90,34 @@ func (c *ConcreteClient) SendActionsMail(ctx context.Context, realm, userID stri
 	return c.keycloak.ExecuteActionsEmail(ctx, token.AccessToken, realm, params)
 }
 
-// getUserByParams looks up a user by the given parameters.
-func (c *ConcreteClient) getUserByParams(ctx context.Context, realm string, params gocloak.GetUsersParams) (*libregraph.User, error) {
+// GetUserByEmail looks up a user by email.
+func (c *ConcreteClient) GetUserByEmail(ctx context.Context, realm, mail string) (*libregraph.User, error) {
 	token, err := c.getToken(ctx)
 	if err != nil {
 		return nil, err
 	}
 
-	users, err := c.keycloak.GetUsers(ctx, token.AccessToken, realm, params)
+	users, err := c.keycloak.GetUsers(ctx, token.AccessToken, realm, gocloak.GetUsersParams{
+		Email: &mail,
+	})
 	if err != nil {
 		return nil, err
 	}
 
 	if len(users) == 0 {
-		return nil, fmt.Errorf("no users found")
+		return nil, fmt.Errorf("no users found with mail address %s", mail)
 	}
 
 	if len(users) > 1 {
-		return nil, fmt.Errorf("%d users found", len(users))
+		return nil, fmt.Errorf("%d users found with mail address %s, expected 1", len(users), mail)
 	}
 
 	return c.keycloakUserToLibregraph(users[0]), nil
 }
 
-// GetUserByUsername looks up a user by username.
-func (c *ConcreteClient) GetUserByUsername(ctx context.Context, realm, username string) (*libregraph.User, error) {
-	return c.getUserByParams(ctx, realm, gocloak.GetUsersParams{
-		Username: &username,
-	})
-}
-
 // GetPIIReport returns a structure with all the PII for the user.
-func (c *ConcreteClient) GetPIIReport(ctx context.Context, realm, username string) (*PIIReport, error) {
-	u, err := c.GetUserByUsername(ctx, realm, username)
+func (c *ConcreteClient) GetPIIReport(ctx context.Context, realm string, email string) (*PIIReport, error) {
+	u, err := c.GetUserByEmail(ctx, realm, email)
 	if err != nil {
 		return nil, err
 	}
diff --git a/ocis-pkg/keycloak/types.go b/ocis-pkg/keycloak/types.go
index 632d0975c0f..9018938232d 100644
--- a/ocis-pkg/keycloak/types.go
+++ b/ocis-pkg/keycloak/types.go
@@ -34,6 +34,6 @@ type PIIReport struct {
 type Client interface {
 	CreateUser(ctx context.Context, realm string, user *libregraph.User, userActions []UserAction) (string, error)
 	SendActionsMail(ctx context.Context, realm, userID string, userActions []UserAction) error
-	GetUserByUsername(ctx context.Context, realm, username string) (*libregraph.User, error)
-	GetPIIReport(ctx context.Context, realm, username string) (*PIIReport, error)
+	GetUserByEmail(ctx context.Context, realm, email string) (*libregraph.User, error)
+	GetPIIReport(ctx context.Context, realm string, email string) (*PIIReport, error)
 }
diff --git a/ocis-pkg/middleware/oidc.go b/ocis-pkg/middleware/oidc.go
index 2c1dd5b194d..4bab746e2bf 100644
--- a/ocis-pkg/middleware/oidc.go
+++ b/ocis-pkg/middleware/oidc.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 	"sync"
 
-	gOidc "github.com/coreos/go-oidc/v3/oidc"
+	goidc "github.com/coreos/go-oidc/v3/oidc"
 	"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
 	"golang.org/x/oauth2"
 )
@@ -24,7 +24,7 @@ func newOidcOptions(opts ...Option) Options {
 
 // OIDCProvider used to mock the oidc provider during tests
 type OIDCProvider interface {
-	UserInfo(ctx context.Context, ts oauth2.TokenSource) (*gOidc.UserInfo, error)
+	UserInfo(ctx context.Context, ts oauth2.TokenSource) (*goidc.UserInfo, error)
 }
 
 // OidcAuth provides a middleware to authenticate a bearer auth with an OpenID Connect identity provider
@@ -38,7 +38,7 @@ func OidcAuth(opts ...Option) func(http.Handler) http.Handler {
 		// Initialize a provider by specifying the issuer URL.
 		// it will fetch the keys from the issuer using the .well-known
 		// endpoint
-		return gOidc.NewProvider(
+		return goidc.NewProvider(
 			context.WithValue(context.Background(), oauth2.HTTPClient, http.Client{}),
 			opt.OidcIssuer,
 		)
diff --git a/ocis-pkg/oidc/client.go b/ocis-pkg/oidc/client.go
new file mode 100644
index 00000000000..f06837a155d
--- /dev/null
+++ b/ocis-pkg/oidc/client.go
@@ -0,0 +1,438 @@
+package oidc
+
+import (
+	"bytes"
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"mime"
+	"net/http"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/MicahParks/keyfunc"
+	goidc "github.com/coreos/go-oidc/v3/oidc"
+	"github.com/golang-jwt/jwt/v4"
+	"github.com/owncloud/ocis/v2/ocis-pkg/log"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
+	"golang.org/x/oauth2"
+	"gopkg.in/square/go-jose.v2"
+)
+
+// OIDCClient used to mock the oidc client during tests
+type OIDCClient interface {
+	UserInfo(ctx context.Context, ts oauth2.TokenSource) (*UserInfo, error)
+	VerifyAccessToken(ctx context.Context, token string) (jwt.RegisteredClaims, []string, error)
+	VerifyLogoutToken(ctx context.Context, token string) (*LogoutToken, error)
+}
+
+// KeySet is a set of public JSON Web Keys that can be used to validate the signature
+// of JSON web tokens. This is expected to be backed by a remote key set through
+// provider metadata discovery or an in-memory set of keys delivered out-of-band.
+type KeySet interface {
+	// VerifySignature parses the JSON web token, verifies the signature, and returns
+	// the raw payload. Header and claim fields are validated by other parts of the
+	// package. For example, the KeySet does not need to check values such as signature
+	// algorithm, issuer, and audience since the IDTokenVerifier validates these values
+	// independently.
+	//
+	// If VerifySignature makes HTTP requests to verify the token, it's expected to
+	// use any HTTP client associated with the context through ClientContext.
+	VerifySignature(ctx context.Context, jwt string) (payload []byte, err error)
+}
+
+type oidcClient struct {
+	// Logger to use for logging, must be set
+	Logger log.Logger
+
+	clientID                string
+	skipClientIDCheck       bool
+	issuer                  string
+	provider                *ProviderMetadata
+	providerLock            *sync.Mutex
+	skipIssuerValidation    bool
+	accessTokenVerifyMethod string
+	remoteKeySet            KeySet
+	algorithms              []string
+
+	JWKSOptions config.JWKS
+	JWKS        *keyfunc.JWKS
+	jwksLock    *sync.Mutex
+
+	httpClient *http.Client
+}
+
+// _supportedAlgorithms is a list of algorithms explicitly supported by this
+// package. If a provider supports other algorithms, such as HS256 or none,
+// those values won't be passed to the IDTokenVerifier.
+var _supportedAlgorithms = map[string]bool{
+	RS256: true,
+	RS384: true,
+	RS512: true,
+	ES256: true,
+	ES384: true,
+	ES512: true,
+	PS256: true,
+	PS384: true,
+	PS512: true,
+}
+
+// NewOIDCClient returns an OIDClient instance for the given issuer
+func NewOIDCClient(opts ...Option) OIDCClient {
+	options := newOptions(opts...)
+
+	return &oidcClient{
+		Logger:                  options.Logger,
+		issuer:                  options.OIDCIssuer,
+		httpClient:              options.HTTPClient,
+		accessTokenVerifyMethod: options.AccessTokenVerifyMethod,
+		JWKSOptions:             options.JWKSOptions, // TODO I don't like that we pass down config options ...
+		providerLock:            &sync.Mutex{},
+		jwksLock:                &sync.Mutex{},
+		clientID:                options.ClientID,
+		skipClientIDCheck:       options.SkipClientIDCheck,
+		remoteKeySet:            options.KeySet,
+		provider:                options.ProviderMetadata,
+	}
+}
+
+func (c *oidcClient) lookupWellKnownOpenidConfiguration(ctx context.Context) error {
+	c.providerLock.Lock()
+	defer c.providerLock.Unlock()
+	if c.provider == nil {
+		wellKnown := strings.TrimSuffix(c.issuer, "/") + wellknownPath
+		req, err := http.NewRequest("GET", wellKnown, nil)
+		if err != nil {
+			return err
+		}
+		resp, err := c.httpClient.Do(req.WithContext(ctx))
+		if err != nil {
+			return err
+		}
+		defer resp.Body.Close()
+
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return fmt.Errorf("unable to read response body: %v", err)
+		}
+
+		if resp.StatusCode != http.StatusOK {
+			return fmt.Errorf("%s: %s", resp.Status, body)
+		}
+
+		var p ProviderMetadata
+		err = unmarshalResp(resp, body, &p)
+		if err != nil {
+			return fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
+		}
+
+		if !c.skipIssuerValidation && p.Issuer != c.issuer {
+			return fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", c.issuer, p.Issuer)
+		}
+		var algs []string
+		for _, a := range p.IDTokenSigningAlgValuesSupported {
+			if _supportedAlgorithms[a] {
+				algs = append(algs, a)
+			}
+		}
+		c.provider = &p
+		c.algorithms = algs
+		c.remoteKeySet = goidc.NewRemoteKeySet(goidc.ClientContext(ctx, c.httpClient), p.JwksURI)
+	}
+	return nil
+}
+
+func (c *oidcClient) getKeyfunc() *keyfunc.JWKS {
+	c.jwksLock.Lock()
+	defer c.jwksLock.Unlock()
+	if c.JWKS == nil {
+		var err error
+		c.Logger.Debug().Str("jwks", c.provider.JwksURI).Msg("discovered jwks endpoint")
+		options := keyfunc.Options{
+			Client: c.httpClient,
+			RefreshErrorHandler: func(err error) {
+				c.Logger.Error().Err(err).Msg("There was an error with the jwt.Keyfunc")
+			},
+			RefreshInterval:   time.Minute * time.Duration(c.JWKSOptions.RefreshInterval),
+			RefreshRateLimit:  time.Second * time.Duration(c.JWKSOptions.RefreshRateLimit),
+			RefreshTimeout:    time.Second * time.Duration(c.JWKSOptions.RefreshTimeout),
+			RefreshUnknownKID: c.JWKSOptions.RefreshUnknownKID,
+		}
+		c.JWKS, err = keyfunc.Get(c.provider.JwksURI, options)
+		if err != nil {
+			c.JWKS = nil
+			c.Logger.Error().Err(err).Msg("Failed to create JWKS from resource at the given URL.")
+			return nil
+		}
+	}
+	return c.JWKS
+}
+
+type stringAsBool bool
+
+// Claims unmarshals the raw JSON string into a bool.
+func (sb *stringAsBool) UnmarshalJSON(b []byte) error {
+	v, err := strconv.ParseBool(string(b))
+	if err != nil {
+		return err
+	}
+	*sb = stringAsBool(v)
+	return nil
+}
+
+// UserInfo represents the OpenID Connect userinfo claims.
+type UserInfo struct {
+	Subject       string `json:"sub"`
+	Profile       string `json:"profile"`
+	Email         string `json:"email"`
+	EmailVerified bool   `json:"email_verified"`
+
+	claims []byte
+}
+
+type userInfoRaw struct {
+	Subject string `json:"sub"`
+	Profile string `json:"profile"`
+	Email   string `json:"email"`
+	// Handle providers that return email_verified as a string
+	// https://forums.aws.amazon.com/thread.jspa?messageID=949441&#949441 and
+	// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
+	EmailVerified stringAsBool `json:"email_verified"`
+}
+
+// Claims unmarshals the raw JSON object claims into the provided object.
+func (u *UserInfo) Claims(v interface{}) error {
+	if u.claims == nil {
+		return errors.New("oidc: claims not set")
+	}
+	return json.Unmarshal(u.claims, v)
+}
+
+// UserInfo retrieves the userinfo from a Token
+func (c *oidcClient) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) {
+	if err := c.lookupWellKnownOpenidConfiguration(ctx); err != nil {
+		return nil, err
+	}
+
+	if c.provider.UserinfoEndpoint == "" {
+		return nil, errors.New("oidc: user info endpoint is not supported by this provider")
+	}
+
+	req, err := http.NewRequest("GET", c.provider.UserinfoEndpoint, nil)
+	if err != nil {
+		return nil, fmt.Errorf("oidc: create GET request: %v", err)
+	}
+
+	token, err := tokenSource.Token()
+	if err != nil {
+		return nil, fmt.Errorf("oidc: get access token: %v", err)
+	}
+	token.SetAuthHeader(req)
+
+	resp, err := c.httpClient.Do(req.WithContext(ctx))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("%s: %s", resp.Status, body)
+	}
+
+	ct := resp.Header.Get("Content-Type")
+	mediaType, _, err := mime.ParseMediaType(ct)
+	if err == nil && mediaType == "application/jwt" {
+		payload, err := c.remoteKeySet.VerifySignature(goidc.ClientContext(ctx, c.httpClient), string(body))
+		if err != nil {
+			return nil, fmt.Errorf("oidc: invalid userinfo jwt signature %v", err)
+		}
+		body = payload
+	}
+
+	var userInfo userInfoRaw
+	if err := json.Unmarshal(body, &userInfo); err != nil {
+		return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err)
+	}
+	return &UserInfo{
+		Subject:       userInfo.Subject,
+		Profile:       userInfo.Profile,
+		Email:         userInfo.Email,
+		EmailVerified: bool(userInfo.EmailVerified),
+		claims:        body,
+	}, nil
+}
+
+func (c *oidcClient) VerifyAccessToken(ctx context.Context, token string) (jwt.RegisteredClaims, []string, error) {
+	var mapClaims []string
+	if err := c.lookupWellKnownOpenidConfiguration(ctx); err != nil {
+		return jwt.RegisteredClaims{}, mapClaims, err
+	}
+	switch c.accessTokenVerifyMethod {
+	case config.AccessTokenVerificationJWT:
+		return c.verifyAccessTokenJWT(token)
+	case config.AccessTokenVerificationNone:
+		c.Logger.Debug().Msg("Access Token verification disabled")
+		return jwt.RegisteredClaims{}, mapClaims, nil
+	default:
+		c.Logger.Error().Str("access_token_verify_method", c.accessTokenVerifyMethod).Msg("Unknown Access Token verification setting")
+		return jwt.RegisteredClaims{}, mapClaims, errors.New("unknown Access Token Verification method")
+	}
+}
+
+// verifyAccessTokenJWT tries to parse and verify the access token as a JWT.
+func (c *oidcClient) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, []string, error) {
+	var claims jwt.RegisteredClaims
+	var mapClaims []string
+	jwks := c.getKeyfunc()
+	if jwks == nil {
+		return claims, mapClaims, errors.New("error initializing jwks keyfunc")
+	}
+
+	_, err := jwt.ParseWithClaims(token, &claims, jwks.Keyfunc)
+	if err != nil {
+		return claims, mapClaims, err
+	}
+	_, mapClaims, err = new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
+	// TODO: decode mapClaims to sth readable
+	c.Logger.Debug().Interface("access token", &claims).Msg("parsed access token")
+	if err != nil {
+		c.Logger.Info().Err(err).Msg("Failed to parse/verify the access token.")
+		return claims, mapClaims, err
+	}
+
+	if !claims.VerifyIssuer(c.issuer, true) {
+		vErr := jwt.ValidationError{}
+		vErr.Inner = jwt.ErrTokenInvalidIssuer
+		vErr.Errors |= jwt.ValidationErrorIssuer
+		return claims, mapClaims, vErr
+	}
+
+	return claims, mapClaims, nil
+}
+
+func (c *oidcClient) VerifyLogoutToken(ctx context.Context, rawToken string) (*LogoutToken, error) {
+	if err := c.lookupWellKnownOpenidConfiguration(ctx); err != nil {
+		return nil, err
+	}
+	jws, err := jose.ParseSigned(rawToken)
+	if err != nil {
+		return nil, err
+	}
+	// Throw out tokens with invalid claims before trying to verify the token. This lets
+	// us do cheap checks before possibly re-syncing keys.
+	payload, err := parseJWT(rawToken)
+	if err != nil {
+		return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
+	}
+	var token LogoutToken
+	if err := json.Unmarshal(payload, &token); err != nil {
+		return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
+	}
+
+	//4. Verify that the Logout Token contains a sub Claim, a sid Claim, or both.
+	if token.Subject == "" && token.SessionId == "" {
+		return nil, fmt.Errorf("oidc: logout token must contain either sub or sid and MAY contain both")
+	}
+	//5. Verify that the Logout Token contains an events Claim whose value is JSON object containing the member name http://schemas.openid.net/event/backchannel-logout.
+	if token.Events.Event == nil {
+		return nil, fmt.Errorf("oidc: logout token must contain logout event")
+	}
+	//6. Verify that the Logout Token does not contain a nonce Claim.
+	var n struct {
+		Nonce *string `json:"nonce"`
+	}
+	json.Unmarshal(payload, &n)
+	if n.Nonce != nil {
+		return nil, fmt.Errorf("oidc: nonce on logout token MUST NOT be present")
+	}
+	// Check issuer.
+	if !c.skipIssuerValidation && token.Issuer != c.issuer {
+		return nil, fmt.Errorf("oidc: logout token issued by a different provider, expected %q got %q", c.issuer, token.Issuer)
+	}
+
+	// If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty.
+	//
+	// This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party).
+	if !c.skipClientIDCheck {
+		if c.clientID != "" {
+			if !contains(token.Audience, c.clientID) {
+				return nil, fmt.Errorf("oidc: expected audience %q got %q", c.clientID, token.Audience)
+			}
+		} else {
+			return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
+		}
+	}
+
+	switch len(jws.Signatures) {
+	case 0:
+		return nil, fmt.Errorf("oidc: logout token not signed")
+	case 1:
+		// do nothing
+	default:
+		return nil, fmt.Errorf("oidc: multiple signatures on logout token not supported")
+	}
+
+	sig := jws.Signatures[0]
+	supportedSigAlgs := c.algorithms
+	if len(supportedSigAlgs) == 0 {
+		supportedSigAlgs = []string{RS256}
+	}
+
+	if !contains(supportedSigAlgs, sig.Header.Algorithm) {
+		return nil, fmt.Errorf("oidc: logout token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
+	}
+
+	gotPayload, err := c.remoteKeySet.VerifySignature(goidc.ClientContext(ctx, c.httpClient), rawToken)
+	if err != nil {
+		return nil, fmt.Errorf("failed to verify signature: %v", err)
+	}
+
+	// Ensure that the payload returned by the square actually matches the payload parsed earlier.
+	if !bytes.Equal(gotPayload, payload) {
+		return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
+	}
+
+	return &token, nil
+}
+
+func unmarshalResp(r *http.Response, body []byte, v interface{}) error {
+	err := json.Unmarshal(body, &v)
+	if err == nil {
+		return nil
+	}
+	ct := r.Header.Get("Content-Type")
+	mediaType, _, err := mime.ParseMediaType(ct)
+	if err == nil && mediaType == "application/json" {
+		return fmt.Errorf("got Content-Type = application/json, but could not unmarshal as JSON: %v", err)
+	}
+	return fmt.Errorf("expected Content-Type = application/json, got %q: %v", ct, err)
+}
+
+func contains(sli []string, ele string) bool {
+	for _, s := range sli {
+		if s == ele {
+			return true
+		}
+	}
+	return false
+}
+
+func parseJWT(p string) ([]byte, error) {
+	parts := strings.Split(p, ".")
+	if len(parts) < 2 {
+		return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
+	}
+	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
+	if err != nil {
+		return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
+	}
+	return payload, nil
+}
diff --git a/ocis-pkg/oidc/jose.go b/ocis-pkg/oidc/jose.go
new file mode 100644
index 00000000000..8afa895c1cc
--- /dev/null
+++ b/ocis-pkg/oidc/jose.go
@@ -0,0 +1,16 @@
+package oidc
+
+// JOSE asymmetric signing algorithm values as defined by RFC 7518
+//
+// see: https://tools.ietf.org/html/rfc7518#section-3.1
+const (
+	RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256
+	RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384
+	RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512
+	ES256 = "ES256" // ECDSA using P-256 and SHA-256
+	ES384 = "ES384" // ECDSA using P-384 and SHA-384
+	ES512 = "ES512" // ECDSA using P-521 and SHA-512
+	PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256
+	PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384
+	PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512
+)
diff --git a/ocis-pkg/oidc/metadata.go b/ocis-pkg/oidc/metadata.go
index bcbecaf8221..c10db68b6e8 100644
--- a/ocis-pkg/oidc/metadata.go
+++ b/ocis-pkg/oidc/metadata.go
@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"strings"
 
+	"github.com/golang-jwt/jwt/v4"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 )
 
@@ -53,6 +54,44 @@ type ProviderMetadata struct {
 	//claim_types_supported
 }
 
+// Logout Token defines an logout Token
+type LogoutToken struct {
+	// The URL of the server which issued this token. OpenID Connect
+	// requires this value always be identical to the URL used for
+	// initial discovery.
+	//
+	// Note: Because of a known issue with Google Accounts' implementation
+	// this value may differ when using Google.
+	//
+	// See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo
+	Issuer string `json:"iss"` // example "https://server.example.com"
+
+	// A unique string which identifies the end user.
+	Subject string `json:"sub"` //"248289761001"
+
+	// The client ID, or set of client IDs, that this token is issued for. For
+	// common uses, this is the client that initialized the auth flow.
+	//
+	// This package ensures the audience contains an expected value.
+	Audience jwt.ClaimStrings `json:"aud"` // "s6BhdRkqt3"
+
+	// When the token was issued by the provider.
+	IssuedAt *jwt.NumericDate `json:"iat"`
+
+	// The Session Id
+	SessionId string `json:"sid"`
+
+	Events LogoutEvent `json:"events"`
+
+	// Jwt Id
+	JwtID string `json:"jti"`
+}
+
+// LogoutEvent defines a logout Event
+type LogoutEvent struct {
+	Event *struct{} `json:"http://schemas.openid.net/event/backchannel-logout"`
+}
+
 func GetIDPMetadata(logger log.Logger, client *http.Client, idpURI string) (ProviderMetadata, error) {
 	wellknownURI := strings.TrimSuffix(idpURI, "/") + wellknownPath
 
diff --git a/ocis-pkg/oidc/mocks/OIDCProvider.go b/ocis-pkg/oidc/mocks/OIDCProvider.go
new file mode 100644
index 00000000000..a0396d819b6
--- /dev/null
+++ b/ocis-pkg/oidc/mocks/OIDCProvider.go
@@ -0,0 +1,58 @@
+// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	mock "github.com/stretchr/testify/mock"
+	oauth2 "golang.org/x/oauth2"
+
+	oidc "github.com/coreos/go-oidc"
+)
+
+// OIDCProvider is an autogenerated mock type for the OIDCProvider type
+type OIDCProvider struct {
+	mock.Mock
+}
+
+// UserInfo provides a mock function with given fields: ctx, ts
+func (_m *OIDCProvider) UserInfo(ctx context.Context, ts oauth2.TokenSource) (*oidc.UserInfo, error) {
+	ret := _m.Called(ctx, ts)
+
+	var r0 *oidc.UserInfo
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, oauth2.TokenSource) (*oidc.UserInfo, error)); ok {
+		return rf(ctx, ts)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, oauth2.TokenSource) *oidc.UserInfo); ok {
+		r0 = rf(ctx, ts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*oidc.UserInfo)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, oauth2.TokenSource) error); ok {
+		r1 = rf(ctx, ts)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+type mockConstructorTestingTNewOIDCProvider interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewOIDCProvider creates a new instance of OIDCProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewOIDCProvider(t mockConstructorTestingTNewOIDCProvider) *OIDCProvider {
+	mock := &OIDCProvider{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/ocis-pkg/oidc/options.go b/ocis-pkg/oidc/options.go
new file mode 100644
index 00000000000..86439532aef
--- /dev/null
+++ b/ocis-pkg/oidc/options.go
@@ -0,0 +1,121 @@
+package oidc
+
+import (
+	"net/http"
+
+	"github.com/owncloud/ocis/v2/ocis-pkg/log"
+	"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
+
+	goidc "github.com/coreos/go-oidc/v3/oidc"
+)
+
+// Option defines a single option function.
+type Option func(o *Options)
+
+// Options defines the available options for this package.
+type Options struct {
+	// HTTPClient to use for requests
+	HTTPClient *http.Client
+	// Logger to use for logging, must be set
+	Logger log.Logger
+	// The OpenID Connect Issuer URL
+	OIDCIssuer string
+	// JWKSOptions to use when retrieving keys
+	JWKSOptions config.JWKS
+	// KeySet to use when verifiing signatures
+	KeySet KeySet
+	// AccessTokenVerifyMethod to use when verifying access tokens
+	// TODO pass a function or interface to verify? an AccessTokenVerifier?
+	AccessTokenVerifyMethod string
+	// ClientID the client id to expect in tokens. If not set SkipClientIDCheck must be true
+	// TODO also check in access token
+	ClientID string
+	// SkipClientIDCheck must be true if ClientID is empty
+	SkipClientIDCheck bool
+	// Config to use
+	Config *goidc.Config
+
+	// ProviderMetadata to use
+	ProviderMetadata *ProviderMetadata
+}
+
+// newOptions initializes the available default options.
+func newOptions(opts ...Option) Options {
+	opt := Options{}
+
+	for _, o := range opts {
+		o(&opt)
+	}
+
+	return opt
+}
+
+// WithOidcIssuer provides a function to set the openid connect issuer option.
+func WithOidcIssuer(val string) Option {
+	return func(o *Options) {
+		o.OIDCIssuer = val
+	}
+}
+
+// WithLogger provides a function to set the logger option.
+func WithLogger(val log.Logger) Option {
+	return func(o *Options) {
+		o.Logger = val
+	}
+}
+
+// WithAccessTokenVerifyMethod provides a function to set the accessTokenVerifyMethod option.
+func WithAccessTokenVerifyMethod(val string) Option {
+	return func(o *Options) {
+		o.AccessTokenVerifyMethod = val
+	}
+}
+
+// WithHTTPClient provides a function to set the httpClient option.
+func WithHTTPClient(val *http.Client) Option {
+	return func(o *Options) {
+		o.HTTPClient = val
+	}
+}
+
+// WithJWKSOptions provides a function to set the jwksOptions option.
+func WithJWKSOptions(val config.JWKS) Option {
+	return func(o *Options) {
+		o.JWKSOptions = val
+	}
+}
+
+// WithKeySet provides a function to set the KeySet option.
+func WithKeySet(val KeySet) Option {
+	return func(o *Options) {
+		o.KeySet = val
+	}
+}
+
+// WithClientID provides a function to set the clientID option.
+func WithClientID(val string) Option {
+	return func(o *Options) {
+		o.ClientID = val
+	}
+}
+
+// WithSkipClientIDCheck provides a function to set the skipClientIDCheck option.
+func WithSkipClientIDCheck(val bool) Option {
+	return func(o *Options) {
+		o.SkipClientIDCheck = val
+	}
+}
+
+// WithConfig provides a function to set the Config option.
+func WithConfig(val *goidc.Config) Option {
+	return func(o *Options) {
+		o.Config = val
+	}
+}
+
+// WithProviderMetadata provides a function to set the provider option.
+func WithProviderMetadata(val *ProviderMetadata) Option {
+	return func(o *Options) {
+		o.ProviderMetadata = val
+	}
+}
diff --git a/services/graph/pkg/service/v0/personaldata.go b/services/graph/pkg/service/v0/personaldata.go
index 474e00f78b6..2017c603147 100644
--- a/services/graph/pkg/service/v0/personaldata.go
+++ b/services/graph/pkg/service/v0/personaldata.go
@@ -90,7 +90,7 @@ func (g Graph) GatherPersonalData(usr *user.User, ref *provider.Reference, token
 
 	// Check if we have a keycloak client, and if so, get the keycloak export.
 	if ctx != nil && g.keycloakClient != nil {
-		kcd, err := g.keycloakClient.GetPIIReport(ctx, g.config.Keycloak.UserRealm, usr.GetUsername())
+		kcd, err := g.keycloakClient.GetPIIReport(ctx, g.config.Keycloak.UserRealm, usr.GetMail())
 		if err != nil {
 			g.logger.Error().Err(err).Str("userID", usr.GetId().GetOpaqueId()).Msg("cannot get keycloak personal data")
 		}
diff --git a/services/invitations/pkg/mocks/client.go b/services/invitations/pkg/mocks/client.go
index a83ddafe4b9..42efdfbf05f 100644
--- a/services/invitations/pkg/mocks/client.go
+++ b/services/invitations/pkg/mocks/client.go
@@ -40,17 +40,17 @@ func (_m *Client) CreateUser(ctx context.Context, realm string, user *libregraph
 	return r0, r1
 }
 
-// GetPIIReport provides a mock function with given fields: ctx, realm, username
-func (_m *Client) GetPIIReport(ctx context.Context, realm string, username string) (*keycloak.PIIReport, error) {
-	ret := _m.Called(ctx, realm, username)
+// GetPIIReport provides a mock function with given fields: ctx, realm, email
+func (_m *Client) GetPIIReport(ctx context.Context, realm string, email string) (*keycloak.PIIReport, error) {
+	ret := _m.Called(ctx, realm, email)
 
 	var r0 *keycloak.PIIReport
 	var r1 error
 	if rf, ok := ret.Get(0).(func(context.Context, string, string) (*keycloak.PIIReport, error)); ok {
-		return rf(ctx, realm, username)
+		return rf(ctx, realm, email)
 	}
 	if rf, ok := ret.Get(0).(func(context.Context, string, string) *keycloak.PIIReport); ok {
-		r0 = rf(ctx, realm, username)
+		r0 = rf(ctx, realm, email)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*keycloak.PIIReport)
@@ -58,7 +58,7 @@ func (_m *Client) GetPIIReport(ctx context.Context, realm string, username strin
 	}
 
 	if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
-		r1 = rf(ctx, realm, username)
+		r1 = rf(ctx, realm, email)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -66,17 +66,17 @@ func (_m *Client) GetPIIReport(ctx context.Context, realm string, username strin
 	return r0, r1
 }
 
-// GetUserByUsername provides a mock function with given fields: ctx, realm, username
-func (_m *Client) GetUserByUsername(ctx context.Context, realm string, username string) (*libregraph.User, error) {
-	ret := _m.Called(ctx, realm, username)
+// GetUserByEmail provides a mock function with given fields: ctx, realm, email
+func (_m *Client) GetUserByEmail(ctx context.Context, realm string, email string) (*libregraph.User, error) {
+	ret := _m.Called(ctx, realm, email)
 
 	var r0 *libregraph.User
 	var r1 error
 	if rf, ok := ret.Get(0).(func(context.Context, string, string) (*libregraph.User, error)); ok {
-		return rf(ctx, realm, username)
+		return rf(ctx, realm, email)
 	}
 	if rf, ok := ret.Get(0).(func(context.Context, string, string) *libregraph.User); ok {
-		r0 = rf(ctx, realm, username)
+		r0 = rf(ctx, realm, email)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*libregraph.User)
@@ -84,7 +84,7 @@ func (_m *Client) GetUserByUsername(ctx context.Context, realm string, username
 	}
 
 	if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
-		r1 = rf(ctx, realm, username)
+		r1 = rf(ctx, realm, email)
 	} else {
 		r1 = ret.Error(1)
 	}
diff --git a/services/notifications/README.md b/services/notifications/README.md
index 755cdf191fe..81b25c4b999 100644
--- a/services/notifications/README.md
+++ b/services/notifications/README.md
@@ -1,26 +1,28 @@
 # Notification
 
-The notification service is responsible for sending emails to users informing them about events that happened. To do this, it hooks into the event system and listens for certain events that the users need to be informed about.
+The notification service is responsible for sending emails to users informing them about events that happened. To do this it hooks into the event system and listens for certain events that the users need to be informed about.
 
 ## Email Notification Templates
 
-The `notifications` service has embedded email body templates. Email templates can use the placeholders `{{ .Greeting }}`, `{{ .MessageBody }}` and `{{ .CallToAction }}` which are replaced with translations when sent, see the [Translations](#translations) section for more details. Depending on the email purpose, placeholders will contain different strings. An individual translatable string is available for each purpose, finally resolved by the placeholder. Though the email subject is also part of translations, it has no placeholder as it is a mandatory email component. The embedded templates are available for all deployment scenarios.
-
-```text
-template 
-  placeholders
-    translated strings <-- source strings <-- purpose
-final output
-```
-
-In addition, the notifications service supports custom templates. Custom email templates take precedence over the embedded ones. If a custom email template exists, the embedded templates are not used. To configure custom email templates, the `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` environment variable needs to point to a base folder that will contain the email templates. This path must be available from all instances of the notifications service, a shared storage is recommended. The source templates provided by ocis you can derive from are located in following base folder [https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates](https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates) with subfolders `shares` and `spaces`.
-
--   [shares/shareCreated.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/shares/shareCreated.email.body.tmpl)
--   [shares/shareExpired.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/shares/shareExpired.email.body.tmpl)
--   [spaces/membershipExpired.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/membershipExpired.email.body.tmpl)
--   [spaces/sharedSpace.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/sharedSpace.email.body.tmpl)
--   [spaces/unsharedSpace.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.body.tmpl)
-
+The `notifications` service has embedded email body templates.
+The email templates contain placeholders `{{ .Greeting }}`, `{{ .MessageBody }}`, `{{ .CallToAction }}` that are
+replaced with translations, see the [Translations](#translations) section.
+These embedded templates are available for all deployment scenarios. In addition, the service supports custom
+templates.
+The custom email template takes precedence over the embedded one. If a custom email template exists, the embedded ones
+are not used. To configure custom email templates,
+the `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` environment variable needs to point to a base folder that will contain the email
+templates. The source template files provided by ocis are located
+in [https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates](https://github.com/owncloud/ocis/tree/master/services/notifications/pkg/email/templates) in the `shares`
+and `spaces` subfolders:
+[shares/shareCreated.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/shares/shareCreated.email.body.tmpl)
+[shares/shareExpired.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/shares/shareExpired.email.body.tmpl)
+[spaces/membershipExpired.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/membershipExpired.email.body.tmpl)
+[spaces/sharedSpace.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/sharedSpace.email.body.tmpl)
+[spaces/unsharedSpace.email.body.tmpl](https://github.com/owncloud/ocis/blob/master/services/notifications/pkg/email/templates/spaces/unsharedSpace.email.body.tmpl)
+
+Custom Email templates referenced via `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` must be located in subfolders `shares`
+and `spaces` and have the same names as the embedded templates. This naming must match the embedded ones.
 ```text
 templates
 │
@@ -34,22 +36,28 @@ templates
     │   unsharedSpace.email.body.tmpl
 ```
 
-Custom email templates referenced via `NOTIFICATIONS_EMAIL_TEMPLATE_PATH` must also be located in subfolders `shares` and `spaces` and must have the same names as the embedded templates. It is important that the names of these files and  folders match the embedded ones.
-
 ## Translations
 
-The `notifications` service has embedded translations sourced via transifex to provide a basic set of translated languages. These embedded translations are available for all deployment scenarios.
-
-In addition, the service supports custom translations, though it is currently not possible to just add custom translations to embedded ones. If custom translations are configured, the embedded ones are not used. To configure custom translations,
-the `NOTIFICATIONS_TRANSLATION_PATH` environment variable needs to point to a base folder that will contain the translation files. This path must be available from all instances of the notifications service, a shared storage is recommended. Translation files must be of type  [.po](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files) or [.mo](https://www.gnu.org/software/gettext/manual/html_node/Binaries.html). For each language, the filename needs to be `translations.po` (or `translations.mo`) and stored in a folder structure defining the language code. In general the path/name pattern for a translation file needs to be:
+The `notifications` service has embedded translations sourced via transifex to provide a basic set of translated languages.
+These embedded translations are available for all deployment scenarios. In addition, the service supports custom
+translations, though it is currently not possible to just add custom translations to embedded ones. If custom
+translations are configured, the embedded ones are not used. To configure custom translations,
+the `NOTIFICATIONS_TRANSLATION_PATH` environment variable needs to point to a base folder that will contain the translation
+files. This path must be available from all instances of the notifications service, a shared storage is recommended.
+Translation files must be of type  [.po](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files)
+or [.mo](https://www.gnu.org/software/gettext/manual/html_node/Binaries.html). For each language, the filename needs to
+be `translations.po` (or `translations.mo`) and stored in a folder structure defining the language code. In general the path/name
+pattern for a translation file needs to be:
 
 ```text
 {NOTIFICATIONS_TRANSLATION_PATH}/{language-code}/LC_MESSAGES/translations.po
 ```
 
-The language code pattern is composed of `language[_territory]` where  `language` is the base language and `_territory` is optional and defines a country.
+The language code pattern is composed of `language[_territory]` where  `language` is the base language and `_territory`
+is optional and defines a country.
 
-For example, for the language `de_DE`, one needs to place the corresponding translation files to `{NOTIFICATIONS_TRANSLATION_PATH}/de_DE/LC_MESSAGES/translations.po`.
+For example, for the language `de_DE`, one needs to place the corresponding translation files
+to `{NOTIFICATIONS_TRANSLATION_PATH}/de_DE/LC_MESSAGES/translations.po`.
 
 ### Translation Rules
 
diff --git a/services/notifications/pkg/email/l10n/locale/es/LC_MESSAGES/notifications.po b/services/notifications/pkg/email/l10n/locale/es/LC_MESSAGES/notifications.po
deleted file mode 100644
index dbad9551808..00000000000
--- a/services/notifications/pkg/email/l10n/locale/es/LC_MESSAGES/notifications.po
+++ /dev/null
@@ -1,119 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-# 
-# Translators:
-# Juan Carlos Garrote, 2023
-# 
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2023-04-20 04:03+0000\n"
-"PO-Revision-Date: 2023-04-19 11:11+0000\n"
-"Last-Translator: Juan Carlos Garrote, 2023\n"
-"Language-Team: Spanish (https://app.transifex.com/owncloud-org/teams/6149/es/)\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Language: es\n"
-"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
-
-#. UnsharedSpace email template, resolves via {{ .CallToAction }}
-#: pkg/email/templates.go:57
-msgid "Click here to check it: {ShareLink}"
-msgstr "Click aquí para comprobarlo: {ShareLink}"
-
-#. ShareCreated email template, resolves via {{ .CallToAction }}
-#. SharedSpace email template, resolves via {{ .CallToAction }}
-#: pkg/email/templates.go:18 pkg/email/templates.go:43
-msgid "Click here to view it: {ShareLink}"
-msgstr "Click aquí para verlo: {ShareLink}"
-
-#. ShareCreated email template, resolves via {{ .Greeting }}
-#: pkg/email/templates.go:14
-msgid "Hello {ShareGrantee}"
-msgstr "Hola {ShareGrantee}"
-
-#. ShareExpired email template, resolves via {{ .Greeting }}
-#: pkg/email/templates.go:26
-msgid "Hello {ShareGrantee},"
-msgstr "Hola {ShareGrantee},"
-
-#. SharedSpace email template, resolves via {{ .Greeting }}
-#. UnsharedSpace email template, resolves via {{ .Greeting }}
-#. MembershipExpired email template, resolves via {{ .Greeting }}
-#: pkg/email/templates.go:39 pkg/email/templates.go:51
-#: pkg/email/templates.go:65
-msgid "Hello {SpaceGrantee},"
-msgstr "Hola {SpaceGrantee},"
-
-#. MembershipExpired email template, Subject field (resolves directly)
-#: pkg/email/templates.go:63
-msgid "Membership of '{SpaceName}' expired at {ExpiredAt}"
-msgstr "La membresía de '{SpaceName}' caducó en {ExpiredAt}"
-
-#. ShareExpired email template, Subject field (resolves directly)
-#: pkg/email/templates.go:24
-msgid "Share to '{ShareFolder}' expired at {ExpiredAt}"
-msgstr "La compartición a '{ShareFolder}' caducó en {ExpiredAt}"
-
-#. MembershipExpired email template, resolves via {{ .MessageBody }}
-#: pkg/email/templates.go:67
-msgid ""
-"Your membership of space {SpaceName} has expired at {ExpiredAt}\n"
-"\n"
-"Even though this membership has expired you still might have access through other shares and/or space memberships"
-msgstr ""
-"Tu membresía del space {SpaceName} ha caducado en {ExpiredAt}\n"
-"\n"
-"Aunque esta membresía ha caducado, aún puedes tener acceso a través de otras comparticiones y/o membresías al space"
-
-#. ShareExpired email template, resolves via {{ .MessageBody }}
-#: pkg/email/templates.go:28
-msgid ""
-"Your share to {ShareFolder} has expired at {ExpiredAt}\n"
-"\n"
-"Even though this share has been revoked you still might have access through other shares and/or space memberships."
-msgstr ""
-"Tu compartición a {ShareFolder} ha caducado en {ExpiredAt}\n"
-"\n"
-"Aunque esta compartición ha sido anulada, aún puedes tener acceso a través de otras comparticiones y/o membresías al space."
-
-#. ShareCreated email template, resolves via {{ .MessageBody }}
-#: pkg/email/templates.go:16
-msgid "{ShareSharer} has shared \"{ShareFolder}\" with you."
-msgstr "{ShareSharer} ha compartido \"{ShareFolder}\" contigo."
-
-#. ShareCreated email template, Subject field (resolves directly)
-#: pkg/email/templates.go:12
-msgid "{ShareSharer} shared '{ShareFolder}' with you"
-msgstr "{ShareSharer} compartió '{ShareFolder}' contigo"
-
-#. SharedSpace email template, resolves via {{ .MessageBody }}
-#: pkg/email/templates.go:41
-msgid "{SpaceSharer} has invited you to join \"{SpaceName}\"."
-msgstr "{SpaceSharer} te ha invitado a unirte a \"{SpaceName}\"."
-
-#. UnsharedSpace email template, resolves via {{ .MessageBody }}
-#: pkg/email/templates.go:53
-msgid ""
-"{SpaceSharer} has removed you from \"{SpaceName}\".\n"
-"\n"
-"You might still have access through your other groups or direct membership."
-msgstr ""
-"{SpaceSharer} te ha eliminado de \"{SpaceName}\".\n"
-"\n"
-"Aún puedes tener acceso a través de tus otros grupos o membresía directa."
-
-#. SharedSpace email template, Subject field (resolves directly)
-#: pkg/email/templates.go:37
-msgid "{SpaceSharer} invited you to join {SpaceName}"
-msgstr "{SpaceSharer} te invitó a unirte a {SpaceName}"
-
-#. UnsharedSpace email template, Subject field (resolves directly)
-#: pkg/email/templates.go:49
-msgid "{SpaceSharer} removed you from {SpaceName}"
-msgstr "{SpaceSharer} te eliminó de {SpaceName}"
diff --git a/services/notifications/pkg/email/templates.go b/services/notifications/pkg/email/templates.go
index f8161521764..c42f205a55d 100644
--- a/services/notifications/pkg/email/templates.go
+++ b/services/notifications/pkg/email/templates.go
@@ -8,23 +8,23 @@ var (
 	// Shares
 	ShareCreated = MessageTemplate{
 		bodyTemplate: "shares/shareCreated.email.body.tmpl",
-		// ShareCreated email template, Subject field (resolves directly)
+		// ShareCreated email template, Subject field
 		Subject: Template(`{ShareSharer} shared '{ShareFolder}' with you`),
-		// ShareCreated email template, resolves via {{ .Greeting }}
+		// ShareCreated email template, Greeting field
 		Greeting: Template(`Hello {ShareGrantee}`),
-		// ShareCreated email template, resolves via {{ .MessageBody }}
+		// ShareCreated email template, Message field
 		MessageBody: Template(`{ShareSharer} has shared "{ShareFolder}" with you.`),
-		// ShareCreated email template, resolves via {{ .CallToAction }}
+		// ShareCreated email template, CallToAction field
 		CallToAction: Template(`Click here to view it: {ShareLink}`),
 	}
 
 	ShareExpired = MessageTemplate{
 		bodyTemplate: "shares/shareExpired.email.body.tmpl",
-		// ShareExpired email template, Subject field (resolves directly)
+		// ShareExpired email template, Subject field
 		Subject: Template(`Share to '{ShareFolder}' expired at {ExpiredAt}`),
-		// ShareExpired email template, resolves via {{ .Greeting }}
+		// ShareExpired email template, Greeting field
 		Greeting: Template(`Hello {ShareGrantee},`),
-		// ShareExpired email template, resolves via {{ .MessageBody }}
+		// ShareExpired email template, Message field
 		MessageBody: Template(`Your share to {ShareFolder} has expired at {ExpiredAt}
 
 Even though this share has been revoked you still might have access through other shares and/or space memberships.`),
@@ -33,37 +33,37 @@ Even though this share has been revoked you still might have access through othe
 	// Spaces templates
 	SharedSpace = MessageTemplate{
 		bodyTemplate: "spaces/sharedSpace.email.body.tmpl",
-		// SharedSpace email template, Subject field (resolves directly)
+		// SharedSpace email template, Subject field
 		Subject: Template("{SpaceSharer} invited you to join {SpaceName}"),
-		// SharedSpace email template, resolves via {{ .Greeting }}
+		// SharedSpace email template, Greeting field
 		Greeting: Template(`Hello {SpaceGrantee},`),
-		// SharedSpace email template, resolves via {{ .MessageBody }}
+		// SharedSpace email template, Message field
 		MessageBody: Template(`{SpaceSharer} has invited you to join "{SpaceName}".`),
-		// SharedSpace email template, resolves via {{ .CallToAction }}
+		// SharedSpace email template, CallToAction field
 		CallToAction: Template(`Click here to view it: {ShareLink}`),
 	}
 
 	UnsharedSpace = MessageTemplate{
 		bodyTemplate: "spaces/unsharedSpace.email.body.tmpl",
-		// UnsharedSpace email template, Subject field (resolves directly)
+		// UnsharedSpace email template, Subject field
 		Subject: Template(`{SpaceSharer} removed you from {SpaceName}`),
-		// UnsharedSpace email template, resolves via {{ .Greeting }}
+		// UnsharedSpace email template, Greeting field
 		Greeting: Template(`Hello {SpaceGrantee},`),
-		// UnsharedSpace email template, resolves via {{ .MessageBody }}
+		// UnsharedSpace email template, Message field
 		MessageBody: Template(`{SpaceSharer} has removed you from "{SpaceName}".
 
 You might still have access through your other groups or direct membership.`),
-		// UnsharedSpace email template, resolves via {{ .CallToAction }}
+		// UnsharedSpace email template, CallToAction field
 		CallToAction: Template(`Click here to check it: {ShareLink}`),
 	}
 
 	MembershipExpired = MessageTemplate{
 		bodyTemplate: "spaces/membershipExpired.email.body.tmpl",
-		// MembershipExpired email template, Subject field (resolves directly)
+		// MembershipExpired email template, Subject field
 		Subject: Template(`Membership of '{SpaceName}' expired at {ExpiredAt}`),
-		// MembershipExpired email template, resolves via {{ .Greeting }}
+		// MembershipExpired email template, Greeting field
 		Greeting: Template(`Hello {SpaceGrantee},`),
-		// MembershipExpired email template, resolves via {{ .MessageBody }}
+		// MembershipExpired email template, Message field
 		MessageBody: Template(`Your membership of space {SpaceName} has expired at {ExpiredAt}
 
 Even though this membership has expired you still might have access through other shares and/or space memberships`),
diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go
index d2aee9d878f..4f78a318f74 100644
--- a/services/proxy/pkg/command/server.go
+++ b/services/proxy/pkg/command/server.go
@@ -3,19 +3,22 @@ package command
 import (
 	"context"
 	"crypto/tls"
+	"errors"
 	"fmt"
 	"net/http"
 	"time"
 
-	"github.com/coreos/go-oidc/v3/oidc"
 	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
 	"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
+	"github.com/go-chi/chi/v5"
 	chimiddleware "github.com/go-chi/chi/v5/middleware"
+	"github.com/go-chi/render"
 	"github.com/justinas/alice"
 	"github.com/oklog/run"
 	"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	pkgmiddleware "github.com/owncloud/ocis/v2/ocis-pkg/middleware"
+	"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
 	"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
 	"github.com/owncloud/ocis/v2/ocis-pkg/store"
 	"github.com/owncloud/ocis/v2/ocis-pkg/version"
@@ -36,7 +39,6 @@ import (
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
 	"github.com/urfave/cli/v2"
 	microstore "go-micro.dev/v4/store"
-	"golang.org/x/oauth2"
 )
 
 // Server is the entrypoint for the server command.
@@ -49,6 +51,15 @@ func Server(cfg *config.Config) *cli.Command {
 			return configlog.ReturnFatal(parser.ParseConfig(cfg))
 		},
 		Action: func(c *cli.Context) error {
+			userInfoCache := store.Create(
+				store.Store(cfg.OIDC.UserinfoCache.Store),
+				store.TTL(cfg.OIDC.UserinfoCache.TTL),
+				store.Size(cfg.OIDC.UserinfoCache.Size),
+				microstore.Nodes(cfg.OIDC.UserinfoCache.Nodes...),
+				microstore.Database(cfg.OIDC.UserinfoCache.Database),
+				microstore.Table(cfg.OIDC.UserinfoCache.Table),
+			)
+
 			logger := logging.Configure(cfg.Service.Name, cfg.Log)
 			err := tracing.Configure(cfg)
 			if err != nil {
@@ -59,6 +70,27 @@ func Server(cfg *config.Config) *cli.Command {
 				return err
 			}
 
+			var oidcHTTPClient = &http.Client{
+				Transport: &http.Transport{
+					TLSClientConfig: &tls.Config{
+						MinVersion:         tls.VersionTLS12,
+						InsecureSkipVerify: cfg.OIDC.Insecure, //nolint:gosec
+					},
+					DisableKeepAlives: true,
+				},
+				Timeout: time.Second * 10,
+			}
+
+			oidcClient := oidc.NewOIDCClient(
+				oidc.WithAccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod),
+				oidc.WithLogger(logger),
+				oidc.WithHTTPClient(oidcHTTPClient),
+				oidc.WithOidcIssuer(cfg.OIDC.Issuer),
+				oidc.WithJWKSOptions(cfg.OIDC.JWKS),
+				oidc.WithClientID(cfg.OIDC.ClientID),
+				oidc.WithSkipClientIDCheck(cfg.OIDC.SkipClientIDCheck),
+			)
+
 			var (
 				m = metrics.New()
 			)
@@ -79,18 +111,28 @@ func Server(cfg *config.Config) *cli.Command {
 				proxy.Logger(logger),
 				proxy.Config(cfg),
 			)
+
+			lh := StaticRouteHandler{
+				prefix:        cfg.HTTP.Root,
+				userInfoCache: userInfoCache,
+				logger:        logger,
+				config:        *cfg,
+				oidcClient:    oidcClient,
+				proxy:         rp,
+			}
 			if err != nil {
-				return fmt.Errorf("Failed to initialize reverse proxy: %w", err)
+				return fmt.Errorf("failed to initialize reverse proxy: %w", err)
 			}
 
 			{
+				middlewares := loadMiddlewares(ctx, logger, cfg, userInfoCache)
 				server, err := proxyHTTP.Server(
-					proxyHTTP.Handler(rp),
+					proxyHTTP.Handler(lh.handler()),
 					proxyHTTP.Logger(logger),
 					proxyHTTP.Context(ctx),
 					proxyHTTP.Config(cfg),
 					proxyHTTP.Metrics(metrics.New()),
-					proxyHTTP.Middlewares(loadMiddlewares(ctx, logger, cfg)),
+					proxyHTTP.Middlewares(middlewares),
 				)
 
 				if err != nil {
@@ -137,7 +179,86 @@ func Server(cfg *config.Config) *cli.Command {
 	}
 }
 
-func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) alice.Chain {
+// StaticRouteHandler defines a Route Handler for static routes
+type StaticRouteHandler struct {
+	prefix        string
+	proxy         http.Handler
+	userInfoCache microstore.Store
+	logger        log.Logger
+	config        config.Config
+	oidcClient    oidc.OIDCClient
+}
+
+func (h *StaticRouteHandler) handler() http.Handler {
+	m := chi.NewMux()
+	m.Route(h.prefix, func(r chi.Router) {
+		// Wrapper for backchannel logout
+		r.Post("/backchannel_logout", h.backchannelLogout)
+
+		// TODO: migrate oidc well knowns here in a second wrapper
+		r.HandleFunc("/*", h.proxy.ServeHTTP)
+
+	})
+	// This is commented out due to a race issue in chi
+	//var methods = []string{"PROPFIND", "DELETE", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "REPORT"}
+	//for _, k := range methods {
+	//	chi.RegisterMethod(k)
+	//}
+
+	// To avoid using the chi.RegisterMethod() this is basically a catchAll for all HTTP Methods that are not
+	// covered in chi by default
+	m.MethodNotAllowed(h.proxy.ServeHTTP)
+
+	return m
+}
+
+type jse struct {
+	Error            string `json:"error"`
+	ErrorDescription string `json:"error_description"`
+}
+
+// handle backchannel logout requests as per https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest
+func (h *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Request) {
+	// parse the application/x-www-form-urlencoded POST request
+	if err := r.ParseForm(); err != nil {
+		render.Status(r, http.StatusBadRequest)
+		render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
+		return
+	}
+
+	logoutToken, err := h.oidcClient.VerifyLogoutToken(r.Context(), r.PostFormValue("logout_token"))
+	if err != nil {
+		render.Status(r, http.StatusBadRequest)
+		render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
+		return
+	}
+
+	records, err := h.userInfoCache.Read(logoutToken.SessionId)
+	if errors.Is(err, microstore.ErrNotFound) || len(records) == 0 {
+		render.Status(r, http.StatusOK)
+		render.JSON(w, r, nil)
+		return
+	}
+
+	for _, record := range records {
+		err = h.userInfoCache.Delete(string(record.Value))
+		if !errors.Is(err, microstore.ErrNotFound) {
+			// Spec requires us to return a 400 BadRequest when the session could not be destroyed
+			h.logger.Err(err).Msg("could not delete user info from cache")
+			render.Status(r, http.StatusBadRequest)
+			render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
+			return
+		}
+	}
+
+	// we can ignore errors when cleaning up the lookup table
+	_ = h.userInfoCache.Delete(logoutToken.SessionId)
+
+	render.Status(r, http.StatusOK)
+	render.JSON(w, r, nil)
+}
+
+func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config, userInfoCache microstore.Store) alice.Chain {
 	rolesClient := settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient())
 	revaClient, err := pool.GetGatewayServiceClient(cfg.Reva.Address, cfg.Reva.GetRevaOptions()...)
 	if err != nil {
@@ -212,32 +333,19 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
 		})
 	}
 
-	cache := store.Create(
-		store.Store(cfg.OIDC.UserinfoCache.Store),
-		store.TTL(cfg.OIDC.UserinfoCache.TTL),
-		store.Size(cfg.OIDC.UserinfoCache.Size),
-		microstore.Nodes(cfg.OIDC.UserinfoCache.Nodes...),
-		microstore.Database(cfg.OIDC.UserinfoCache.Database),
-		microstore.Table(cfg.OIDC.UserinfoCache.Table),
-	)
-
 	authenticators = append(authenticators, middleware.NewOIDCAuthenticator(
 		middleware.Logger(logger),
-		middleware.Cache(cache),
+		middleware.UserInfoCache(userInfoCache),
 		middleware.DefaultAccessTokenTTL(cfg.OIDC.UserinfoCache.TTL),
 		middleware.HTTPClient(oidcHTTPClient),
 		middleware.OIDCIss(cfg.OIDC.Issuer),
-		middleware.JWKSOptions(cfg.OIDC.JWKS),
-		middleware.AccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod),
-		middleware.OIDCProviderFunc(func() (middleware.OIDCProvider, error) {
-			// Initialize a provider by specifying the issuer URL.
-			// it will fetch the keys from the issuer using the .well-known
-			// endpoint
-			return oidc.NewProvider(
-				context.WithValue(ctx, oauth2.HTTPClient, oidcHTTPClient),
-				cfg.OIDC.Issuer,
-			)
-		}),
+		middleware.OIDCClient(oidc.NewOIDCClient(
+			oidc.WithAccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod),
+			oidc.WithLogger(logger),
+			oidc.WithHTTPClient(oidcHTTPClient),
+			oidc.WithOidcIssuer(cfg.OIDC.Issuer),
+			oidc.WithJWKSOptions(cfg.OIDC.JWKS),
+		)),
 	))
 	authenticators = append(authenticators, middleware.PublicShareAuthenticator{
 		Logger:            logger,
diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go
index a2d6daed6fc..22f818e6083 100644
--- a/services/proxy/pkg/config/config.go
+++ b/services/proxy/pkg/config/config.go
@@ -108,6 +108,8 @@ type OIDC struct {
 	UserinfoCache           *Cache `yaml:"user_info_cache"`
 	JWKS                    JWKS   `yaml:"jwks"`
 	RewriteWellKnown        bool   `yaml:"rewrite_well_known" env:"PROXY_OIDC_REWRITE_WELLKNOWN" desc:"Enables rewriting the /.well-known/openid-configuration to the configured OIDC issuer. Needed by the Desktop Client, Android Client and iOS Client to discover the OIDC provider."`
+	ClientID                string `yaml:"client_id" env:"OCIS_OIDC_CLIENT_ID;PROXY_OIDC_CLIENT_ID" desc:"OIDC client ID, which ownCloud Web uses. This client needs to be set up in your external IDP (has no effect when using the builtin IDP)."`
+	SkipClientIDCheck       bool   `yaml:"skip_client_id_check" env:"PROXY_OIDC_SKIP_CLIENT_ID_CHECK" desc:"If true will skip checking the configured client ID is present in audience claims. See following chapter for more details: https://openid.net/specs/openid-connect-core-1_0.html#IDToken"`
 }
 
 type JWKS struct {
diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go
index b3316083e15..956ca69c68f 100644
--- a/services/proxy/pkg/config/defaults/defaultconfig.go
+++ b/services/proxy/pkg/config/defaults/defaultconfig.go
@@ -53,6 +53,7 @@ func DefaultConfig() *config.Config {
 				RefreshTimeout:    10, // seconds
 				RefreshUnknownKID: true,
 			},
+			ClientID: "web",
 		},
 		PolicySelector: nil,
 		RoleAssignment: config.RoleAssignment{
diff --git a/services/proxy/pkg/middleware/oidc_auth.go b/services/proxy/pkg/middleware/oidc_auth.go
index 9ba727b222c..1156f2b9a1c 100644
--- a/services/proxy/pkg/middleware/oidc_auth.go
+++ b/services/proxy/pkg/middleware/oidc_auth.go
@@ -3,17 +3,14 @@ package middleware
 import (
 	"context"
 	"encoding/base64"
+	"fmt"
 	"net/http"
 	"strings"
-	"sync"
 	"time"
 
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
-	"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
 
-	"github.com/MicahParks/keyfunc"
-	gOidc "github.com/coreos/go-oidc/v3/oidc"
 	"github.com/golang-jwt/jwt/v4"
 	"github.com/pkg/errors"
 	"github.com/shamaton/msgpack/v2"
@@ -27,26 +24,18 @@ const (
 	_bearerPrefix        = "Bearer "
 )
 
-// OIDCProvider used to mock the oidc provider during tests
-type OIDCProvider interface {
-	UserInfo(ctx context.Context, ts oauth2.TokenSource) (*gOidc.UserInfo, error)
-}
-
 // NewOIDCAuthenticator returns a ready to use authenticator which can handle OIDC authentication.
 func NewOIDCAuthenticator(opts ...Option) *OIDCAuthenticator {
 	options := newOptions(opts...)
 
 	return &OIDCAuthenticator{
 		Logger:                  options.Logger,
-		userInfoCache:           options.Cache,
+		userInfoCache:           options.UserInfoCache,
 		DefaultTokenCacheTTL:    options.DefaultAccessTokenTTL,
 		HTTPClient:              options.HTTPClient,
 		OIDCIss:                 options.OIDCIss,
-		ProviderFunc:            options.OIDCProviderFunc,
-		JWKSOptions:             options.JWKS,
+		oidcClient:              options.OIDCClient,
 		AccessTokenVerifyMethod: options.AccessTokenVerifyMethod,
-		providerLock:            &sync.Mutex{},
-		jwksLock:                &sync.Mutex{},
 	}
 }
 
@@ -57,21 +46,14 @@ type OIDCAuthenticator struct {
 	OIDCIss                 string
 	userInfoCache           store.Store
 	DefaultTokenCacheTTL    time.Duration
-	ProviderFunc            func() (OIDCProvider, error)
+	oidcClient              oidc.OIDCClient
 	AccessTokenVerifyMethod string
-	JWKSOptions             config.JWKS
-
-	providerLock *sync.Mutex
-	provider     OIDCProvider
-
-	jwksLock *sync.Mutex
-	JWKS     *keyfunc.JWKS
 }
 
 func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[string]interface{}, error) {
 	var claims map[string]interface{}
 
-	// usea 64 bytes long hash to have 256-bit collision resistance.
+	// use a 64 bytes long hash to have 256-bit collision resistance.
 	hash := make([]byte, 64)
 	sha3.ShakeSum256(hash, []byte(token))
 	encodedHash := base64.URLEncoding.EncodeToString(hash)
@@ -87,7 +69,8 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
 		}
 		m.Logger.Error().Err(err).Msg("could not unmarshal userinfo")
 	}
-	aClaims, err := m.verifyAccessToken(token)
+
+	aClaims, _, err := m.oidcClient.VerifyAccessToken(req.Context(), token)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to verify access token")
 	}
@@ -96,7 +79,7 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
 		AccessToken: token,
 	}
 
-	userInfo, err := m.getProvider().UserInfo(
+	userInfo, err := m.oidcClient.UserInfo(
 		context.WithValue(req.Context(), oauth2.HTTPClient, m.HTTPClient),
 		oauth2.StaticTokenSource(oauth2Token),
 	)
@@ -120,6 +103,18 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
 			if err != nil {
 				m.Logger.Error().Err(err).Msg("failed to write to userinfo cache")
 			}
+
+			if sid, ok := claims["sid"]; ok {
+				// reuse user cache for session id lookup
+				err = m.userInfoCache.Write(&store.Record{
+					Key:    fmt.Sprintf("%s", sid),
+					Value:  []byte(encodedHash),
+					Expiry: time.Until(expiration),
+				})
+				if err != nil {
+					m.Logger.Error().Err(err).Msg("failed to write session lookup cache")
+				}
+			}
 		}
 	}()
 
@@ -127,44 +122,6 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
 	return claims, nil
 }
 
-func (m OIDCAuthenticator) verifyAccessToken(token string) (jwt.RegisteredClaims, error) {
-	switch m.AccessTokenVerifyMethod {
-	case config.AccessTokenVerificationJWT:
-		return m.verifyAccessTokenJWT(token)
-	case config.AccessTokenVerificationNone:
-		m.Logger.Debug().Msg("Access Token verification disabled")
-		return jwt.RegisteredClaims{}, nil
-	default:
-		m.Logger.Error().Str("access_token_verify_method", m.AccessTokenVerifyMethod).Msg("Unknown Access Token verification setting")
-		return jwt.RegisteredClaims{}, errors.New("Unknown Access Token Verification method")
-	}
-}
-
-// verifyAccessTokenJWT tries to parse and verify the access token as a JWT.
-func (m OIDCAuthenticator) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, error) {
-	var claims jwt.RegisteredClaims
-	jwks := m.getKeyfunc()
-	if jwks == nil {
-		return claims, errors.New("Error initializing jwks keyfunc")
-	}
-
-	_, err := jwt.ParseWithClaims(token, &claims, jwks.Keyfunc)
-	m.Logger.Debug().Interface("access token", &claims).Msg("parsed access token")
-	if err != nil {
-		m.Logger.Info().Err(err).Msg("Failed to parse/verify the access token.")
-		return claims, err
-	}
-
-	if !claims.VerifyIssuer(m.OIDCIss, true) {
-		vErr := jwt.ValidationError{}
-		vErr.Inner = jwt.ErrTokenInvalidIssuer
-		vErr.Errors |= jwt.ValidationErrorIssuer
-		return claims, vErr
-	}
-
-	return claims, nil
-}
-
 // extractExpiration tries to extract the expriration time from the access token
 // If the access token does not have an exp claim it will fallback to the configured
 // default expiration
@@ -186,56 +143,6 @@ func (m OIDCAuthenticator) shouldServe(req *http.Request) bool {
 	return strings.HasPrefix(header, _bearerPrefix)
 }
 
-func (m *OIDCAuthenticator) getKeyfunc() *keyfunc.JWKS {
-	m.jwksLock.Lock()
-	defer m.jwksLock.Unlock()
-	if m.JWKS == nil {
-		oidcMetadata, err := oidc.GetIDPMetadata(m.Logger, m.HTTPClient, m.OIDCIss)
-		if err != nil {
-			m.Logger.Error().Err(err).Msg("failed to decode provider openid-configuration")
-			return nil
-		}
-		m.Logger.Debug().Str("jwks", oidcMetadata.JwksURI).Msg("discovered jwks endpoint")
-		options := keyfunc.Options{
-			Client: m.HTTPClient,
-			RefreshErrorHandler: func(err error) {
-				m.Logger.Error().Err(err).Msg("There was an error with the jwt.Keyfunc")
-			},
-			RefreshInterval:   time.Minute * time.Duration(m.JWKSOptions.RefreshInterval),
-			RefreshRateLimit:  time.Second * time.Duration(m.JWKSOptions.RefreshRateLimit),
-			RefreshTimeout:    time.Second * time.Duration(m.JWKSOptions.RefreshTimeout),
-			RefreshUnknownKID: m.JWKSOptions.RefreshUnknownKID,
-		}
-		m.JWKS, err = keyfunc.Get(oidcMetadata.JwksURI, options)
-		if err != nil {
-			m.JWKS = nil
-			m.Logger.Error().Err(err).Msg("Failed to create JWKS from resource at the given URL.")
-			return nil
-		}
-	}
-	return m.JWKS
-}
-
-func (m *OIDCAuthenticator) getProvider() OIDCProvider {
-	m.providerLock.Lock()
-	defer m.providerLock.Unlock()
-	if m.provider == nil {
-		// Lazily initialize a provider
-
-		// provider needs to be cached as when it is created
-		// it will fetch the keys from the issuer using the .well-known
-		// endpoint
-		provider, err := m.ProviderFunc()
-		if err != nil {
-			m.Logger.Error().Err(err).Msg("could not initialize oidcAuth provider")
-			return nil
-		}
-
-		m.provider = provider
-	}
-	return m.provider
-}
-
 // Authenticate implements the authenticator interface to authenticate requests via oidc auth.
 func (m *OIDCAuthenticator) Authenticate(r *http.Request) (*http.Request, bool) {
 	// there is no bearer token on the request,
@@ -245,15 +152,6 @@ func (m *OIDCAuthenticator) Authenticate(r *http.Request) (*http.Request, bool)
 		// implement an early return here for paths we can't authenticate in this authenticator.
 		return nil, false
 	}
-
-	if m.getProvider() == nil {
-		return nil, false
-	}
-
-	// Force init of jwks keyfunc if needed (contacts the .well-known and jwks endpoints on first call)
-	if m.AccessTokenVerifyMethod == config.AccessTokenVerificationJWT && m.getKeyfunc() == nil {
-		return nil, false
-	}
 	token := strings.TrimPrefix(r.Header.Get(_headerAuthorization), _bearerPrefix)
 
 	claims, err := m.getClaims(token, r)
diff --git a/services/proxy/pkg/middleware/options.go b/services/proxy/pkg/middleware/options.go
index 1e9d05739ad..d4759079f49 100644
--- a/services/proxy/pkg/middleware/options.go
+++ b/services/proxy/pkg/middleware/options.go
@@ -6,6 +6,7 @@ import (
 
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
+	"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
 	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
 	"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
@@ -34,7 +35,7 @@ type Options struct {
 	// SettingsRoleService for the roles API in settings
 	SettingsRoleService settingssvc.RoleService
 	// OIDCProviderFunc to lazily initialize an oidc provider, must be set for the oidc_auth middleware
-	OIDCProviderFunc func() (OIDCProvider, error)
+	OIDCClient oidc.OIDCClient
 	// OIDCIss is the oidcAuth-issuer
 	OIDCIss string
 	// RevaGatewayClient to send requests to the reva gateway
@@ -53,8 +54,8 @@ type Options struct {
 	EnableBasicAuth bool
 	// DefaultAccessTokenTTL is used to calculate the expiration when an access token has no expiration set
 	DefaultAccessTokenTTL time.Duration
-	// Cache sets the access token cache store
-	Cache store.Store
+	// UserInfoCache sets the access token cache store
+	UserInfoCache store.Store
 	// CredentialsByUserAgent sets the auth challenges on a per user-agent basis
 	CredentialsByUserAgent map[string]string
 	// AccessTokenVerifyMethod configures how access_tokens should be verified but the oidc_auth middleware.
@@ -113,10 +114,10 @@ func SettingsRoleService(rc settingssvc.RoleService) Option {
 	}
 }
 
-// OIDCProviderFunc provides a function to set the the oidc provider function option.
-func OIDCProviderFunc(f func() (OIDCProvider, error)) Option {
+// OIDCClient provides a function to set the the oidc client option.
+func OIDCClient(val oidc.OIDCClient) Option {
 	return func(o *Options) {
-		o.OIDCProviderFunc = f
+		o.OIDCClient = val
 	}
 }
 
@@ -190,10 +191,10 @@ func DefaultAccessTokenTTL(ttl time.Duration) Option {
 	}
 }
 
-// Cache provides a function to set the Cache
-func Cache(val store.Store) Option {
+// UserInfoCache provides a function to set the UserInfoCache
+func UserInfoCache(val store.Store) Option {
 	return func(o *Options) {
-		o.Cache = val
+		o.UserInfoCache = val
 	}
 }
 
@@ -218,13 +219,6 @@ func AccessTokenVerifyMethod(method string) Option {
 	}
 }
 
-// JWKSOptions sets the options for fetching the JWKS from the IDP
-func JWKSOptions(jo config.JWKS) Option {
-	return func(o *Options) {
-		o.JWKS = jo
-	}
-}
-
 // RoleQuotas sets the role quota mapping setting
 func RoleQuotas(roleQuotas map[string]uint64) Option {
 	return func(o *Options) {
diff --git a/services/search/pkg/content/basic.go b/services/search/pkg/content/basic.go
index 0130584bab8..ba95ad38ed5 100644
--- a/services/search/pkg/content/basic.go
+++ b/services/search/pkg/content/basic.go
@@ -6,7 +6,6 @@ import (
 
 	storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	"github.com/cs3org/reva/v2/pkg/tags"
-	"github.com/cs3org/reva/v2/pkg/utils"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 )
 
@@ -35,7 +34,7 @@ func (b Basic) Extract(_ context.Context, ri *storageProvider.ResourceInfo) (Doc
 	}
 
 	if ri.Mtime != nil {
-		doc.Mtime = utils.TSToTime(ri.Mtime).Format(time.RFC3339Nano)
+		doc.Mtime = time.Unix(int64(ri.Mtime.Seconds), int64(ri.Mtime.Nanos)).UTC().Format(time.RFC3339)
 	}
 
 	return doc, nil
diff --git a/services/web/pkg/config/config.go b/services/web/pkg/config/config.go
index 41e622fd19d..c27ab9d181f 100644
--- a/services/web/pkg/config/config.go
+++ b/services/web/pkg/config/config.go
@@ -67,7 +67,7 @@ type WebConfig struct {
 type OIDC struct {
 	MetadataURL  string `json:"metadata_url,omitempty" yaml:"metadata_url" env:"WEB_OIDC_METADATA_URL" desc:"URL for the OIDC well-known configuration endpoint. Defaults to the oCIS API URL + \"/.well-known/openid-configuration\"."`
 	Authority    string `json:"authority,omitempty" yaml:"authority" env:"OCIS_URL;OCIS_OIDC_ISSUER;WEB_OIDC_AUTHORITY" desc:"URL of the OIDC issuer. It defaults to URL of the builtin IDP."`
-	ClientID     string `json:"client_id,omitempty" yaml:"client_id" env:"WEB_OIDC_CLIENT_ID" desc:"OIDC client ID, which ownCloud Web uses. This client needs to be set up in your IDP."`
+	ClientID     string `json:"client_id,omitempty" yaml:"client_id" env:"OCIS_OIDC_CLIENT_ID;WEB_OIDC_CLIENT_ID" desc:"OIDC client ID, which ownCloud Web uses. This client needs to be set up in your IDP (has no effect when using the builtin IDP)."`
 	ResponseType string `json:"response_type,omitempty" yaml:"response_type" env:"WEB_OIDC_RESPONSE_TYPE" desc:"OIDC response type to use for authentication."`
 	Scope        string `json:"scope,omitempty" yaml:"scope" env:"WEB_OIDC_SCOPE" desc:"OIDC scopes to request during authentication to authorize access to user details. Defaults to 'openid profile email'. Values are separated by blank. More example values but not limited to are 'address' or 'phone' etc."`
 }


Test patch:
diff --git a/ocis-pkg/oidc/client_test.go b/ocis-pkg/oidc/client_test.go
new file mode 100644
index 00000000000..e33dbf0d7a6
--- /dev/null
+++ b/ocis-pkg/oidc/client_test.go
@@ -0,0 +1,308 @@
+package oidc_test
+
+import (
+	"context"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+	"testing"
+
+	goidc "github.com/coreos/go-oidc/v3/oidc"
+	"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
+	"gopkg.in/square/go-jose.v2"
+)
+
+type signingKey struct {
+	keyID string // optional
+	priv  interface{}
+	pub   interface{}
+	alg   jose.SignatureAlgorithm
+}
+
+// sign creates a JWS using the private key from the provided payload.
+func (s *signingKey) sign(t testing.TB, payload []byte) string {
+	privKey := &jose.JSONWebKey{Key: s.priv, Algorithm: string(s.alg), KeyID: s.keyID}
+
+	signer, err := jose.NewSigner(jose.SigningKey{Algorithm: s.alg, Key: privKey}, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	jws, err := signer.Sign(payload)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	data, err := jws.CompactSerialize()
+	if err != nil {
+		t.Fatal(err)
+	}
+	return data
+}
+
+func (s *signingKey) jwk() jose.JSONWebKey {
+	return jose.JSONWebKey{Key: s.pub, Use: "sig", Algorithm: string(s.alg), KeyID: s.keyID}
+}
+
+func TestLogoutVerify(t *testing.T) {
+	tests := []logoutVerificationTest{
+		{
+			name: "good token",
+			logoutToken: ` {
+							   "iss": "https://foo",
+							   "sub": "248289761001",
+							   "aud": "s6BhdRkqt3",
+							   "iat": 1471566154,
+							   "jti": "bWJq",
+							   "sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
+							   "events": {
+								 "http://schemas.openid.net/event/backchannel-logout": {}
+								 }
+							  }`,
+			config: goidc.Config{
+				SkipClientIDCheck: true,
+			},
+			signKey: newRSAKey(t),
+		},
+		{
+			name:        "invalid issuer",
+			issuer:      "https://bar",
+			logoutToken: `{"iss":"https://foo"}`,
+			config: goidc.Config{
+				SkipClientIDCheck: true,
+				SkipExpiryCheck:   true,
+			},
+			signKey: newRSAKey(t),
+			wantErr: true,
+		},
+		{
+			name: "invalid sig",
+			logoutToken: `{
+							   "iss": "https://foo",
+							   "sub": "248289761001",
+							   "aud": "s6BhdRkqt3",
+							   "iat": 1471566154,
+							   "jti": "bWJq",
+							   "sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
+							   "events": {
+								 "http://schemas.openid.net/event/backchannel-logout": {}
+								 }
+							  }`,
+			config: goidc.Config{
+				SkipClientIDCheck: true,
+				SkipExpiryCheck:   true,
+			},
+			signKey:         newRSAKey(t),
+			verificationKey: newRSAKey(t),
+			wantErr:         true,
+		},
+		{
+			name: "no sid and no sub",
+			logoutToken: ` {
+								"iss": "https://foo",
+							   "aud": "s6BhdRkqt3",
+							   "iat": 1471566154,
+							   "jti": "bWJq",
+							   "events": {
+								 "http://schemas.openid.net/event/backchannel-logout": {}
+								 }
+							  }`,
+			config: goidc.Config{
+				SkipClientIDCheck: true,
+			},
+			signKey: newRSAKey(t),
+			wantErr: true,
+		},
+		{
+			name: "Prohibited nonce present",
+			logoutToken: ` {
+							   	"iss": "https://foo",
+								"sub": "248289761001",
+							   	"aud": "s6BhdRkqt3",
+							   	"iat": 1471566154,
+							   	"jti": "bWJq",
+								"nonce" : "prohibited",
+							   	"events": {
+								 "http://schemas.openid.net/event/backchannel-logout": {}
+								 }
+							  }`,
+			config: goidc.Config{
+				SkipClientIDCheck: true,
+			},
+			signKey: newRSAKey(t),
+			wantErr: true,
+		},
+		{
+			name: "Wrong Event string",
+			logoutToken: ` {
+							   "iss": "https://foo",
+							   "sub": "248289761001",
+							   "aud": "s6BhdRkqt3",
+							   "iat": 1471566154,
+							   "jti": "bWJq",
+							   "sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
+							   "events": {
+								 "not a logout event": {}
+								 }
+							  }`,
+			config: goidc.Config{
+				SkipClientIDCheck: true,
+			},
+			signKey: newRSAKey(t),
+			wantErr: true,
+		},
+		{
+			name: "No Event string",
+			logoutToken: ` {
+							   "iss": "https://foo",
+							   "sub": "248289761001",
+							   "aud": "s6BhdRkqt3",
+							   "iat": 1471566154,
+							   "jti": "bWJq",
+							   "sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
+							  }`,
+			config: goidc.Config{
+				SkipClientIDCheck: true,
+			},
+			signKey: newRSAKey(t),
+			wantErr: true,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, test.run)
+	}
+}
+
+func TestVerifyAudienceLogout(t *testing.T) {
+	tests := []logoutVerificationTest{
+		{
+			name: "good audience",
+			logoutToken: `{"iss":"https://foo","aud":"client1","sub":"subject","events": {
+								 "http://schemas.openid.net/event/backchannel-logout": {}
+								 }
+							}`,
+			config: goidc.Config{
+				ClientID:        "client1",
+				SkipExpiryCheck: true,
+			},
+			signKey: newRSAKey(t),
+		},
+		{
+			name: "mismatched audience",
+			logoutToken: `{"iss":"https://foo","aud":"client2","sub":"subject","events": {
+								 "http://schemas.openid.net/event/backchannel-logout": {}
+								 }}`,
+			config: goidc.Config{
+				ClientID:        "client1",
+				SkipExpiryCheck: true,
+			},
+			signKey: newRSAKey(t),
+			wantErr: true,
+		},
+		{
+			name: "multiple audiences, one matches",
+			logoutToken: `{"iss":"https://foo","aud":["client1","client2"],"sub":"subject","events": {
+								 "http://schemas.openid.net/event/backchannel-logout": {}
+								 }}`,
+			config: goidc.Config{
+				ClientID:        "client2",
+				SkipExpiryCheck: true,
+			},
+			signKey: newRSAKey(t),
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, test.run)
+	}
+}
+
+type logoutVerificationTest struct {
+	// Name of the subtest.
+	name string
+
+	// If not provided defaults to "https://foo"
+	issuer string
+
+	// JWT payload (just the claims).
+	logoutToken string
+
+	// Key to sign the ID Token with.
+	signKey *signingKey
+	// If not provided defaults to signKey. Only useful when
+	// testing invalid signatures.
+	verificationKey *signingKey
+
+	config  goidc.Config
+	wantErr bool
+}
+
+type testVerifier struct {
+	jwk jose.JSONWebKey
+}
+
+func (t *testVerifier) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
+	jws, err := jose.ParseSigned(jwt)
+	if err != nil {
+		return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
+	}
+	return jws.Verify(&t.jwk)
+}
+
+func (v logoutVerificationTest) runGetToken(t *testing.T) (*oidc.LogoutToken, error) {
+	token := v.signKey.sign(t, []byte(v.logoutToken))
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	issuer := "https://foo"
+	var ks goidc.KeySet
+	if v.verificationKey == nil {
+		ks = &testVerifier{v.signKey.jwk()}
+	} else {
+		ks = &testVerifier{v.verificationKey.jwk()}
+	}
+
+	pm := oidc.ProviderMetadata{}
+	var clientID string
+	switch t.Name() {
+	case "TestLogoutVerify/good_token":
+		clientID = "s6BhdRkqt3"
+	default:
+		clientID = "client1"
+	}
+	verifier := oidc.NewOIDCClient(
+		oidc.WithOidcIssuer(issuer),
+		oidc.WithKeySet(ks),
+		oidc.WithConfig(&v.config),
+		oidc.WithProviderMetadata(&pm),
+		oidc.WithClientID(clientID),
+	)
+
+	return verifier.VerifyLogoutToken(ctx, token)
+}
+
+func (l logoutVerificationTest) run(t *testing.T) {
+	_, err := l.runGetToken(t)
+	if err != nil && !l.wantErr {
+		t.Errorf("%v", err)
+	}
+	if err == nil && l.wantErr {
+		t.Errorf("expected error")
+	}
+}
+
+func newRSAKey(t testing.TB) *signingKey {
+	priv, err := rsa.GenerateKey(rand.Reader, 1028)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return &signingKey{"", priv, priv.Public(), jose.RS256}
+}
+
+func newECDSAKey(t *testing.T) *signingKey {
+	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return &signingKey{"", priv, priv.Public(), jose.ES256}
+}