├── Resources ├── Private │ ├── .htaccess │ └── Language │ │ ├── locallang_db.xlf │ │ └── de.locallang_db.xlf └── Public │ └── Icons │ ├── icon_tx_oidc_application.png │ └── Extension.svg ├── Build ├── .dockerignore ├── .gitignore ├── typo3 │ ├── typo3-v12 │ │ ├── .gitignore │ │ └── composer.json │ ├── typo3-v13 │ │ ├── .gitignore │ │ └── composer.json │ ├── typo3 │ │ ├── .gitignore │ │ ├── packages │ │ │ └── oidc-sitepackage │ │ │ │ ├── Configuration │ │ │ │ └── Sets │ │ │ │ │ └── Oidc │ │ │ │ │ ├── constants.typoscript │ │ │ │ │ ├── config.yaml │ │ │ │ │ └── setup.typoscript │ │ │ │ ├── composer.json │ │ │ │ ├── ext_localconf.php │ │ │ │ ├── Resources │ │ │ │ └── Private │ │ │ │ │ └── Felogin │ │ │ │ │ └── Templates │ │ │ │ │ └── Login │ │ │ │ │ └── Login.html │ │ │ │ └── ext_tables_static+adt.sql │ │ └── config │ │ │ ├── sites │ │ │ └── main │ │ │ │ └── config.yaml │ │ │ └── system │ │ │ ├── additional.php │ │ │ └── settings.php │ ├── docker │ │ ├── entrypoint.sh │ │ ├── 000-default.conf │ │ ├── typo3.ini │ │ └── 000-default-ssl.conf │ ├── oidc.env │ └── Dockerfile ├── playwright │ ├── e2e │ │ ├── .gitignore │ │ ├── package.json │ │ ├── tests │ │ │ ├── login-via-uri-view-helper.test.ts │ │ │ └── login-via-login-controller.test.ts │ │ ├── playwright.config.ts │ │ └── package-lock.json │ └── Dockerfile ├── docker-compose.override.sample.yml ├── create_certs.sh ├── oidc-server-mock │ └── clients-config.json ├── docker-compose.build.yml ├── certs │ ├── developer.pem │ └── developer.key └── docker-compose.yml ├── scripts ├── phpcs.sh ├── composer.sh └── runphptool.sh ├── ext_tables.sql ├── Configuration ├── TCA │ └── Overrides │ │ ├── sys_template.php │ │ ├── tt_content.php │ │ ├── fe_groups.php │ │ └── fe_users.php ├── Icons.php ├── RequestMiddlewares.php ├── TypoScript │ └── setup.typoscript └── Services.yaml ├── Classes ├── Event │ ├── GetAuthorizationUrlEvent.php │ ├── AuthenticationProcessMappingEvent.php │ ├── AuthenticationPreUserEvent.php │ ├── AuthenticationGetUserEvent.php │ ├── ModifyResourceOwnerEvent.php │ ├── ModifyUserEvent.php │ ├── AuthenticationFetchUserEvent.php │ └── AuthenticationGetUserGroupsEvent.php ├── Frontend │ ├── FrontendSimulationInterface.php │ ├── FrontendSimulationV12.php │ └── FrontendSimulationV13.php ├── Factory │ ├── OAuthProviderFactoryInterface.php │ ├── RequestFactory.php │ └── GenericOAuthProviderFactory.php ├── EventListener │ ├── ProcessRequestTokenListener.php │ └── GetAuthorizationUrlSetLanguageEventListener.php ├── AuthenticationContext.php ├── ViewHelpers │ └── OidcLinkViewHelper.php ├── Service │ ├── ResourceOwnerHookInterface.php │ ├── AuthenticationContextService.php │ ├── OpenIdConnectService.php │ └── OAuthService.php ├── Provider │ └── GenericOpenIdProvider.php ├── Middleware │ ├── AuthenticationUrlRequest.php │ └── OauthCallback.php ├── Security │ └── JwtTrait.php ├── Hooks │ └── DataHandlerOidc.php ├── OidcConfiguration.php └── Controller │ └── LoginController.php ├── docker-compose.yaml ├── ext_emconf.php ├── .editorconfig ├── ext_localconf.php ├── composer.json ├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── ext_conf_template.txt ├── .phpstorm.meta.php ├── README.md └── LICENSE /Resources/Private/.htaccess: -------------------------------------------------------------------------------- 1 | deny from all -------------------------------------------------------------------------------- /Build/.dockerignore: -------------------------------------------------------------------------------- 1 | typo3-v12/vendor 2 | typo3-v13/vendor 3 | -------------------------------------------------------------------------------- /Build/.gitignore: -------------------------------------------------------------------------------- 1 | /docker-compose.override.yml 2 | /docker-compose.*.override.yml 3 | -------------------------------------------------------------------------------- /Build/typo3/typo3-v12/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/.gitignore 3 | !/composer.json 4 | !/composer.lock 5 | -------------------------------------------------------------------------------- /Build/typo3/typo3-v13/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/.gitignore 3 | !/composer.json 4 | !/composer.lock 5 | -------------------------------------------------------------------------------- /Build/typo3/typo3/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | nbproject 4 | /var/* 5 | !/var/labels 6 | /vendor 7 | /public/* 8 | !/public/.htaccess 9 | -------------------------------------------------------------------------------- /Build/playwright/e2e/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Playwright 3 | node_modules/ 4 | /test-results/ 5 | /playwright-report/ 6 | /blob-report/ 7 | /playwright/.cache/ 8 | -------------------------------------------------------------------------------- /Resources/Public/Icons/icon_tx_oidc_application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xperseguers/t3ext-oidc/HEAD/Resources/Public/Icons/icon_tx_oidc_application.png -------------------------------------------------------------------------------- /Build/docker-compose.override.sample.yml: -------------------------------------------------------------------------------- 1 | services: 2 | v12: 3 | volumes: 4 | - ../:/app/packages/oidc/ 5 | 6 | v13: 7 | volumes: 8 | - ../:/app/packages/oidc/ 9 | -------------------------------------------------------------------------------- /Build/typo3/typo3/packages/oidc-sitepackage/Configuration/Sets/Oidc/constants.typoscript: -------------------------------------------------------------------------------- 1 | plugin.tx_felogin_login.view.templateRootPath = EXT:oidc-sitepackage/Resources/Private/Felogin/Templates/ 2 | -------------------------------------------------------------------------------- /scripts/phpcs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TOOL_DIR=.Build/tools/phpcs 4 | TOOL_PACKAGE="friendsofphp/php-cs-fixer" 5 | TOOL_COMMAND="php-cs-fixer fix -v --diff" 6 | 7 | source scripts/runphptool.sh 8 | -------------------------------------------------------------------------------- /Build/typo3/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | runuser -u www-data vendor/bin/typo3 install:extensionsetupifpossible 5 | vendor/bin/typo3 backend:createadmin admin password || true 6 | 7 | exec apache2-foreground 8 | -------------------------------------------------------------------------------- /ext_tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE fe_users 2 | ( 3 | tx_oidc varchar(100) DEFAULT '' NOT NULL, 4 | 5 | KEY fk_oidc (tx_oidc) 6 | ); 7 | 8 | CREATE TABLE fe_groups 9 | ( 10 | tx_oidc_pattern varchar(255) DEFAULT '' NOT NULL 11 | ); 12 | -------------------------------------------------------------------------------- /Configuration/TCA/Overrides/sys_template.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'provider' => SvgIconProvider::class, 10 | 'source' => 'EXT:oidc/Resources/Public/Icons/Extension.svg', 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /Build/playwright/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": {}, 6 | "keywords": [], 7 | "author": "", 8 | "license": "ISC", 9 | "description": "", 10 | "devDependencies": { 11 | "@playwright/test": "1.56.1", 12 | "@types/node": "^22.15.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Build/create_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | openssl req -x509 -nodes -days 1460 -sha256 -newkey rsa:2048 \ 3 | -subj '/CN=t3ext-oidc.test' \ 4 | -reqexts SAN -extensions SAN \ 5 | -config <(cat /etc/ssl/openssl.cnf \ 6 | <(printf "\n[SAN]\nsubjectAltName=DNS:*.t3ext-oidc.test")) \ 7 | -keyout ./certs/developer.key \ 8 | -out ./certs/developer.pem 9 | -------------------------------------------------------------------------------- /Build/typo3/docker/000-default.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName ${SERVER_NAME} 3 | 4 | ServerAdmin webmaster@localhost 5 | DocumentRoot /app/public 6 | 7 | Require all granted 8 | AllowOverride All 9 | 10 | 11 | ErrorLog ${APACHE_LOG_DIR}/error.log 12 | CustomLog ${APACHE_LOG_DIR}/access.log combined 13 | 14 | -------------------------------------------------------------------------------- /Build/typo3/docker/typo3.ini: -------------------------------------------------------------------------------- 1 | always_populate_raw_post_data = -1 2 | max_execution_time = 240 3 | memory_limit = 2G 4 | max_input_vars = 1500 5 | upload_max_filesize = 32M 6 | post_max_size = 32M 7 | log_errors = On 8 | error_reporting = E_ALL & ~E_NOTICE 9 | date.timezone = Europe/Berlin 10 | 11 | xdebug.mode = off 12 | xdebug.client_host = host.docker.internal 13 | xdebug.cli_color = 1 14 | xdebug.max_nesting_level = 1000 15 | -------------------------------------------------------------------------------- /Classes/Event/GetAuthorizationUrlEvent.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'exclude' => true, 8 | 'label' => 'LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:fe_groups.tx_oidc_pattern', 9 | 'config' => [ 10 | 'type' => 'input', 11 | 'size' => 30, 12 | ], 13 | ], 14 | ]; 15 | 16 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('fe_groups', $tempColumns); 17 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes('fe_groups', 'tx_oidc_pattern'); 18 | -------------------------------------------------------------------------------- /Build/docker-compose.build.yml: -------------------------------------------------------------------------------- 1 | services: 2 | composer-v12: 3 | build: 4 | context: typo3 5 | target: php-composer 6 | image: composer:2.7.2 7 | volumes: 8 | - ./typo3/typo3-v12:/app 9 | - ..:/app/packages/oidc 10 | - ./typo3/typo3/packages/oidc-sitepackage:/app/packages/oidc-sitepackage 11 | command: install 12 | 13 | composer-v13: 14 | build: 15 | context: typo3 16 | target: php-composer 17 | volumes: 18 | - ./typo3/typo3-v13:/app 19 | - ..:/app/packages/oidc 20 | - ./typo3/typo3/packages/oidc-sitepackage:/app/packages/oidc-sitepackage 21 | command: install 22 | -------------------------------------------------------------------------------- /ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'OpenID Connect Authentication', 5 | 'description' => 'This extension uses OpenID Connect to authenticate users.', 6 | 'category' => 'services', 7 | 'author' => 'Xavier Perseguers, Markus Klein', 8 | 'author_company' => 'Causal Sàrl, Reelworx GmbH', 9 | 'author_email' => 'xavier@causal.ch', 10 | 'state' => 'stable', 11 | 'version' => '4.0.0', 12 | 'constraints' => [ 13 | 'depends' => [ 14 | 'php' => '8.1.99-8.4.99', 15 | 'typo3' => '11.5.0-13.4.99', 16 | ], 17 | 'conflicts' => [], 18 | 'suggests' => [], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /Build/typo3/typo3/config/sites/main/config.yaml: -------------------------------------------------------------------------------- 1 | rootPageId: 1 2 | base: 'https://%env(SERVER_NAME)%/' 3 | dependencies: 4 | - somebdyelse/oidc-sitepackage 5 | baseVariants: { } 6 | languages: 7 | - 8 | title: English 9 | enabled: true 10 | languageId: '0' 11 | base: /en/ 12 | typo3Language: en 13 | locale: en_US.UTF-8 14 | iso-639-1: en 15 | navigationTitle: EN 16 | hreflang: en-US 17 | direction: ltr 18 | flag: us 19 | - 20 | title: Deutsch 21 | enabled: true 22 | languageId: '1' 23 | base: /de/ 24 | typo3Language: de 25 | locale: de_DE.UTF-8 26 | iso-639-1: de 27 | navigationTitle: DE 28 | hreflang: de_DE 29 | direction: ltr 30 | fallbackType: strict 31 | flag: de 32 | -------------------------------------------------------------------------------- /Configuration/RequestMiddlewares.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'oidccallback' => [ 6 | 'target' => \Causal\Oidc\Middleware\OauthCallback::class, 7 | 'after' => [ 8 | 'typo3/cms-core/normalized-params-attribute', 9 | ], 10 | 'before' => [ 11 | 'typo3/cms-frontend/eid', 12 | ], 13 | ], 14 | 'oidcauthurl' => [ 15 | 'target' => \Causal\Oidc\Middleware\AuthenticationUrlRequest::class, 16 | 'after' => [ 17 | 'typo3/cms-frontend/site', 18 | ], 19 | 'before' => [ 20 | 'typo3/cms-frontend/authentication', 21 | ], 22 | ], 23 | ], 24 | ]; 25 | -------------------------------------------------------------------------------- /Build/typo3/typo3/packages/oidc-sitepackage/Configuration/Sets/Oidc/setup.typoscript: -------------------------------------------------------------------------------- 1 | plugin.tx_oidc_login.defaultRedirectPid = 1 2 | 3 | page = PAGE 4 | page { 5 | 10 = TEXT 6 | 10 { 7 | value = Link to login page 8 | wrap =
|
9 | typolink { 10 | parameter = 3 11 | } 12 | } 13 | 14 | 20 = TEXT 15 | 20 { 16 | value = Link to login page with redirect_url 17 | wrap =
|
18 | typolink { 19 | parameter = 3 20 | additionalParams = &redirect_url=/en/login-redirect-target/ 21 | } 22 | } 23 | 24 | 30 = TEXT 25 | 30.value =
26 | 27 | 40 = CONTENT 28 | 40 { 29 | table = tt_content 30 | select { 31 | orderBy = sorting 32 | where = {#colPos}=0 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Configuration/TCA/Overrides/fe_users.php: -------------------------------------------------------------------------------- 1 | get('oidc') ?? []; 6 | 7 | $tempColumns = [ 8 | 'tx_oidc' => [ 9 | 'exclude' => true, 10 | 'label' => 'LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:fe_users.tx_oidc', 11 | 'config' => [ 12 | 'type' => 'input', 13 | 'size' => 30, 14 | 'readOnly' => !($settings['frontendUserMustExistLocally'] ?? ''), 15 | ], 16 | ], 17 | ]; 18 | 19 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('fe_users', $tempColumns); 20 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes('fe_users', 'tx_oidc'); 21 | -------------------------------------------------------------------------------- /Classes/Factory/OAuthProviderFactoryInterface.php: -------------------------------------------------------------------------------- 1 | getUser(); 15 | $requestToken = $event->getRequestToken(); 16 | if ($requestToken instanceof RequestToken) { 17 | // fine, there is a valid request-token 18 | return; 19 | } 20 | if (!isset($event->getRequest()->getQueryParams()['tx_oidc'])) { 21 | return; 22 | } 23 | $event->setRequestToken(RequestToken::create('core/user-auth/' . strtolower($user->loginType))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Configuration/TypoScript/setup.typoscript: -------------------------------------------------------------------------------- 1 | # Setting oidc plugin TypoScript 2 | plugin.tx_oidc_login = USER_INT 3 | plugin.tx_oidc_login { 4 | userFunc = Causal\Oidc\Controller\LoginController->login 5 | defaultRedirectPid = 6 | # Additional URL parameters for the authorization URL of the identity server 7 | authorizationUrlOptions { 8 | # login_theme = dark 9 | } 10 | } 11 | 12 | # Setting oidc plugin TypoScript 13 | tt_content.oidc_login =< plugin.tx_oidc_login 14 | 15 | # format is "TYPO3 column" = 16 | plugin.tx_oidc.mapping { 17 | fe_users { 18 | username = 19 | name = 20 | first_name = 21 | last_name = 22 | address = 23 | title = 24 | zip = <postal_code> 25 | city = <locality> 26 | country = <country> 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Build/typo3/docker/000-default-ssl.conf: -------------------------------------------------------------------------------- 1 | <VirtualHost *:443> 2 | ServerName ${SERVER_NAME} 3 | 4 | ServerAdmin webmaster@localhost 5 | DocumentRoot /app/public 6 | <Directory /app/public> 7 | Require all granted 8 | AllowOverride All 9 | </Directory> 10 | 11 | ErrorLog ${APACHE_LOG_DIR}/error.log 12 | CustomLog ${APACHE_LOG_DIR}/access.log combined 13 | 14 | SSLEngine on 15 | SSLCertificateFile /etc/ssl/certs/developer.pem 16 | SSLCertificateKeyFile /etc/ssl/private/developer.key 17 | 18 | 19 | <FilesMatch "\.(cgi|shtml|phtml|php)$"> 20 | SSLOptions +StdEnvVars 21 | </FilesMatch> 22 | <Directory /usr/lib/cgi-bin> 23 | SSLOptions +StdEnvVars 24 | </Directory> 25 | 26 | BrowserMatch "MSIE [2-6]" \ 27 | nokeepalive ssl-unclean-shutdown \ 28 | downgrade-1.0 force-response-1.0 29 | # MSIE 7 and newer should be able to use keepalive 30 | BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown 31 | </VirtualHost> 32 | -------------------------------------------------------------------------------- /Classes/Event/AuthenticationProcessMappingEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Event; 19 | 20 | use Psr\Http\Message\RequestInterface; 21 | 22 | final class AuthenticationProcessMappingEvent 23 | { 24 | public function __construct( 25 | public readonly RequestInterface $request, 26 | public readonly string $databaseTable, 27 | public readonly array $existingUser, 28 | public readonly array $resourceOwner, 29 | public array $mappedData, 30 | ) {} 31 | } 32 | -------------------------------------------------------------------------------- /Classes/EventListener/GetAuthorizationUrlSetLanguageEventListener.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc\EventListener; 6 | 7 | use Causal\Oidc\Event\GetAuthorizationUrlEvent; 8 | use TYPO3\CMS\Core\Site\Entity\SiteLanguage; 9 | 10 | class GetAuthorizationUrlSetLanguageEventListener 11 | { 12 | public function __invoke(GetAuthorizationUrlEvent $event): void 13 | { 14 | $settings = $event->settings; 15 | $languageOption = $settings->authorizeLanguageParameter; 16 | if (!$languageOption) { 17 | return; 18 | } 19 | 20 | $language = 'en'; 21 | $request = $event->request; 22 | if ($request) { 23 | /** @var SiteLanguage $siteLanguage */ 24 | $siteLanguage = $request->getAttribute('language', $request->getAttribute('site')->getDefaultLanguage()); 25 | $language = $siteLanguage->getLocale()->getLanguageCode(); 26 | } 27 | $event->options[$languageOption] = $language; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Classes/AuthenticationContext.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc; 6 | 7 | use Causal\Oidc\Security\JwtTrait; 8 | 9 | class AuthenticationContext 10 | { 11 | use JwtTrait; 12 | 13 | public function __construct( 14 | public readonly string $state, 15 | public readonly string $loginUrl, 16 | public readonly string $authorizationUrl, 17 | public readonly string $requestId, 18 | public readonly bool $secureContext, 19 | public readonly ?string $codeVerifier = null, 20 | ) {} 21 | 22 | public static function fromJwt(string $cookieValue): self 23 | { 24 | $payload = self::decodeJwt($cookieValue, self::createSigningKeyFromEncryptionKey(static::class), true); 25 | return new self(...$payload); 26 | } 27 | 28 | public function toHashSignedJwt(): string 29 | { 30 | $payload = get_object_vars($this); 31 | return self::encodeHashSignedJwt($payload, self::createSigningKeyFromEncryptionKey(static::class)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Build/typo3/typo3/packages/oidc-sitepackage/ext_localconf.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use TYPO3\CMS\Core\Information\Typo3Version; 4 | use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; 5 | 6 | defined('TYPO3') || die(); 7 | 8 | if ((new Typo3Version())->getMajorVersion() < 13) { 9 | ExtensionManagementUtility::addTypoScriptSetup("@import 'EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript'"); 10 | ExtensionManagementUtility::addTypoScriptSetup("@import 'EXT:fluid_styled_content/Configuration/TypoScript/Styling/setup.typoscript'"); 11 | ExtensionManagementUtility::addTypoScriptSetup("@import 'EXT:oidc-sitepackage/Configuration/Sets/Oidc/setup.typoscript'"); 12 | ExtensionManagementUtility::addTypoScriptConstants("@import 'EXT:fluid_styled_content/Configuration/TypoScript/constants.typoscript'"); 13 | ExtensionManagementUtility::addTypoScriptConstants("@import 'EXT:fluid_styled_content/Configuration/TypoScript/Styling/constants.typoscript'"); 14 | ExtensionManagementUtility::addTypoScriptConstants("@import 'EXT:oidc-sitepackage/Configuration/Sets/Oidc/constants.typoscript'"); 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_style = space 11 | indent_size = 4 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | # TS/JS-Files 16 | [*.{ts,js}] 17 | indent_size = 2 18 | 19 | # JSON-Files 20 | [*.json] 21 | indent_style = tab 22 | 23 | # ReST-Files 24 | [*.{rst,rst.txt}] 25 | indent_size = 4 26 | max_line_length = 80 27 | 28 | # Markdown-Files 29 | [*.md] 30 | max_line_length = 80 31 | 32 | # YAML-Files 33 | [*.{yaml,yml}] 34 | indent_size = 2 35 | 36 | # NEON-Files 37 | [*.neon] 38 | indent_size = 2 39 | indent_style = tab 40 | 41 | # package.json 42 | [package.json] 43 | indent_size = 2 44 | 45 | # TypoScript 46 | [*.{typoscript,tsconfig}] 47 | indent_size = 2 48 | 49 | # XLF-Files 50 | [*.xlf] 51 | indent_style = tab 52 | 53 | # SQL-Files 54 | [*.sql] 55 | indent_style = tab 56 | indent_size = 2 57 | 58 | # .htaccess 59 | [{_.htaccess,.htaccess}] 60 | indent_style = tab 61 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/OidcLinkViewHelper.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\ViewHelpers; 19 | 20 | use Causal\Oidc\Service\OpenIdConnectService; 21 | use TYPO3\CMS\Core\Utility\GeneralUtility; 22 | use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; 23 | 24 | class OidcLinkViewHelper extends AbstractViewHelper 25 | { 26 | /** 27 | * @return string Authentication Request URL 28 | */ 29 | public function render(): string 30 | { 31 | $url = GeneralUtility::makeInstance(OpenIdConnectService::class)->getAuthenticationRequestUrl(); 32 | return (string)$url; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Classes/Factory/RequestFactory.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc\Factory; 6 | 7 | use GuzzleHttp\Psr7\Utils; 8 | use League\OAuth2\Client\Tool\RequestFactory as Oauth2RequestFactory; 9 | use Psr\Http\Message\RequestFactoryInterface; 10 | 11 | class RequestFactory extends Oauth2RequestFactory 12 | { 13 | protected RequestFactoryInterface $requestFactory; 14 | 15 | public function __construct(RequestFactoryInterface $requestFactory) 16 | { 17 | $this->requestFactory = $requestFactory; 18 | } 19 | 20 | public function getRequest( 21 | $method, 22 | $uri, 23 | array $headers = [], 24 | $body = null, 25 | $version = '1.1' 26 | ) { 27 | $request = $this->requestFactory->createRequest($method, $uri); 28 | foreach ($headers as $name => $value) { 29 | $request = $request->withHeader((string)$name, $value); 30 | } 31 | if ($body !== '' && $body !== null) { 32 | $request = $request->withBody(Utils::streamFor($body)); 33 | } 34 | return $request->withProtocolVersion($version); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Resources/Public/Icons/Extension.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 2 | 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'> 3 | <svg height="512px" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" width="512px" 4 | xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="_x32_39-openid"><g><path d="M234.849,419v6.623c-79.268-9.958-139.334-53.393-139.334-105.757 c0-39.313,33.873-73.595,84.485-92.511L178.023,180C88.892,202.497,26.001,256.607,26.001,319.866 c0,76.288,90.871,139.128,208.95,149.705l0.018-0.009V419H234.849z" style="fill:#B2B2B2;"/><polygon 5 | points="304.772,436.713 304.67,436.713 304.67,221.667 304.67,213.667 304.67,42.429 234.849,78.25 234.849,221.667 234.969,221.667 234.969,469.563 " 6 | style="fill:#F7931E;"/><path 7 | d="M485.999,291.938l-9.446-100.114l-35.938,20.331C415.087,196.649,382.5,177.5,340,177.261 l0.002,36.406v7.498c3.502,0.968,6.923,2.024,10.301,3.125c14.145,4.611,27.176,10.352,38.666,17.128l-37.786,21.254 L485.999,291.938z" 8 | style="fill:#B2B2B2;"/></g></g> 9 | <g id="Layer_1"/></svg> -------------------------------------------------------------------------------- /Build/certs/developer.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDATCCAemgAwIBAgIUCYqKIRipKZl7qK86hE/lleuMS30wDQYJKoZIhvcNAQEL 3 | BQAwGjEYMBYGA1UEAwwPdDNleHQtb2lkYy50ZXN0MB4XDTI1MDUwMzE1NTcxMVoX 4 | DTI5MDUwMjE1NTcxMVowGjEYMBYGA1UEAwwPdDNleHQtb2lkYy50ZXN0MIIBIjAN 5 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqb6USux8uiOkQ9BA8mbolkxk0DDN 6 | ELlaxeWjpq596xwChXcaN7QtF1uGStSXmgtrknePNNhZ1agt7rf1YRgX6fg1iv+5 7 | Ml3MZ1sruzb/s+c4raQPGPFINFMyVBZI/gHLPAX+93gThLGgQWLTPLBlk9nC0NRe 8 | /Lyo+yI5LJKr/aYS12E6A9GAMFGayi9N4i5w1goDz20597fDAyLE6agIanu6uLtA 9 | CYhY7wGbBLbwGHPPPGBaUP/Lp9pvXXbP37e9eC+LXd49dhATarSjFCsHml5Xvt9c 10 | RkAhilbOyHnSZRcT0iOPJNqmjzbKzYr7cKGEBr8FuxFFc++6zlHmZrCokwIDAQAB 11 | oz8wPTAcBgNVHREEFTATghEqLnQzZXh0LW9pZGMudGVzdDAdBgNVHQ4EFgQU+NnB 12 | gLIN/yveiuRjRRdeDKhZsJQwDQYJKoZIhvcNAQELBQADggEBACFHQbKZhybqZjWV 13 | fhPhBQuBqjwMnle29VGmPJ/l7X6iMxjEuEFpWLNj6mPzAM07RP825SAWNEUlkhmF 14 | ZkxurjE8OnpAajD2n6Iq68ro4fLstyifNVJ/ExXFequcbKibYYRyYN+ZOyufhzLF 15 | eaQhTzITS++6+Bwz7vmpH88eNtEPPsEiC+UHX8sWId/Vn0YkmeoVjfzX+HEsuTwE 16 | 3a6e+5kv2MMJ+wttgnMJJK3vwGTM4tOROwuWQAqXSQLYbp5ECUaJMfQapNAXtrJD 17 | F0Qlj/9pkoaCQhaBuSqQRZLCXbfJ3LdPDEMlJeQk0gL/JM9c2Cf5FfYofySgUGDv 18 | 5y/nir4= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /Classes/Event/AuthenticationPreUserEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Event; 19 | 20 | use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; 21 | 22 | final class AuthenticationPreUserEvent 23 | { 24 | public array $loginData = []; 25 | 26 | protected AbstractAuthenticationService $authenticationService; 27 | 28 | public bool $shouldProcess = true; 29 | 30 | public function __construct(array $loginData, AbstractAuthenticationService $authenticationService) 31 | { 32 | $this->loginData = $loginData; 33 | } 34 | 35 | public function getAuthenticationService(): AbstractAuthenticationService 36 | { 37 | return $this->authenticationService; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Classes/Service/ResourceOwnerHookInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Service; 19 | 20 | /** 21 | * Interface for hooks related to the resource owner. 22 | */ 23 | interface ResourceOwnerHookInterface 24 | { 25 | /** 26 | * Post-process for the user record (which is already persisted to the database). 27 | * The full record will automatically get reloaded from database after the hook 28 | * have been invoked. 29 | * 30 | * @param string $context The TYPO3 context (either 'BE' or 'FE') 31 | * @param array $user TYPO3 user record 32 | * @param array $data OpenID Connect data 33 | */ 34 | public function postProcessUser(string $context, array $user, array $data): void; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Build/playwright/e2e/tests/login-via-uri-view-helper.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('Login via OidcLinkViewHelper', async ({ page }) => { 4 | await page.goto('/'); 5 | await page.getByRole('link', { name: 'Login with OpenID Connect' }).click(); 6 | await expect(page).toHaveURL(new RegExp('http://oidc.t3ext-oidc.test/')); 7 | 8 | await page.getByLabel('Username').fill('User1'); 9 | await page.getByLabel('Password').fill('pwd'); 10 | await page.getByRole('button', {name: 'Login'}).click() 11 | await expect(page).toHaveURL(new RegExp('/')); 12 | await expect(page.getByText('You are now logged in as \'1\'')).toBeVisible(); 13 | 14 | await page.goto('/en/'); 15 | await expect(page.getByText('Username 1')).toBeVisible(); 16 | 17 | await page.getByRole('button', {name: 'Logout'}).click(); 18 | await expect(page.getByText('You have logged out.')).toBeVisible(); 19 | 20 | await page.getByRole('link', { name: 'Login with OpenID Connect' }).click(); 21 | await expect(page.getByText('You are now logged in as \'1\'')).toBeVisible(); 22 | 23 | await page.goto('/en/'); 24 | await expect(page.getByText('Username 1')).toBeVisible(); 25 | 26 | await page.getByRole('button', {name: 'Logout'}).click(); 27 | await expect(page.getByText('You have logged out.')).toBeVisible(); 28 | }); 29 | -------------------------------------------------------------------------------- /Classes/Provider/GenericOpenIdProvider.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc\Provider; 6 | 7 | use League\OAuth2\Client\Provider\Exception\IdentityProviderException; 8 | use League\OAuth2\Client\Provider\GenericProvider; 9 | use League\OAuth2\Client\Token\AccessToken; 10 | use UnexpectedValueException; 11 | 12 | class GenericOpenIdProvider extends GenericProvider 13 | { 14 | /** 15 | * @param AccessToken $token 16 | * @return mixed 17 | * @throws UnexpectedValueException 18 | * @throws IdentityProviderException 19 | */ 20 | protected function fetchResourceOwnerDetails(AccessToken $token) 21 | { 22 | if ($this->getResourceOwnerDetailsUrl($token)) { 23 | // Using the access token, we may look up details about the resource owner 24 | return parent::fetchResourceOwnerDetails($token); 25 | } 26 | 27 | // extract resource owner from ID token 28 | $jwt = $token->getToken(); 29 | $jwtDecoded = base64_decode(str_replace(['_', '-'], ['/' - '+'], explode('.', $jwt)[1])); 30 | $resourceOwner = json_decode($jwtDecoded, true); 31 | if (json_last_error() !== JSON_ERROR_NONE) { 32 | throw new UnexpectedValueException('The provided JWT is invalid', 1759222069); 33 | } 34 | return $resourceOwner; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Configuration/Services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: false 6 | 7 | Causal\Oidc\: 8 | resource: '../Classes/*' 9 | 10 | Causal\Oidc\Service\AuthenticationService: 11 | public: true 12 | 13 | Causal\Oidc\Service\OAuthService: 14 | public: true 15 | 16 | Causal\Oidc\Service\OpenIdConnectService: 17 | public: true 18 | 19 | Causal\Oidc\Factory\RequestFactory: 20 | public: true 21 | 22 | Causal\Oidc\Factory\GenericOAuthProviderFactory: 23 | public: true 24 | 25 | Causal\Oidc\OidcConfiguration: 26 | public: true 27 | 28 | Causal\Oidc\EventListener\ProcessRequestTokenListener: 29 | tags: 30 | - name: event.listener 31 | identifier: 'causal/oidc-request-token' 32 | event: TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent 33 | 34 | Causal\Oidc\EventListener\GetAuthorizationUrlSetLanguageEventListener: 35 | tags: 36 | - name: event.listener 37 | identifier: 'causal/oidc-get-authorization-url-set-language' 38 | event: Causal\Oidc\Event\GetAuthorizationUrlEvent 39 | 40 | 41 | 42 | # Causal\Oidc\Frontend\FrontendSimulationV13 instantiates two internal core classes via GeneralUtility::makeInstance 43 | TYPO3\CMS\Frontend\Page\PageInformationFactory: 44 | public: true 45 | TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory: 46 | public: true 47 | -------------------------------------------------------------------------------- /Build/typo3/typo3-v12/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somebdyelse/oidc-v12", 3 | "type": "project", 4 | "config": { 5 | "allow-plugins": { 6 | "typo3/class-alias-loader": true, 7 | "typo3/cms-composer-installers": true 8 | }, 9 | "platform": { 10 | "php": "8.2", 11 | "ext-intl": "1" 12 | }, 13 | "sort-packages": true 14 | }, 15 | "require": { 16 | "causal/oidc": "@dev", 17 | "helhum/typo3-console": "^8.2", 18 | "somebdyelse/oidc-sitepackage": "@dev", 19 | "typo3/cms-backend": "^12.4", 20 | "typo3/cms-belog": "^12.4", 21 | "typo3/cms-beuser": "^12.4", 22 | "typo3/cms-core": "^12.4", 23 | "typo3/cms-dashboard": "^12.4", 24 | "typo3/cms-extbase": "^12.4", 25 | "typo3/cms-extensionmanager": "^12.4", 26 | "typo3/cms-felogin": "^12.4", 27 | "typo3/cms-filelist": "^12.4", 28 | "typo3/cms-fluid": "^12.4", 29 | "typo3/cms-fluid-styled-content": "^12.4", 30 | "typo3/cms-form": "^12.4", 31 | "typo3/cms-frontend": "^12.4", 32 | "typo3/cms-impexp": "^12.4", 33 | "typo3/cms-info": "^12.4", 34 | "typo3/cms-install": "^12.4", 35 | "typo3/cms-reactions": "^12.4", 36 | "typo3/cms-rte-ckeditor": "^12.4", 37 | "typo3/cms-seo": "^12.4", 38 | "typo3/cms-setup": "^12.4", 39 | "typo3/cms-sys-note": "^12.4", 40 | "typo3/cms-t3editor": "^12.4", 41 | "typo3/cms-tstemplate": "^12.4", 42 | "typo3/cms-viewpage": "^12.4", 43 | "typo3/cms-webhooks": "^12.4" 44 | }, 45 | "repositories": [ 46 | { 47 | "type": "path", 48 | "url": "./packages/*" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /Build/typo3/typo3-v13/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somebdyelse/oidc-v13", 3 | "type": "project", 4 | "config": { 5 | "allow-plugins": { 6 | "typo3/class-alias-loader": true, 7 | "typo3/cms-composer-installers": true 8 | }, 9 | "platform": { 10 | "php": "8.3", 11 | "ext-intl": "1" 12 | }, 13 | "sort-packages": true 14 | }, 15 | "require": { 16 | "causal/oidc": "@dev", 17 | "helhum/typo3-console": "^8.2", 18 | "somebdyelse/oidc-sitepackage": "@dev", 19 | "typo3/cms-backend": "^13.4", 20 | "typo3/cms-belog": "^13.4", 21 | "typo3/cms-beuser": "^13.4", 22 | "typo3/cms-core": "^13.4", 23 | "typo3/cms-dashboard": "^13.4", 24 | "typo3/cms-extbase": "^13.4", 25 | "typo3/cms-extensionmanager": "^13.4", 26 | "typo3/cms-felogin": "^13.4", 27 | "typo3/cms-filelist": "^13.4", 28 | "typo3/cms-fluid": "^13.4", 29 | "typo3/cms-fluid-styled-content": "^13.4", 30 | "typo3/cms-form": "^13.4", 31 | "typo3/cms-frontend": "^13.4", 32 | "typo3/cms-impexp": "^13.4", 33 | "typo3/cms-info": "^13.4", 34 | "typo3/cms-install": "^13.4", 35 | "typo3/cms-reactions": "^13.4", 36 | "typo3/cms-recycler": "^13.4", 37 | "typo3/cms-rte-ckeditor": "^13.4", 38 | "typo3/cms-seo": "^13.4", 39 | "typo3/cms-setup": "^13.4", 40 | "typo3/cms-sys-note": "^13.4", 41 | "typo3/cms-tstemplate": "^13.4", 42 | "typo3/cms-viewpage": "^13.4", 43 | "typo3/cms-webhooks": "^13.4" 44 | }, 45 | "repositories": [ 46 | { 47 | "type": "path", 48 | "url": "./packages/*" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /Build/playwright/e2e/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // import path from 'path'; 9 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 10 | 11 | /** 12 | * See https://playwright.dev/docs/test-configuration. 13 | */ 14 | export default defineConfig({ 15 | testDir: './tests', 16 | /* Run tests in files in parallel */ 17 | fullyParallel: true, 18 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 19 | forbidOnly: !!process.env.CI, 20 | /* Retry on CI only */ 21 | retries: process.env.CI ? 2 : 0, 22 | /* Opt out of parallel tests on CI. */ 23 | workers: process.env.CI ? 1 : undefined, 24 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 25 | reporter: 'html', 26 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 27 | use: { 28 | ...devices['Desktop Chrome'], 29 | trace: 'on-first-retry', 30 | ignoreHTTPSErrors: true, 31 | }, 32 | 33 | /* Configure projects for major browsers */ 34 | projects: [ 35 | { 36 | name: 'v12', 37 | use: { 38 | baseURL: 'https://v12.t3ext-oidc.test', 39 | }, 40 | }, 41 | { 42 | name: 'v13', 43 | use: { 44 | baseURL: 'https://v13.t3ext-oidc.test', 45 | }, 46 | }, 47 | ], 48 | }); 49 | -------------------------------------------------------------------------------- /Classes/Event/AuthenticationGetUserEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Event; 19 | 20 | use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; 21 | 22 | final class AuthenticationGetUserEvent 23 | { 24 | protected array|bool $user; 25 | 26 | protected AbstractAuthenticationService $authenticationService; 27 | 28 | public function __construct(bool|array $user, AbstractAuthenticationService $authenticationService) 29 | { 30 | $this->user = $user; 31 | $this->authenticationService = $authenticationService; 32 | } 33 | 34 | /** 35 | * Array with user if authentication was successful or false on failure. 36 | */ 37 | public function getUser(): bool|array 38 | { 39 | return $this->user; 40 | } 41 | 42 | public function setUser(bool|array $user): void 43 | { 44 | $this->user = $user; 45 | } 46 | 47 | public function getAuthenticationService(): AbstractAuthenticationService 48 | { 49 | return $this->authenticationService; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Classes/Event/ModifyResourceOwnerEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Event; 19 | 20 | use League\OAuth2\Client\Token\AccessToken; 21 | use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; 22 | 23 | final class ModifyResourceOwnerEvent 24 | { 25 | public function __construct( 26 | /** 27 | * @var array resource owner data 28 | */ 29 | protected array $resourceOwner, 30 | protected AbstractAuthenticationService $authenticationService, 31 | protected AccessToken $accessToken, 32 | ) {} 33 | 34 | public function getResourceOwner(): array 35 | { 36 | return $this->resourceOwner; 37 | } 38 | 39 | public function setResourceOwner(array $resourceOwner): void 40 | { 41 | $this->resourceOwner = $resourceOwner; 42 | } 43 | 44 | public function getAuthenticationService(): AbstractAuthenticationService 45 | { 46 | return $this->authenticationService; 47 | } 48 | 49 | public function getAccessToken(): AccessToken 50 | { 51 | return $this->accessToken; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Classes/Event/ModifyUserEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Event; 19 | 20 | use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; 21 | 22 | final class ModifyUserEvent 23 | { 24 | /** 25 | * @var array fe_users record 26 | */ 27 | protected array $user; 28 | 29 | protected array $oidcResourceOwner; 30 | 31 | protected AbstractAuthenticationService $authenticationService; 32 | 33 | public function __construct( 34 | array $user, 35 | AbstractAuthenticationService $authenticationService, 36 | array $oidcResourceOwner 37 | ) { 38 | $this->user = $user; 39 | $this->authenticationService = $authenticationService; 40 | $this->oidcResourceOwner = $oidcResourceOwner; 41 | } 42 | 43 | public function getUser(): array 44 | { 45 | return $this->user; 46 | } 47 | 48 | public function setUser(array $user): void 49 | { 50 | $this->user = $user; 51 | } 52 | 53 | public function getAuthenticationService(): AbstractAuthenticationService 54 | { 55 | return $this->authenticationService; 56 | } 57 | 58 | public function getOidcResourceOwner(): array 59 | { 60 | return $this->oidcResourceOwner; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Build/certs/developer.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpvpRK7Hy6I6RD 3 | 0EDyZuiWTGTQMM0QuVrF5aOmrn3rHAKFdxo3tC0XW4ZK1JeaC2uSd4802FnVqC3u 4 | t/VhGBfp+DWK/7kyXcxnWyu7Nv+z5zitpA8Y8Ug0UzJUFkj+Acs8Bf73eBOEsaBB 5 | YtM8sGWT2cLQ1F78vKj7Ijkskqv9phLXYToD0YAwUZrKL03iLnDWCgPPbTn3t8MD 6 | IsTpqAhqe7q4u0AJiFjvAZsEtvAYc888YFpQ/8un2m9dds/ft714L4td3j12EBNq 7 | tKMUKweaXle+31xGQCGKVs7IedJlFxPSI48k2qaPNsrNivtwoYQGvwW7EUVz77rO 8 | UeZmsKiTAgMBAAECggEAUuQNn47fk7he+Xyjkpv9XZP9sQr7wPx63uP2qClStTKu 9 | 6oUQaP5T/LGFoVoylBealuJKrPgv2l2v3IC504nD983tO60IP5e9fwVvFBPWB4DU 10 | 1Tql4rzE0USFcbmWOHAg0nVtHmTlRynEg9miJONRH0O6Vmc24X2xmT2LpQAU/B5F 11 | xwANin9hK23b4jAWdGb+d7bUVCaH4wY2mz9tPkCzAepPjnwocpfTKdwAyf7obnp7 12 | FJndnRMIw7PACS9/+MpbwWZp/DI8P+sg3LIhix/AKPsIKsKFLh0TA/Jt8tK05VQ/ 13 | OYoiyQNWV1Ho5BVNI7rrxVSp12xy6+RTkFEAg491KQKBgQDrKPC2xW/p+TN1+x6J 14 | qMVrKpaL7IG2o/ZJCUk39pYf0RRSQm4RFfkR81Zt3kNNTjJVi+wnNB+6pSb+r8gi 15 | cTC4HBGSupMtEHRz3PCsiJ61dZBAVSgx3/q6gIzGEof4pDj9pWclRumeqT6bcumP 16 | XOIAoDGh4t2ztA7Iq2l9Bd9KKwKBgQC4yY/YTEfCoMMQMNO4VGNAdzPPmNNAWJcm 17 | 7OUcASD+LAV3jNNIAbQ2ED69McfoQmY6E1j3BRSna5LHHxsLWus+4B8MTIA+H2eR 18 | E6ag3EXf0opeo4onr80QCBX4dmT56+z4+cRFJEv+e2pFU+LA1OT9OrCLd66CViPp 19 | LydsDrHvOQKBgQC6sptBkEjEymtXzNI22n46EerKFi/5j/0ViUDPJH+VrQnkMQdv 20 | mzFOVhIVUjO+qUl+74LCsThLl5j49RgAeDsEwIwXIPd9/KVn405UvHa8fCTVXoZ1 21 | 6mwGK88X5lh7R+5TOkFEsCMEKzwgcEAL6OMpOgWNkC8R3+8o1CYA82uOVwKBgBjA 22 | Fp44CVyOP9JJV/JWizy1OqFRrOnvc79o9376v7qLlufeWuZIJCUho+WfZ1E6o4qf 23 | 0Mi1S4DpLa2yAuNQeDXhuwbgsESh3DNYWHcvScSi5JmsefK631KIYrj20quO6Byp 24 | B5rjM7KQQTmr9LEzUsCAPelQTHV0qDaxdCxLA5YxAoGAWgly2C8IWtn+cai8dqN+ 25 | Xqp2kzgJX5rk76VaILBa2XloQOwt73tbKc8PjGh1qMLAXAt4+RwLHMLCXI+9KwyT 26 | lsxexT7wZuCWz45icfO7pB9AdD8RCW8PS1Rv99SdgDLXlRTMCwvtEEk1344nJBEJ 27 | HH23U4Szez0l2jM3RcJhTgw= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /ext_localconf.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | use Causal\Oidc\Hooks\DataHandlerOidc; 6 | use Causal\Oidc\OidcConfiguration; 7 | use Causal\Oidc\Service\AuthenticationService; 8 | use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; 9 | use TYPO3\CMS\Core\Utility\GeneralUtility; 10 | 11 | defined('TYPO3') or die(); 12 | 13 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = DataHandlerOidc::class; 14 | 15 | $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'tx_oidc[code]'; 16 | 17 | $settings = GeneralUtility::makeInstance(OidcConfiguration::class); 18 | 19 | // Service configuration 20 | $subTypes = ''; 21 | if ($settings->enableFrontendAuthentication ?? '') { 22 | $subTypesArr = [ 23 | 'getUserFE', 24 | 'authUserFE', 25 | 'getGroupsFE', 26 | ]; 27 | $subTypes = implode(',', $subTypesArr); 28 | } 29 | 30 | $authenticationClassName = AuthenticationService::class; 31 | ExtensionManagementUtility::addService( 32 | 'oidc', 33 | 'auth' /* sv type */, 34 | $authenticationClassName /* sv key */, 35 | [ 36 | 'title' => 'Authentication service', 37 | 'description' => 'Authentication service for OpenID Connect.', 38 | 'subtype' => $subTypes, 39 | 'available' => true, 40 | 'priority' => $settings->authenticationServicePriority, 41 | 'quality' => $settings->authenticationServiceQuality, 42 | 'os' => '', 43 | 'exec' => '', 44 | 'className' => $authenticationClassName, 45 | ] 46 | ); 47 | 48 | // Require 3rd-party libraries, in case TYPO3 does not run in composer mode 49 | $pharFileName = ExtensionManagementUtility::extPath('oidc') . 'Libraries/league-oauth2-client.phar'; 50 | if (is_file($pharFileName)) { 51 | @include 'phar://' . $pharFileName . '/vendor/autoload.php'; 52 | } 53 | -------------------------------------------------------------------------------- /Build/playwright/e2e/tests/login-via-login-controller.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('Login via LoginController', async ({ page }) => { 4 | await page.goto('/'); 5 | await page.getByRole('link', { name: 'Link to login page', exact: true }).click(); 6 | await expect(page).toHaveURL(new RegExp('http://oidc.t3ext-oidc.test/')); 7 | 8 | await page.getByLabel('Username').fill('User1'); 9 | await page.getByLabel('Password').fill('pwd'); 10 | await page.getByRole('button', {name: 'Login'}).click() 11 | await expect(page).toHaveURL(new RegExp('/en/')); 12 | await expect(page.getByText('Username 1')).toBeVisible(); 13 | 14 | await page.getByRole('button', {name: 'Logout'}).click(); 15 | await expect(page.getByText('You have logged out.')).toBeVisible(); 16 | 17 | await page.getByRole('link', { name: 'Link to login page', exact: true }).click(); 18 | await expect(page.getByText('Username 1')).toBeVisible(); 19 | 20 | await page.getByRole('button', {name: 'Logout'}).click(); 21 | await expect(page.getByText('You have logged out.')).toBeVisible(); 22 | }); 23 | 24 | test('Login via LoginController with redirect url', async ({ page }) => { 25 | await page.goto('/'); 26 | await page.getByRole('link', { name: 'Link to login page with redirect_url' }).click(); 27 | await expect(page).toHaveURL(new RegExp('http://oidc.t3ext-oidc.test/')); 28 | 29 | await page.getByLabel('Username').fill('User1'); 30 | await page.getByLabel('Password').fill('pwd'); 31 | await page.getByRole('button', {name: 'Login'}).click() 32 | await expect(page).toHaveURL(new RegExp('/en/login-redirect-target/$')); 33 | 34 | await page.goto('/en/'); 35 | await expect(page.getByText('Username 1')).toBeVisible(); 36 | 37 | await page.getByRole('button', {name: 'Logout'}).click(); 38 | await expect(page.getByText('You have logged out.')).toBeVisible(); 39 | }); 40 | -------------------------------------------------------------------------------- /Classes/Factory/GenericOAuthProviderFactory.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /** 6 | * This file is part of the "oidc" extension for TYPO3 CMS. 7 | * 8 | * For the full copyright and license information, please read the 9 | * LICENSE.txt file that was distributed with this source code. 10 | */ 11 | 12 | namespace Causal\Oidc\Factory; 13 | 14 | use Causal\Oidc\OidcConfiguration; 15 | use Causal\Oidc\Provider\GenericOpenIdProvider; 16 | use League\OAuth2\Client\Provider\AbstractProvider; 17 | use TYPO3\CMS\Core\Http\Client\GuzzleClientFactory; 18 | use TYPO3\CMS\Core\Utility\GeneralUtility; 19 | 20 | final readonly class GenericOAuthProviderFactory implements OAuthProviderFactoryInterface 21 | { 22 | public function __construct( 23 | private GuzzleClientFactory $clientFactory, 24 | private RequestFactory $requestFactory, 25 | ) {} 26 | 27 | public function create(OidcConfiguration $settings): AbstractProvider 28 | { 29 | $collaborators = [ 30 | 'httpClient' => $this->clientFactory->getClient(), 31 | 'requestFactory' => $this->requestFactory, 32 | ]; 33 | 34 | return new GenericOpenIdProvider( 35 | [ 36 | 'clientId' => $settings->oidcClientKey, 37 | 'clientSecret' => $settings->oidcClientSecret, 38 | 'redirectUri' => $settings->oidcRedirectUri, 39 | 'urlAuthorize' => $settings->endpointAuthorize, 40 | 'urlAccessToken' => $settings->endpointToken, 41 | 'urlResourceOwnerDetails' => $settings->endpointUserInfo, 42 | 'responseResourceOwnerId' => 'sub', 43 | 'accessTokenResourceOwnerId' => 'sub', 44 | 'scopes' => GeneralUtility::trimExplode(',', $settings->oidcClientScopes, true), 45 | 'scopeSeparator' => $settings->oidcClientScopeSeparator, 46 | ], 47 | $collaborators 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Classes/Middleware/AuthenticationUrlRequest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc\Middleware; 6 | 7 | use Causal\Oidc\Service\AuthenticationContextService; 8 | use Causal\Oidc\Service\OpenIdConnectService; 9 | use InvalidArgumentException; 10 | use Psr\Http\Message\ResponseInterface; 11 | use Psr\Http\Message\ServerRequestInterface; 12 | use Psr\Http\Server\MiddlewareInterface; 13 | use Psr\Http\Server\RequestHandlerInterface; 14 | use Psr\Log\LoggerAwareInterface; 15 | use Psr\Log\LoggerAwareTrait; 16 | use Throwable; 17 | use TYPO3\CMS\Core\Http\Response; 18 | 19 | class AuthenticationUrlRequest implements MiddlewareInterface, LoggerAwareInterface 20 | { 21 | use LoggerAwareTrait; 22 | 23 | public function __construct( 24 | protected OpenIdConnectService $openIdConnectService, 25 | protected AuthenticationContextService $authenticationContextService, 26 | ) {} 27 | 28 | /** 29 | * @param ServerRequestInterface $request 30 | * @param RequestHandlerInterface $handler 31 | * @return ResponseInterface 32 | * see https://github.com/thephpleague/oauth2-client 33 | */ 34 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 35 | { 36 | if ($request->getMethod() === 'GET' && $this->openIdConnectService->isAuthenticationRequest($request)) { 37 | try { 38 | $authContext = $this->openIdConnectService->generateAuthenticationContext($request); 39 | $response = $this->openIdConnectService->getAuthorizationRedirect($authContext); 40 | return $response; 41 | } catch (InvalidArgumentException|Throwable $e) { 42 | $this->logger->alert('OIDC authentication provider error', ['exception' => $e]); 43 | // config error or 44 | // whatever the provider did wrong (can be connection errors) 45 | return (new Response())->withStatus(500)->withHeader('x-reason', 'Authentication provider error'); 46 | } 47 | } 48 | return $handler->handle($request); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Classes/Security/JwtTrait.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Security; 19 | 20 | use Firebase\JWT\JWT; 21 | use Firebase\JWT\Key; 22 | 23 | /** 24 | * Trait providing support for JWT using symmetric hash signing. 25 | * 26 | * The benefit of using a trait in this particular case is, that defaults in `self::class` 27 | * (used as a pepper during the singing process) are specific for that a particular implementation. 28 | * 29 | * @internal 30 | * 31 | * IMPORTANT: This is a _partial_ copy of \TYPO3\CMS\Core\Security\JwtTrait in TYPO3 v12 32 | */ 33 | trait JwtTrait 34 | { 35 | private static function getDefaultSigningAlgorithm(): string 36 | { 37 | return 'HS256'; 38 | } 39 | 40 | private static function createSigningKeyFromEncryptionKey(string $pepper = self::class): Key 41 | { 42 | if ($pepper === '') { 43 | $pepper = self::class; 44 | } 45 | $encryptionKey = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] ?? ''; 46 | $keyMaterial = hash('sha256', $encryptionKey) . '/' . $pepper; 47 | return new Key($keyMaterial, self::getDefaultSigningAlgorithm()); 48 | } 49 | 50 | private static function encodeHashSignedJwt(array $payload, Key $key): string 51 | { 52 | return JWT::encode($payload, $key->getKeyMaterial(), self::getDefaultSigningAlgorithm()); 53 | } 54 | 55 | /** 56 | * @param string $jwt 57 | * @param Key $key 58 | * @param bool $associative 59 | * @return \stdClass|array 60 | */ 61 | private static function decodeJwt(string $jwt, Key $key, bool $associative = false) 62 | { 63 | $payload = JWT::decode($jwt, $key); 64 | return $associative ? json_decode(json_encode($payload), true) : $payload; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Classes/Event/AuthenticationFetchUserEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Event; 19 | 20 | use Doctrine\DBAL\Query\Expression\CompositeExpression; 21 | use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; 22 | use TYPO3\CMS\Core\Database\Query\QueryBuilder; 23 | 24 | final class AuthenticationFetchUserEvent 25 | { 26 | protected array $resourceInfo; 27 | 28 | /** 29 | * @var array<string|CompositeExpression> 30 | */ 31 | protected array $conditions; 32 | 33 | protected QueryBuilder $queryBuilder; 34 | 35 | protected AbstractAuthenticationService $authenticationService; 36 | 37 | public function __construct( 38 | array $resourceInfo, 39 | array $conditions, 40 | QueryBuilder $queryBuilder, 41 | AbstractAuthenticationService $authenticationService 42 | ) { 43 | $this->resourceInfo = $resourceInfo; 44 | $this->conditions = $conditions; 45 | $this->queryBuilder = $queryBuilder; 46 | $this->authenticationService = $authenticationService; 47 | } 48 | 49 | public function getResourceInfo(): array 50 | { 51 | return $this->resourceInfo; 52 | } 53 | 54 | /** 55 | * @return array<CompositeExpression|string> 56 | */ 57 | public function getConditions(): array 58 | { 59 | return $this->conditions; 60 | } 61 | 62 | public function setConditions(array $conditions): void 63 | { 64 | $this->conditions = $conditions; 65 | } 66 | 67 | public function getQueryBuilder(): QueryBuilder 68 | { 69 | return $this->queryBuilder; 70 | } 71 | 72 | public function getAuthenticationService(): AbstractAuthenticationService 73 | { 74 | return $this->authenticationService; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Classes/Event/AuthenticationGetUserGroupsEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Event; 19 | 20 | use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService; 21 | 22 | /** 23 | * Customize user group mapping 24 | * if your resource owner structure differs from the default "Roles" workflow 25 | */ 26 | final class AuthenticationGetUserGroupsEvent 27 | { 28 | protected string $groupTable; 29 | protected array $groups; 30 | protected array $resource; 31 | protected AbstractAuthenticationService $authenticationService; 32 | 33 | /** 34 | * @param string $groupTable - fe_groups or be_groups 35 | * @param array $groups - known user group ids 36 | * @param array $resourceOwner - resource owner data 37 | */ 38 | public function __construct(string $groupTable, array $groups, array $resourceOwner, AbstractAuthenticationService $authenticationService) 39 | { 40 | $this->groupTable = $groupTable; 41 | $this->groups = $groups; 42 | $this->resource = $resourceOwner; 43 | $this->authenticationService = $authenticationService; 44 | } 45 | 46 | public function getGroupTable(): string 47 | { 48 | return $this->groupTable; 49 | } 50 | 51 | public function getUserGroups(): array 52 | { 53 | return $this->groups; 54 | } 55 | 56 | public function getResource(): array 57 | { 58 | return $this->resource; 59 | } 60 | 61 | public function getAuthenticationService(): AbstractAuthenticationService 62 | { 63 | return $this->authenticationService; 64 | } 65 | 66 | /** 67 | * Set your customized user group ids 68 | */ 69 | public function setUserGroups(array $groups): void 70 | { 71 | $this->groups = $groups; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Classes/Service/AuthenticationContextService.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc\Service; 6 | 7 | use Causal\Oidc\AuthenticationContext; 8 | use Psr\Http\Message\ServerRequestInterface; 9 | use Symfony\Component\HttpFoundation\Cookie; 10 | use TYPO3\CMS\Core\Http\NormalizedParams; 11 | 12 | class AuthenticationContextService 13 | { 14 | protected const SECURE_PREFIX = '__Secure-'; 15 | protected const COOKIE_NAME = 'oidc_context'; 16 | 17 | /** 18 | * Produce a cookie that stores the authentication context 19 | * while the client visits the identity provider. 20 | */ 21 | public function getCookieForAuthenticationContext(AuthenticationContext $authenticationContext): Cookie 22 | { 23 | return new Cookie( 24 | $this->getCookieName($authenticationContext->secureContext), 25 | $authenticationContext->toHashSignedJwt(), 26 | 0, 27 | '/', 28 | null, 29 | $authenticationContext->secureContext, 30 | true, 31 | false, 32 | Cookie::SAMESITE_LAX 33 | ); 34 | } 35 | 36 | /** 37 | * Find an oidc authentication context (cookie) in the given request 38 | */ 39 | public function resolveAuthenticationContext(ServerRequestInterface $request): ?AuthenticationContext 40 | { 41 | $isHttps = $this->isHttps($request); 42 | $cookie = $request->getCookieParams()[$this->getCookieName($isHttps)] ?? null; 43 | if (!isset($cookie)) { 44 | return null; 45 | } 46 | 47 | $authenticationContext = AuthenticationContext::fromJwt($cookie); 48 | 49 | if ($authenticationContext->secureContext && !$isHttps) { 50 | return null; 51 | } 52 | 53 | return $authenticationContext; 54 | } 55 | 56 | protected function isHttps(ServerRequestInterface $request): bool 57 | { 58 | $normalizedParams = $request->getAttribute('normalizedParams'); 59 | return $normalizedParams instanceof NormalizedParams && $normalizedParams->isHttps(); 60 | } 61 | 62 | protected function getCookieName(bool $secureContext): string 63 | { 64 | $cookiePrefix = $secureContext ? self::SECURE_PREFIX : ''; 65 | return $cookiePrefix . self::COOKIE_NAME; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Classes/Hooks/DataHandlerOidc.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Hooks; 19 | 20 | use TYPO3\CMS\Core\Context\Context; 21 | use TYPO3\CMS\Core\Database\ConnectionPool; 22 | use TYPO3\CMS\Core\Utility\GeneralUtility; 23 | 24 | /** 25 | * Hooks for \TYPO3\CMS\Core\DataHandling\DataHandler. 26 | */ 27 | class DataHandlerOidc 28 | { 29 | public function processDatamap_afterDatabaseOperations(string $operation, string $table, int|string $id, array $fieldArray): void 30 | { 31 | if ($table !== 'fe_groups' || $operation !== 'update') { 32 | return; 33 | } 34 | 35 | if (isset($fieldArray['tx_oidc_pattern']) && empty($fieldArray['tx_oidc_pattern'])) { 36 | // Pattern has been cleared => disconnect group from users (see https://github.com/xperseguers/t3ext-oidc/issues/11) 37 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 38 | ->getQueryBuilderForTable('fe_users'); 39 | $queryBuilder->getRestrictions()->removeAll(); 40 | $usersInThisUserGroup = $queryBuilder 41 | ->select('uid', 'usergroup') 42 | ->from('fe_users') 43 | ->where( 44 | $queryBuilder->expr()->inSet('usergroup', (string)$id) 45 | ) 46 | ->executeQuery(); 47 | 48 | $tableConnection = GeneralUtility::makeInstance(ConnectionPool::class) 49 | ->getConnectionForTable('fe_users'); 50 | while ($user = $usersInThisUserGroup->fetchAssociative()) { 51 | $userGroups = GeneralUtility::intExplode(',', $user['usergroup'], true); 52 | // Remove this user group from the list 53 | $index = array_search($id, $userGroups); 54 | unset($userGroups[$index]); 55 | 56 | $tableConnection->update( 57 | 'fe_users', 58 | [ 59 | 'usergroup' => implode(',', $userGroups), 60 | 'tstamp' => GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp'), 61 | ], 62 | [ 63 | 'uid' => $user['uid'], 64 | ] 65 | ); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "causal/oidc", 3 | "type": "typo3-cms-extension", 4 | "description": "This extension uses OpenID Connect to authenticate users.", 5 | "keywords": [ 6 | "TYPO3", 7 | "OpenID", 8 | "OIDC", 9 | "Authentication" 10 | ], 11 | "homepage": "https://github.com/xperseguers/t3ext-oidc", 12 | "support": { 13 | "issues": "https://github.com/xperseguers/t3ext-oidc/issues" 14 | }, 15 | "authors": [ 16 | { 17 | "name": "Xavier Perseguers", 18 | "email": "xavier@causal.ch", 19 | "homepage": "https://www.causal.ch", 20 | "role": "Developer" 21 | }, 22 | { 23 | "name": "Markus Klein", 24 | "homepage": "https://reelworx.at/", 25 | "role": "Developer" 26 | } 27 | ], 28 | "license": "GPL-2.0-or-later", 29 | "require": { 30 | "php": "^8.2", 31 | "ext-json": "*", 32 | "guzzlehttp/psr7": "^2.4", 33 | "typo3/cms-core": "^12.4.33 || ^13.4.14", 34 | "typo3/cms-frontend": "^12.4 || ^13.4", 35 | "symfony/http-foundation": "^7", 36 | "league/oauth2-client": "^2.8", 37 | "firebase/php-jwt": "^6.10" 38 | }, 39 | "require-dev": { 40 | "phpunit/phpunit": "^10", 41 | "typo3/testing-framework": "8.x-dev", 42 | "typo3/minimal": "^12 || ^13", 43 | "friendsofphp/php-cs-fixer": "^3.88" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "Causal\\Oidc\\": "Classes/" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "Causal\\Oidc\\Tests\\": "Tests/" 53 | } 54 | }, 55 | "scripts": { 56 | "extension-create-libs": [ 57 | "mkdir -p Libraries/temp", 58 | "[ -f $COMPOSER_HOME/vendor/bin/phar-composer ] || composer global require clue/phar-composer", 59 | "if [ ! -f Libraries/league-oauth2-client.phar ]; then cd Libraries/temp && composer require league/oauth2-client=^2.8 && composer require firebase/php-jwt=^6.10 && composer config classmap-authoritative true && composer config prepend-autoloader false && composer dump-autoload && cd ../../; fi", 60 | "[ -f Libraries/league-oauth2-client.phar ] || php -d phar.readonly=off $COMPOSER_HOME/vendor/bin/phar-composer build Libraries/temp/ Libraries/league-oauth2-client.phar", 61 | "chmod -x Libraries/*.phar", 62 | "rm -rf Libraries/temp" 63 | ], 64 | "extension-build": [ 65 | "@extension-create-libs" 66 | ], 67 | "extension-release": [ 68 | "@extension-build", 69 | "rm -rf Tests/", 70 | "rm .gitattributes", 71 | "rm .gitignore" 72 | ], 73 | "extension-clean": [ 74 | "rm -rf Libraries" 75 | ] 76 | }, 77 | "extra": { 78 | "branch-alias": { 79 | "dev-master": "5.x-dev" 80 | }, 81 | "typo3/cms": { 82 | "web-dir": ".Build/web", 83 | "extension-key": "oidc" 84 | } 85 | }, 86 | "config": { 87 | "vendor-dir": ".Build/vendor", 88 | "lock": false, 89 | "allow-plugins": { 90 | "typo3/cms-composer-installers": true, 91 | "typo3/class-alias-loader": true 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | // Return a Code Sniffing configuration using 6 | // all sniffers needed for PSR-2 7 | // and additionally: 8 | // - Remove leading slashes in use clauses. 9 | // - PHP single-line arrays should not have trailing comma. 10 | // - Single-line whitespace before closing semicolon are prohibited. 11 | // - Remove unused use statements in the PHP source code 12 | // - Ensure Concatenation to have at least one whitespace around 13 | // - Remove trailing whitespace at the end of blank lines. 14 | return (new \PhpCsFixer\Config()) 15 | ->setFinder( 16 | (new PhpCsFixer\Finder()) 17 | ->ignoreVCSIgnored(true) 18 | ->in(realpath(__DIR__)) 19 | ) 20 | ->setRiskyAllowed(true) 21 | ->setRules([ 22 | '@DoctrineAnnotation' => true, 23 | '@PER-CS3x0' => true, 24 | '@PHP8x2Migration' => true, 25 | 'cast_spaces' => ['space' => 'none'], 26 | 'declare_parentheses' => true, 27 | 'dir_constant' => true, 28 | 'modernize_types_casting' => true, 29 | 'native_function_casing' => true, 30 | 'no_alias_functions' => true, 31 | 'no_blank_lines_after_phpdoc' => true, 32 | 'no_empty_phpdoc' => true, 33 | 'no_empty_statement' => true, 34 | 'no_extra_blank_lines' => true, 35 | 'no_leading_namespace_whitespace' => true, 36 | 'no_null_property_initialization' => true, 37 | 'no_short_bool_cast' => true, 38 | 'no_singleline_whitespace_before_semicolons' => true, 39 | 'no_superfluous_elseif' => true, 40 | 'no_trailing_comma_in_singleline' => true, 41 | 'no_unneeded_control_parentheses' => true, 42 | 'no_unused_imports' => true, 43 | 'no_useless_else' => true, 44 | 'no_useless_nullsafe_operator' => true, 45 | 'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], 46 | 'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']], 47 | 'php_unit_mock_short_will_return' => true, 48 | 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], 49 | 'phpdoc_no_empty_return' => true, 50 | 'phpdoc_no_package' => true, 51 | 'phpdoc_scalar' => true, 52 | 'phpdoc_trim' => true, 53 | 'phpdoc_types' => true, 54 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 55 | 'single_line_comment_style' => ['comment_types' => ['hash']], 56 | 'single_quote' => true, 57 | 'single_space_around_construct' => true, 58 | 'type_declaration_spaces' => true, 59 | 'whitespace_after_comma_in_array' => ['ensure_single_space' => true], 60 | 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], 61 | ]); 62 | -------------------------------------------------------------------------------- /Classes/Frontend/FrontendSimulationV12.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc\Frontend; 6 | 7 | use InvalidArgumentException; 8 | use Psr\Http\Message\ServerRequestInterface; 9 | use TYPO3\CMS\Core\Context\Context; 10 | use TYPO3\CMS\Core\Routing\PageArguments; 11 | use TYPO3\CMS\Core\Routing\RouteNotFoundException; 12 | use TYPO3\CMS\Core\Routing\SiteMatcher; 13 | use TYPO3\CMS\Core\Routing\SiteRouteResult; 14 | use TYPO3\CMS\Core\Site\Entity\Site; 15 | use TYPO3\CMS\Core\Utility\GeneralUtility; 16 | use TYPO3\CMS\Core\Utility\RootlineUtility; 17 | use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; 18 | use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; 19 | 20 | class FrontendSimulationV12 implements FrontendSimulationInterface 21 | { 22 | public function getTSFE(ServerRequestInterface $originalRequest): TypoScriptFrontendController 23 | { 24 | $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); 25 | $routeResult = $siteMatcher->matchRequest($originalRequest); 26 | if ($routeResult instanceof SiteRouteResult) { 27 | $site = $routeResult->getSite(); 28 | if ($site instanceof Site) { 29 | try { 30 | $pageArguments = $site->getRouter()->matchRequest($originalRequest, $routeResult); 31 | if ($pageArguments instanceof PageArguments) { 32 | return GeneralUtility::makeInstance( 33 | TypoScriptFrontendController::class, 34 | GeneralUtility::makeInstance(Context::class), 35 | $site, 36 | $routeResult->getLanguage(), 37 | $pageArguments, 38 | GeneralUtility::makeInstance(FrontendUserAuthentication::class) 39 | ); 40 | } 41 | } catch (RouteNotFoundException) { 42 | } 43 | } 44 | } 45 | throw new InvalidArgumentException('Failed to initialize TSFE'); 46 | } 47 | 48 | public function getTypoScriptSetup(ServerRequestInterface $originalRequest, TypoScriptFrontendController $tsfe): array 49 | { 50 | /** @var RootlineUtility $rootlineUtility */ 51 | $rootlineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $tsfe->getPageArguments()->getPageId()); 52 | $tsfe->rootLine = $rootlineUtility->get(); 53 | $newRequest = $tsfe->getFromCache($originalRequest); 54 | $tsfe->releaseLocks(); 55 | return $newRequest->getAttribute('frontend.typoscript')->getSetupArray(); 56 | } 57 | 58 | public function cleanupTSFE(): void 59 | { 60 | /** @var Context $context */ 61 | $context = GeneralUtility::makeInstance(Context::class); 62 | $context->unsetAspect('typoscript'); 63 | $context->unsetAspect('frontend.preview'); 64 | unset($GLOBALS['TSFE']); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Classes/Middleware/OauthCallback.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc\Middleware; 6 | 7 | use Causal\Oidc\OidcConfiguration; 8 | use Causal\Oidc\Service\AuthenticationContextService; 9 | use Causal\Oidc\Service\OpenIdConnectService; 10 | use Psr\Http\Message\ResponseInterface; 11 | use Psr\Http\Message\ServerRequestInterface; 12 | use Psr\Http\Server\MiddlewareInterface; 13 | use Psr\Http\Server\RequestHandlerInterface; 14 | use Psr\Log\LoggerAwareInterface; 15 | use Psr\Log\LoggerAwareTrait; 16 | use TYPO3\CMS\Core\Http\RedirectResponse; 17 | use TYPO3\CMS\Core\Http\Response; 18 | use TYPO3\CMS\Core\Utility\GeneralUtility; 19 | 20 | class OauthCallback implements MiddlewareInterface, LoggerAwareInterface 21 | { 22 | use LoggerAwareTrait; 23 | 24 | public function __construct( 25 | protected OpenIdConnectService $openIdConnectService, 26 | protected AuthenticationContextService $authenticationContextService, 27 | protected OidcConfiguration $settings 28 | ) {} 29 | 30 | /** 31 | * @param ServerRequestInterface $request 32 | * @param RequestHandlerInterface $handler 33 | * @return ResponseInterface 34 | * see https://github.com/thephpleague/oauth2-client 35 | */ 36 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 37 | { 38 | if ($request->getMethod() !== 'GET') { 39 | return $handler->handle($request); 40 | } 41 | 42 | $queryParams = $request->getQueryParams(); 43 | $code = $queryParams['code'] ?? ''; 44 | if (!$code) { 45 | return $handler->handle($request); 46 | } 47 | 48 | // A code was supplied, we start the OIDC handling 49 | $authContext = $this->authenticationContextService->resolveAuthenticationContext($request); 50 | if (!$authContext) { 51 | return (new Response())->withStatus(400, 'Missing OIDC authentication context'); 52 | } 53 | 54 | $this->logger->debug('Authentication context is available', ['data' => $authContext]); 55 | 56 | $this->logger->debug('Initiating the silent authentication'); 57 | 58 | $state = $queryParams['state'] ?? ''; 59 | if (!$state) { 60 | return (new Response())->withStatus(400, 'Invalid state'); 61 | } 62 | if ($state !== $authContext->state) { 63 | if (!$this->settings->disableCSRFProtection) { 64 | $this->logger->error('Invalid returning state detected', [ 65 | 'expected' => $authContext->state, 66 | 'actual' => $state, 67 | ]); 68 | return (new Response())->withStatus(400, 'Invalid state'); 69 | } 70 | $this->logger->info('State mismatch. Bypassing CSRF attack mitigation protection according to the extension configuration', [ 71 | 'expected' => $authContext->state, 72 | 'actual' => $state, 73 | ]); 74 | } 75 | 76 | $loginUrl = $this->openIdConnectService->getFinalLoginUrl($authContext, $code); 77 | 78 | $this->logger->info('Redirecting to login URL', ['url' => (string)$loginUrl]); 79 | 80 | return new RedirectResponse(GeneralUtility::locationHeaderUrl((string)$loginUrl), 303); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Build/playwright/e2e/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "e2e", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@playwright/test": "1.56.1", 13 | "@types/node": "^22.15.3" 14 | } 15 | }, 16 | "node_modules/@playwright/test": { 17 | "version": "1.56.1", 18 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", 19 | "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", 20 | "dev": true, 21 | "license": "Apache-2.0", 22 | "dependencies": { 23 | "playwright": "1.56.1" 24 | }, 25 | "bin": { 26 | "playwright": "cli.js" 27 | }, 28 | "engines": { 29 | "node": ">=18" 30 | } 31 | }, 32 | "node_modules/@types/node": { 33 | "version": "22.15.3", 34 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", 35 | "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", 36 | "dev": true, 37 | "license": "MIT", 38 | "dependencies": { 39 | "undici-types": "~6.21.0" 40 | } 41 | }, 42 | "node_modules/fsevents": { 43 | "version": "2.3.2", 44 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 45 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 46 | "dev": true, 47 | "hasInstallScript": true, 48 | "license": "MIT", 49 | "optional": true, 50 | "os": [ 51 | "darwin" 52 | ], 53 | "engines": { 54 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 55 | } 56 | }, 57 | "node_modules/playwright": { 58 | "version": "1.56.1", 59 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", 60 | "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", 61 | "dev": true, 62 | "license": "Apache-2.0", 63 | "dependencies": { 64 | "playwright-core": "1.56.1" 65 | }, 66 | "bin": { 67 | "playwright": "cli.js" 68 | }, 69 | "engines": { 70 | "node": ">=18" 71 | }, 72 | "optionalDependencies": { 73 | "fsevents": "2.3.2" 74 | } 75 | }, 76 | "node_modules/playwright-core": { 77 | "version": "1.56.1", 78 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", 79 | "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", 80 | "dev": true, 81 | "license": "Apache-2.0", 82 | "bin": { 83 | "playwright-core": "cli.js" 84 | }, 85 | "engines": { 86 | "node": ">=18" 87 | } 88 | }, 89 | "node_modules/undici-types": { 90 | "version": "6.21.0", 91 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 92 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 93 | "dev": true, 94 | "license": "MIT" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Build/typo3/typo3/config/system/additional.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | (function () { 4 | // configure TYPO3 5 | $GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS'], [ 6 | 'DB' => [ 7 | 'Connections' => [ 8 | 'Default' => [ 9 | 'dbname' => getenv('TYPO3_DB_DBNAME'), 10 | 'host' => getenv('TYPO3_DB_HOST'), 11 | 'password' => getenv('TYPO3_DB_PASSWORD'), 12 | 'port' => getenv('TYPO3_DB_PORT'), 13 | 'user' => getenv('TYPO3_DB_USERNAME'), 14 | ], 15 | ], 16 | ], 17 | 'GFX' => [ 18 | 'processor_path' => getenv('TYPO3_GFX_PROCESSOR_PATH'), 19 | 'processor_path_lzw' => getenv('TYPO3_GFX_PROCESSOR_PATH_LZW'), 20 | ], 21 | ]); 22 | 23 | // SMTP mailserver 24 | if (getenv('TYPO3_SMTP_HOST')) { 25 | $port = getenv('TYPO3_SMTP_PORT') ? (int)getenv('TYPO3_SMTP_PORT') : 25; 26 | $server = sprintf('%s:%s', getenv('TYPO3_SMTP_HOST'), $port); 27 | 28 | $GLOBALS['TYPO3_CONF_VARS']['MAIL'] = array_replace($GLOBALS['TYPO3_CONF_VARS']['MAIL'], [ 29 | 'transport' => 'smtp', 30 | 'transport_smtp_server' => $server, 31 | ]); 32 | 33 | $properties = ['encrypt', 'username', 'password']; 34 | foreach ($properties as $property) { 35 | $envVariableName = 'TYPO3_SMTP_' . strtoupper($property); 36 | $configurationKey = 'transport_smtp_' . strtolower($property); 37 | 38 | $envValue = getenv($envVariableName); 39 | if ($envValue !== false) { 40 | $GLOBALS['TYPO3_CONF_VARS']['MAIL'][$configurationKey] = $envValue; 41 | } 42 | } 43 | } 44 | 45 | // install tool 46 | if (getenv('TYPO3_INSTALL_TOOL_PASSWORD')) { 47 | $GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'] = getenv('TYPO3_INSTALL_TOOL_PASSWORD'); 48 | } 49 | 50 | // Development configuration 51 | if (\TYPO3\CMS\Core\Core\Environment::getContext()->isDevelopment()) { 52 | foreach (glob(TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/../configuration/*.php') as $configurationFile) { 53 | require_once($configurationFile); 54 | } 55 | } 56 | 57 | $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] = ''; 58 | 59 | $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] = array_replace_recursive($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['oidc'] ?? [], [ 60 | 'enableFrontendAuthentication' => getenv('TYPO3_OIDC_ENABLE_FRONTEND_AUTHENTICATION'), 61 | 'oidcClientKey' => getenv('TYPO3_OIDC_OIDC_CLIENT_KEY'), 62 | 'oidcClientScopes' => getenv('TYPO3_OIDC_OIDC_CLIENT_SCOPES'), 63 | 'oidcClientSecret' => getenv('TYPO3_OIDC_OIDC_CLIENT_SECRET'), 64 | 'oidcEndpointAuthorize' => getenv('TYPO3_OIDC_OIDC_ENDPOINT_AUTHORIZE'), 65 | 'oidcEndpointLogout' => getenv('TYPO3_OIDC_OIDC_ENDPOINT_LOGOUT'), 66 | 'oidcEndpointRevoke' => getenv('TYPO3_OIDC_OIDC_ENDPOINTREVOKE'), 67 | 'oidcEndpointToken' => getenv('TYPO3_OIDC_OIDC_ENDPOINT_TOKEN'), 68 | 'oidcEndpointUserInfo' => getenv('TYPO3_OIDC_OIDC_ENDPOINT_USERINFO'), 69 | 'oidcRedirectUri' => getenv('TYPO3_OIDC_OIDC_REDIRECT_URI'), 70 | 'enableCodeVerifier' => getenv('TYPO3_OIDC_ENABLE_CODE_VERIFIER'), 71 | ]); 72 | })(); 73 | -------------------------------------------------------------------------------- /Build/typo3/typo3/packages/oidc-sitepackage/Resources/Private/Felogin/Templates/Login/Login.html: -------------------------------------------------------------------------------- 1 | <html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:oidc="http://typo3.org/ns/Causal/Oidc/ViewHelpers" data-namespace-typo3-fluid="true"> 2 | 3 | <f:flashMessages/> 4 | 5 | <f:if condition="{messageKey}"> 6 | <h3> 7 | <f:render partial="RenderLabelOrMessage" arguments="{key: '{messageKey}_header'}"/> 8 | </h3> 9 | <p> 10 | <f:render partial="RenderLabelOrMessage" arguments="{key: '{messageKey}_message'}"/> 11 | </p> 12 | </f:if> 13 | 14 | <f:form target="_top" fieldNamePrefix="" action="login" requestToken="{requestToken}" spellcheck="false"> 15 | <f:render section="content" arguments="{_all}"/> 16 | </f:form> 17 | 18 | <f:if condition="{settings.showForgotPassword}"> 19 | <f:link.action action="recovery" controller="PasswordRecovery"> 20 | <f:render partial="RenderLabelOrMessage" arguments="{key: 'forgot_header'}"/> 21 | </f:link.action> 22 | </f:if> 23 | 24 | <f:section name="content"> 25 | <fieldset> 26 | <legend> 27 | <f:translate key="login"/> 28 | </legend> 29 | <div> 30 | <label for="tx-felogin-input-username"> 31 | <f:translate key="username"/> 32 | </label> 33 | <f:form.textfield name="user" required="true" autocomplete="username" id="tx-felogin-input-username"/> 34 | </div> 35 | <div> 36 | <label for="tx-felogin-input-password"> 37 | <f:translate key="password"/> 38 | </label> 39 | <f:form.password name="pass" required="required" autocomplete="current-password" id="tx-felogin-input-password"/> 40 | </div> 41 | 42 | <f:if condition="{permaloginStatus} > -1"> 43 | <div> 44 | <label for="permalogin"> 45 | <f:translate id="permalogin"/> 46 | </label> 47 | <f:if condition="{permaloginStatus} == 1"> 48 | <f:then> 49 | <f:form.hidden name="permalogin" value="0" disabled="disabled"/> 50 | <f:form.checkbox name="permalogin" id="permalogin" value="1" checked="checked"/> 51 | </f:then> 52 | <f:else> 53 | <f:form.hidden name="permalogin" value="0"/> 54 | <f:form.checkbox name="permalogin" id="permalogin" value="1"/> 55 | </f:else> 56 | </f:if> 57 | </div> 58 | </f:if> 59 | 60 | <div> 61 | <f:form.submit value="{f:translate(key: 'login')}" name="submit"/> 62 | </div> 63 | 64 | 65 | 66 | <div class="felogin-hidden"> 67 | <f:form.hidden name="logintype" value="login"/> 68 | <f:if condition="{redirectURL}!=''"> 69 | <f:form.hidden name="redirect_url" value="{redirectURL}" /> 70 | </f:if> 71 | <f:if condition="{referer}!=''"> 72 | <f:form.hidden name="referer" value="{referer}" /> 73 | </f:if> 74 | <f:if condition="{redirectReferrer}!=''"> 75 | <f:form.hidden name="redirectReferrer" value="off" /> 76 | </f:if> 77 | <f:if condition="{noRedirect}!=''"> 78 | <f:form.hidden name="noredirect" value="1" /> 79 | </f:if> 80 | </div> 81 | </fieldset> 82 | 83 | <div> 84 | <a href="{oidc:oidcLink()}">Login with OpenID Connect</a> 85 | </div> 86 | </f:section> 87 | </html> 88 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # OpenID Connect integration for TYPO3 - changelog 2 | 3 | ## Version 5.x.x 4 | 5 | - Breaking: Dropped support for TYPO3 11 LTS and PHP < 8.2 6 | - Breaking: Introduced OidcConfiguration class to represent extension configuration. 7 | Default values of endpoints have been removed. Please validate your configuration during upgrade. 8 | - Breaking: Dropped direct felogin-integration (via event). 9 | Please use the `OidcLinkViewHelper` instead, if you previously relied on the `{openidConnectUri}` variable in your template. 10 | - Breaking: Move OIDC Login Plugin from "list_type" to real content type. 11 | - Breaking: The default scope separator is changed from comma (`,`) to the space-character (` `) 12 | to follow official [RFC-6749](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). 13 | Change extension configuration `oidcClientScopeSeparator = ,` for old behaviour. 14 | - Feature: Added extension setting `enablePasswordCredentials` to disable password-authentication. 15 | - Method `getFreshAccessToken()` now actually returns the fresh access token. 16 | 17 | 18 | ## Version 4.0.0 19 | 20 | - Breaking: Existing fe_users are not looked up by their username anymore. 21 | You may use the `AuthenticationFetchUserEvent` to re-add this functionality, 22 | if this is secure for your use case. 23 | See commit `[!!!][SECURITY] Do not look up existing users via username field` for details. 24 | - Breaking: Upon login the user's username and email address will now be updated 25 | according to the mapping configuration. The default mapping configuration maps 26 | the username, but not the email address. Custom mapping configurations can now 27 | map none, one or both of those fields. 28 | It is now possible to post-process the mapping by ìmplementing the `AuthenticationProcessMappingEvent` 29 | - The query parameters for the authorization URL can now be modified via `GetAuthorizationUrlEvent`. 30 | 31 | ## Version 3.0.0 32 | 33 | - The callback URL changed from `/typo3conf/ext/oidc/Public/callback.php` to `TYPO3_SITE_URL`. (configurable with option `oidcRedirectUri`) [#116](https://github.com/xperseguers/t3ext-oidc/issues/116) 34 | - No PHP native session is needed anymore. A JWT-Cookie (named `oidc_context`) is now used to store relevant information during an authentication process. [#155](https://github.com/xperseguers/t3ext-oidc/issues/155) 35 | - A dedicated route is used to initiate the authorization flow with the identity provider. (configurable with option `authenticationUrlRoute`) 36 | This avoids creating loads of authentication sessions with the identity provider (IdP), if the Login-button 37 | is placed on a Login-page for instance. Formerly a new auth-session was started with the IdP 38 | every time the page was rendered. [#159](https://github.com/xperseguers/t3ext-oidc/issues/159) 39 | - All previous hooks have been replaced with PSR-14 events. More events were added. 40 | - The extension is now wiring the underlying OAuth2 library with TYPO3's Guzzle wrapper (`GuzzleClientFactory`). 41 | This means that requests done by the library now adhere to TYPO3 configuration. [#167](https://github.com/xperseguers/t3ext-oidc/issues/167) 42 | - Added an event allowing to adjust the where-conditions for fetching the existing fe_users [#164](https://github.com/xperseguers/t3ext-oidc/issues/164) 43 | - Enhanced events to include a reference to the AuthenticationService [#136](https://github.com/xperseguers/t3ext-oidc/issues/136) 44 | - Added a user groups event to map groups by a different pattern than "Roles", e.g. "claims" [#129](https://github.com/xperseguers/t3ext-oidc/pull/129) 45 | -------------------------------------------------------------------------------- /Build/typo3/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.7-labs 2 | FROM php:8.3-apache AS webserver 3 | 4 | # Install common tools 5 | # gettext-base provides envsubst, used for fixture import to db 6 | RUN apt-get update \ 7 | && apt-get install -y \ 8 | wget \ 9 | rsync \ 10 | unzip \ 11 | gettext-base \ 12 | curl \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # Install mysql client 16 | RUN apt-get update \ 17 | && apt-get install -y \ 18 | mariadb-client \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | # Install imagemagick 22 | RUN apt-get update \ 23 | && apt-get install -y \ 24 | imagemagick \ 25 | && rm -rf /var/lib/apt/lists/* 26 | ENV TYPO3_GFX_PROCESSOR_PATH=/usr/bin/ 27 | ENV TYPO3_GFX_PROCESSOR_PATH_LZW=/usr/bin/ 28 | 29 | # Install apcu caching 30 | RUN pecl install apcu \ 31 | && docker-php-ext-enable apcu 32 | 33 | # Install PHP extensions 34 | RUN apt-get update \ 35 | && apt-get install -y \ 36 | libxml2-dev libfreetype6-dev \ 37 | libjpeg62-turbo-dev \ 38 | libmcrypt-dev \ 39 | libpng-dev \ 40 | libzip-dev \ 41 | python3 \ 42 | python3-setuptools \ 43 | libcurl4-openssl-dev \ 44 | && rm -rf /var/lib/apt/lists/* 45 | RUN docker-php-ext-install -j$(nproc) \ 46 | exif \ 47 | mysqli \ 48 | soap \ 49 | curl \ 50 | zip 51 | 52 | # Install php extension intl 53 | RUN apt-get update \ 54 | && apt-get install -y \ 55 | libicu-dev \ 56 | && rm -rf /var/lib/apt/lists/* \ 57 | && docker-php-ext-install intl 58 | 59 | # Install php redis client 60 | RUN pecl install -o -f redis \ 61 | && rm -rf /tmp/pear \ 62 | && docker-php-ext-enable redis 63 | 64 | # PHP gd 65 | RUN docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ 66 | RUN docker-php-ext-install -j$(nproc) gd 67 | 68 | # Link php binary where TYPO3 expects it 69 | RUN ln -s /usr/local/bin/php /usr/bin/php 70 | 71 | # Configure PHP 72 | ADD docker/typo3.ini /usr/local/etc/php/conf.d/typo3.ini 73 | 74 | # Install xdebug 75 | RUN pecl install xdebug \ 76 | && docker-php-ext-enable xdebug 77 | 78 | # Setup locales 79 | RUN apt-get update \ 80 | && apt-get install -y \ 81 | locales \ 82 | && rm -rf /var/lib/apt/lists/* \ 83 | && echo "# Docker locales" > /etc/locale.gen \ 84 | && echo "en_GB.UTF-8 UTF-8" >> /etc/locale.gen \ 85 | && echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen \ 86 | && echo "de_DE.UTF-8 UTF-8" >> /etc/locale.gen \ 87 | && locale-gen 88 | 89 | # Configure Apache as ssl server 90 | RUN a2enmod rewrite 91 | RUN a2enmod ssl 92 | COPY --from=certs /developer.pem /etc/ssl/certs/developer.pem 93 | COPY --from=certs /developer.key /etc/ssl/private/developer.key 94 | RUN ln -s /etc/ssl/certs/developer.pem /etc/ssl/certs/`openssl x509 -noout -hash -in /etc/ssl/certs/developer.pem`.0 95 | ADD docker/000-default.conf /etc/apache2/sites-available/000-default.conf 96 | ADD docker/000-default-ssl.conf /etc/apache2/sites-available/000-default-ssl.conf 97 | RUN a2ensite 000-default-ssl.conf 98 | RUN rm /etc/apache2/sites-available/default-ssl.conf 99 | 100 | COPY --chmod=744 docker/entrypoint.sh /entrypoint.sh 101 | CMD ["/entrypoint.sh"] 102 | 103 | WORKDIR /app 104 | EXPOSE 80 105 | EXPOSE 443 106 | 107 | 108 | 109 | FROM composer:2.7.2 AS php-composer 110 | 111 | 112 | 113 | FROM php-composer AS php-dependencies 114 | COPY --from=typo3-version composer.* /app/ 115 | COPY --parents ./typo3/packages/./*/composer.json /app/packages 116 | COPY --from=oidc composer.json /app/packages/oidc/composer.json 117 | 118 | RUN composer install --no-dev 119 | 120 | 121 | 122 | FROM webserver AS oidc-webserver 123 | COPY --from=php-dependencies /app /app 124 | RUN install -d -o www-data -g www-data -m 775 -v /app/var 125 | RUN cp vendor/typo3/cms-install/Resources/Private/FolderStructureTemplateFiles/root-htaccess public/.htaccess 126 | 127 | COPY ./typo3 /app/ 128 | COPY --from=oidc / /app/packages/oidc 129 | -------------------------------------------------------------------------------- /ext_conf_template.txt: -------------------------------------------------------------------------------- 1 | # cat=basic/enable/1; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.enableFrontendAuthentication 2 | enableFrontendAuthentication = 0 3 | 4 | # cat=basic/enable/2; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.reEnableFrontendUsers 5 | reEnableFrontendUsers = 0 6 | 7 | # cat=basic/enable/3; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.undeleteFrontendUsers 8 | undeleteFrontendUsers = 0 9 | 10 | # cat=basic/enable/4; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.frontendUserMustExistLocally 11 | frontendUserMustExistLocally = 0 12 | 13 | # cat=basic/enable/5; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.enableCodeVerifier 14 | enableCodeVerifier = 0 15 | 16 | # cat=basic/enable/6; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.enablePasswordCredentials 17 | enablePasswordCredentials = 0 18 | 19 | # cat=basic//1; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.usersStoragePid 20 | usersStoragePid = 0 21 | 22 | # cat=basic//2; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.usersDefaultGroup 23 | usersDefaultGroup = 24 | 25 | # cat=basic//2a; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcRedirectUri 26 | oidcRedirectUri = 27 | 28 | # cat=basic//3; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientKey 29 | oidcClientKey = 30 | 31 | # cat=basic//4; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientSecret 32 | oidcClientSecret = 33 | 34 | # cat=basic//5; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientScopes 35 | oidcClientScopes = openid 36 | 37 | # cat=basic//6; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcClientScopeSeparator 38 | oidcClientScopeSeparator = 39 | 40 | # cat=advanced/links/1; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcEndpointAuthorize 41 | oidcEndpointAuthorize = 42 | 43 | # cat=advanced/links/2; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcEndpointToken 44 | oidcEndpointToken = 45 | 46 | # cat=advanced/links/3; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcEndpointUserInfo 47 | oidcEndpointUserInfo = 48 | 49 | # cat=advanced/links/4; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcEndpointLogout 50 | oidcEndpointLogout = 51 | 52 | # cat=advanced/links/5; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcEndpointRevoke 53 | oidcEndpointRevoke = 54 | 55 | # cat=advanced/links/6; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcAuthorizeLanguageParameter 56 | oidcAuthorizeLanguageParameter = language 57 | 58 | # cat=advanced/enable/1; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcUseRequestPathAuthentication 59 | oidcUseRequestPathAuthentication = 0 60 | 61 | # cat=advanced/enable/2; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.revokeAccessTokenAfterLogin 62 | oidcRevokeAccessTokenAfterLogin = 0 63 | 64 | # cat=advanced/enable/3; type=boolean; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oidcDisableCSRFProtection 65 | oidcDisableCSRFProtection = 0 66 | 67 | # cat=advanced//3; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.oauthProviderFactory 68 | oauthProviderFactory = 69 | 70 | # cat=advanced//4; type=int+; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.authenticationServicePriority 71 | authenticationServicePriority = 82 72 | 73 | # cat=advanced//5; type=int+; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.authenticationServiceQuality 74 | authenticationServiceQuality = 80 75 | 76 | # cat=advanced//6; type=string; label=LLL:EXT:oidc/Resources/Private/Language/locallang_db.xlf:settings.authenticationUrlRoute 77 | authenticationUrlRoute = oidc/authentication 78 | -------------------------------------------------------------------------------- /Classes/OidcConfiguration.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc; 6 | 7 | use Causal\Oidc\Factory\GenericOAuthProviderFactory; 8 | use Causal\Oidc\Factory\OAuthProviderFactoryInterface; 9 | use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; 10 | use TYPO3\CMS\Core\Utility\GeneralUtility; 11 | 12 | final class OidcConfiguration 13 | { 14 | public bool $enableFrontendAuthentication = false; 15 | public int $authenticationServicePriority = 82; 16 | public int $authenticationServiceQuality = 80; 17 | /** @var int[] */ 18 | public array $usersStoragePids = [0]; 19 | public string $usersDefaultGroup = ''; 20 | public bool $reEnableFrontendUsers = false; 21 | public bool $undeleteFrontendUsers = false; 22 | public bool $frontendUserMustExistLocally = false; 23 | public bool $disableCSRFProtection = false; 24 | public bool $enableCodeVerifier = false; 25 | public string $authenticationUrlRoute = 'oidc/authentication'; 26 | public string $authorizeLanguageParameter = 'language'; 27 | public bool $useRequestPathAuthentication = false; 28 | /** @var class-string<OAuthProviderFactoryInterface> */ 29 | public string $oauthProviderFactory = ''; 30 | public string $oidcClientKey = ''; 31 | public string $oidcClientSecret = ''; 32 | public string $oidcClientScopes = 'openid'; 33 | public string $oidcClientScopeSeparator = ' '; 34 | public string $oidcRedirectUri = ''; 35 | public string $endpointAuthorize = ''; 36 | public string $endpointToken = ''; 37 | public string $endpointUserInfo = ''; 38 | public string $endpointRevoke = ''; 39 | public string $endpointLogout = ''; 40 | public bool $revokeAccessTokenAfterLogin = false; 41 | public bool $enablePasswordCredentials = false; 42 | 43 | public function __construct(array $extConfig = []) 44 | { 45 | $extConfig = $extConfig ?: $this->getExtensionConfiguration(); 46 | 47 | $this->enableFrontendAuthentication = (bool)$extConfig['enableFrontendAuthentication']; 48 | $this->authenticationServicePriority = (int)$extConfig['authenticationServicePriority']; 49 | $this->authenticationServiceQuality = (int)$extConfig['authenticationServiceQuality']; 50 | $this->reEnableFrontendUsers = (bool)$extConfig['reEnableFrontendUsers']; 51 | $this->undeleteFrontendUsers = (bool)$extConfig['undeleteFrontendUsers']; 52 | $this->frontendUserMustExistLocally = (bool)$extConfig['frontendUserMustExistLocally']; 53 | $this->disableCSRFProtection = (bool)$extConfig['oidcDisableCSRFProtection']; 54 | $this->enableCodeVerifier = (bool)$extConfig['enableCodeVerifier']; 55 | $this->authenticationUrlRoute = $extConfig['authenticationUrlRoute']; 56 | $this->authorizeLanguageParameter = $extConfig['oidcAuthorizeLanguageParameter']; 57 | $this->useRequestPathAuthentication = (bool)$extConfig['oidcUseRequestPathAuthentication']; 58 | $this->oauthProviderFactory = $extConfig['oauthProviderFactory'] ?: GenericOAuthProviderFactory::class; 59 | $this->oidcClientKey = $extConfig['oidcClientKey']; 60 | $this->oidcClientSecret = $extConfig['oidcClientSecret']; 61 | $this->oidcClientScopes = $extConfig['oidcClientScopes']; 62 | $this->oidcClientScopeSeparator = $extConfig['oidcClientScopeSeparator'] === '' ? ' ' : $extConfig['oidcClientScopeSeparator']; 63 | $this->endpointAuthorize = $extConfig['oidcEndpointAuthorize']; 64 | $this->endpointToken = $extConfig['oidcEndpointToken']; 65 | $this->endpointUserInfo = $extConfig['oidcEndpointUserInfo']; 66 | $this->endpointRevoke = $extConfig['oidcEndpointRevoke']; 67 | $this->endpointLogout = $extConfig['oidcEndpointLogout']; 68 | $this->usersStoragePids = GeneralUtility::intExplode(',', (string)$extConfig['usersStoragePid'], true) ?: [0]; 69 | $this->usersDefaultGroup = $extConfig['usersDefaultGroup']; 70 | $this->oidcRedirectUri = $extConfig['oidcRedirectUri']; 71 | $this->revokeAccessTokenAfterLogin = (bool)$extConfig['oidcRevokeAccessTokenAfterLogin']; 72 | $this->enablePasswordCredentials = (bool)$extConfig['enablePasswordCredentials']; 73 | } 74 | 75 | protected function getExtensionConfiguration(): array 76 | { 77 | $config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('oidc'); 78 | if ($config) { 79 | return $config; 80 | } 81 | throw new \UnexpectedValueException('OIDC extension configuration not found', 1763986824); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Classes/Controller/LoginController.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Controller; 19 | 20 | use Causal\Oidc\Service\OpenIdConnectService; 21 | use GuzzleHttp\Psr7\Uri; 22 | use Psr\Http\Message\ServerRequestInterface; 23 | use TYPO3\CMS\Core\Context\Context; 24 | use TYPO3\CMS\Core\Http\PropagateResponseException; 25 | use TYPO3\CMS\Core\Http\RedirectResponse; 26 | use TYPO3\CMS\Core\Http\ServerRequest; 27 | use TYPO3\CMS\Core\Utility\GeneralUtility; 28 | use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; 29 | 30 | class LoginController 31 | { 32 | /** 33 | * TypoScript configuration of this plugin 34 | */ 35 | protected array $pluginConfiguration = []; 36 | 37 | /** 38 | * will automatically be injected, if this controller is called as a plugin 39 | */ 40 | public ?ContentObjectRenderer $cObj = null; 41 | 42 | protected ServerRequest $request; 43 | 44 | public function __construct() 45 | { 46 | $this->request = $GLOBALS['TYPO3_REQUEST']; 47 | } 48 | 49 | public function setContentObjectRenderer(ContentObjectRenderer $cObj): void 50 | { 51 | $this->cObj = $cObj; 52 | } 53 | 54 | /** 55 | * Main entry point for the OIDC plugin. 56 | * 57 | * If the user is not logged in, redirect to the authorization server to start the oidc process 58 | * 59 | * If the user has just been logged in and just came back from the authorization server, redirect the user to the 60 | * final redirect URL. 61 | * 62 | * @param string $_ ignored 63 | * @param array|null $pluginConfiguration 64 | * @throws PropagateResponseException 65 | */ 66 | public function login(string $_, ?array $pluginConfiguration): void 67 | { 68 | if (is_array($pluginConfiguration)) { 69 | $this->pluginConfiguration = $pluginConfiguration; 70 | } 71 | 72 | /** @var Context $context */ 73 | $context = GeneralUtility::makeInstance(Context::class); 74 | $loginType = $this->request->getParsedBody()['logintype'] ?? $this->request->getQueryParams()['logintype'] ?? ''; 75 | if ($loginType === 'login' || $context->getAspect('frontend.user')->isLoggedIn()) { 76 | $redirectUrl = $this->determineRedirectUrl(); 77 | $this->redirect($redirectUrl); 78 | } 79 | 80 | $authorizationRedirect = $this->getAuthorizationRedirect($this->request, $pluginConfiguration['authorizationUrlOptions.'] ?? []); 81 | throw new PropagateResponseException($authorizationRedirect); 82 | } 83 | 84 | protected function getAuthorizationRedirect(ServerRequestInterface $request, array $authorizationUrlOptions): RedirectResponse 85 | { 86 | $oidcService = GeneralUtility::makeInstance(OpenIdConnectService::class); 87 | $authContext = $oidcService->buildAuthenticationContext( 88 | $this->request, 89 | $authorizationUrlOptions, 90 | Uri::withQueryValue($request->getUri(), 'logintype', 'login')->__toString(), 91 | ); 92 | return $oidcService->getAuthorizationRedirect($authContext); 93 | } 94 | 95 | protected function determineRedirectUrl() 96 | { 97 | $redirectUrl = $this->request->getParsedBody()['redirect_url'] ?? $this->request->getQueryParams()['redirect_url'] ?? ''; 98 | if (!empty($redirectUrl)) { 99 | return $redirectUrl; 100 | } 101 | 102 | if (isset($this->pluginConfiguration['defaultRedirectPid'])) { 103 | $defaultRedirectPid = (int)$this->pluginConfiguration['defaultRedirectPid']; 104 | if ($defaultRedirectPid > 0) { 105 | return $this->cObj->typoLink_URL(['parameter' => $defaultRedirectPid]); 106 | } 107 | } 108 | 109 | return '/'; 110 | } 111 | 112 | /** 113 | * @throws PropagateResponseException 114 | */ 115 | protected function redirect(string $redirectUrl): void 116 | { 117 | throw new PropagateResponseException(new RedirectResponse($redirectUrl)); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Build/typo3/typo3/packages/oidc-sitepackage/ext_tables_static+adt.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE pages; 2 | TRUNCATE tt_content; 3 | TRUNCATE sys_template; 4 | 5 | # Needed for TYPO3 v12 6 | INSERT INTO `sys_template` (`uid`, `pid`, `title`, `root`) 7 | VALUES 8 | ('1', '1', 'oidc-sitepackage', '1'); 9 | ; 10 | 11 | INSERT INTO pages SET 12 | uid = 1, 13 | pid = 0, 14 | title = "oidc", 15 | is_siteroot = 1, 16 | slug = "/", 17 | doktype = 1 18 | ; 19 | 20 | INSERT INTO pages 21 | SET 22 | uid = 2, 23 | pid = 1, 24 | title = "Frontend Users", 25 | slug = "/frontend-users/", 26 | doktype = 254, 27 | module = "fe_users", 28 | sorting = 16 29 | ; 30 | 31 | INSERT INTO `tt_content` (`uid`, `pid`, `CType`, `header`, `pi_flexform`) 32 | VALUES 33 | ('1', '1', 'felogin_login', 'Login', '<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<T3FlexForms>\n <data>\n <sheet index=\"sDEF\">\n <language index=\"lDEF\">\n <field index=\"settings.showForgotPassword\">\n <value index=\"vDEF\">0</value>\n </field>\n <field index=\"settings.showPermaLogin\">\n <value index=\"vDEF\">1</value>\n </field>\n <field index=\"settings.showLogoutFormAfterLogin\">\n <value index=\"vDEF\">0</value>\n </field>\n <field index=\"settings.pages\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.recursive\">\n <value index=\"vDEF\"></value>\n </field>\n </language>\n </sheet>\n <sheet index=\"s_redirect\">\n <language index=\"lDEF\">\n <field index=\"settings.redirectMode\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.redirectFirstMethod\">\n <value index=\"vDEF\">0</value>\n </field>\n <field index=\"settings.redirectPageLogin\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.redirectPageLoginError\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.redirectPageLogout\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.redirectDisable\">\n <value index=\"vDEF\">0</value>\n </field>\n </language>\n </sheet>\n <sheet index=\"s_messages\">\n <language index=\"lDEF\">\n <field index=\"settings.welcome_header\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.welcome_message\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.success_header\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.success_message\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.error_header\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.error_message\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.status_header\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.status_message\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.logout_header\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.logout_message\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.forgot_header\">\n <value index=\"vDEF\"></value>\n </field>\n <field index=\"settings.forgot_reset_message\">\n <value index=\"vDEF\"></value>\n </field>\n </language>\n </sheet>\n </data>\n</T3FlexForms>'); 34 | ; 35 | 36 | INSERT INTO pages SET 37 | uid = 3, 38 | pid = 1, 39 | title = "Login", 40 | slug = "/login/", 41 | doktype = 1, 42 | sorting = 1 43 | ; 44 | 45 | INSERT INTO `tt_content` (`uid`, `pid`, `CType`, `header`) 46 | VALUES 47 | ('2', '3', 'oidc_login', 'Login'); 48 | ; 49 | 50 | INSERT INTO pages SET 51 | uid = 4, 52 | pid = 1, 53 | title = "Login Redirect Target", 54 | slug = "/login-redirect-target/", 55 | doktype = 1, 56 | sorting = 8 57 | ; 58 | -------------------------------------------------------------------------------- /Build/typo3/typo3/config/system/settings.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | return [ 4 | 'BE' => [ 5 | 'debug' => true, 6 | 'passwordHashing' => [ 7 | 'className' => 'TYPO3\\CMS\\Core\\Crypto\\PasswordHashing\\Argon2iPasswordHash', 8 | 'options' => [], 9 | ], 10 | ], 11 | 'DB' => [ 12 | 'Connections' => [ 13 | 'Default' => [ 14 | 'charset' => 'utf8', 15 | 'driver' => 'mysqli', 16 | ], 17 | ], 18 | ], 19 | 'EXTENSIONS' => [ 20 | 'backend' => [ 21 | 'backendFavicon' => '', 22 | 'backendLogo' => '', 23 | 'loginBackgroundImage' => '', 24 | 'loginFootnote' => '', 25 | 'loginHighlightColor' => '', 26 | 'loginLogo' => '', 27 | 'loginLogoAlt' => '', 28 | ], 29 | 'extensionmanager' => [ 30 | 'automaticInstallation' => '1', 31 | 'offlineMode' => '0', 32 | ], 33 | 'oidc' => [ 34 | 'authenticationServicePriority' => '82', 35 | 'authenticationServiceQuality' => '80', 36 | 'authenticationUrlRoute' => 'oidc/authentication', 37 | 'enableBackendAuthentication' => '1', 38 | 'enableCodeVerifier' => '1', 39 | 'enableFrontendAuthentication' => '1', 40 | 'enablePasswordCredentials' => '0', 41 | 'frontendUserMustExistLocally' => '0', 42 | 'oauthProviderFactory' => '', 43 | 'oidcAuthorizeLanguageParameter' => 'language', 44 | 'oidcClientKey' => 't3ext-oidc', 45 | 'oidcClientScopes' => 'openid', 46 | 'oidcClientSecret' => 't3ext-oidc', 47 | 'oidcDisableCSRFProtection' => '0', 48 | 'oidcEndpointAuthorize' => 'http://oidc.t3ext-oidc.test/connect/authorize', 49 | 'oidcEndpointLogout' => '', 50 | 'oidcEndpointRevoke' => 'http://oidc.t3ext-oidc.test/connect/revocation', 51 | 'oidcEndpointToken' => 'http://oidc.t3ext-oidc.test/connect/token', 52 | 'oidcEndpointUserInfo' => 'http://oidc.t3ext-oidc.test/connect/userinfo', 53 | 'oidcRedirectUri' => 'https://v13.t3ext-oidc.test/login/redirect', 54 | 'oidcRevokeAccessTokenAfterLogin' => '0', 55 | 'oidcUseRequestPathAuthentication' => '0', 56 | 'reEnableFrontendUsers' => '0', 57 | 'undeleteFrontendUsers' => '0', 58 | 'usersDefaultGroup' => '', 59 | 'usersStoragePid' => '2', 60 | ], 61 | ], 62 | 'FE' => [ 63 | 'cacheHash' => [ 64 | 'enforceValidation' => true, 65 | ], 66 | 'debug' => true, 67 | 'disableNoCacheParameter' => true, 68 | 'passwordHashing' => [ 69 | 'className' => 'TYPO3\\CMS\\Core\\Crypto\\PasswordHashing\\Argon2iPasswordHash', 70 | 'options' => [], 71 | ], 72 | ], 73 | 'GFX' => [ 74 | 'processor' => 'ImageMagick', 75 | 'processor_effects' => true, 76 | 'processor_enabled' => true, 77 | 'processor_path' => '/usr/bin/', 78 | ], 79 | 'LOG' => [ 80 | 'TYPO3' => [ 81 | 'CMS' => [ 82 | 'deprecations' => [ 83 | 'writerConfiguration' => [ 84 | 'notice' => [ 85 | 'TYPO3\CMS\Core\Log\Writer\FileWriter' => [ 86 | 'disabled' => false, 87 | ], 88 | ], 89 | ], 90 | ], 91 | ], 92 | ], 93 | ], 94 | 'MAIL' => [ 95 | 'transport' => 'sendmail', 96 | 'transport_sendmail_command' => '/usr/sbin/sendmail -t -i', 97 | 'transport_smtp_encrypt' => '', 98 | 'transport_smtp_password' => '', 99 | 'transport_smtp_server' => '', 100 | 'transport_smtp_username' => '', 101 | ], 102 | 'SYS' => [ 103 | 'UTF8filesystem' => true, 104 | 'caching' => [ 105 | 'cacheConfigurations' => [ 106 | 'hash' => [ 107 | 'backend' => 'TYPO3\\CMS\\Core\\Cache\\Backend\\Typo3DatabaseBackend', 108 | ], 109 | 'pages' => [ 110 | 'backend' => 'TYPO3\\CMS\\Core\\Cache\\Backend\\Typo3DatabaseBackend', 111 | 'options' => [ 112 | 'compression' => true, 113 | ], 114 | ], 115 | 'rootline' => [ 116 | 'backend' => 'TYPO3\\CMS\\Core\\Cache\\Backend\\Typo3DatabaseBackend', 117 | 'options' => [ 118 | 'compression' => true, 119 | ], 120 | ], 121 | ], 122 | ], 123 | 'devIPmask' => '*', 124 | 'displayErrors' => 1, 125 | 'encryptionKey' => 'dff1b14d5aa12e8f6c840e205a6484f7dd0bfb0f67eea73f114543d284071c020efba9f8bf99abfa14deb1b7c2d182db', 126 | 'exceptionalErrors' => 12290, 127 | 'features' => [ 128 | 'frontend.cache.autoTagging' => true, 129 | ], 130 | 'sitename' => 'New TYPO3 site', 131 | ], 132 | ]; 133 | -------------------------------------------------------------------------------- /Classes/Frontend/FrontendSimulationV13.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc\Frontend; 6 | 7 | use InvalidArgumentException; 8 | use Psr\Http\Message\ServerRequestInterface; 9 | use TYPO3\CMS\Core\Cache\CacheManager; 10 | use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; 11 | use TYPO3\CMS\Core\Context\Context; 12 | use TYPO3\CMS\Core\Routing\RouteNotFoundException; 13 | use TYPO3\CMS\Core\Routing\SiteMatcher; 14 | use TYPO3\CMS\Core\Routing\SiteRouteResult; 15 | use TYPO3\CMS\Core\Site\Entity\Site; 16 | use TYPO3\CMS\Core\Site\Entity\SiteInterface; 17 | use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory; 18 | use TYPO3\CMS\Core\Utility\GeneralUtility; 19 | use TYPO3\CMS\Frontend\Aspect\PreviewAspect; 20 | use TYPO3\CMS\Frontend\Cache\CacheInstruction; 21 | use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; 22 | use TYPO3\CMS\Frontend\Page\PageInformationFactory; 23 | 24 | class FrontendSimulationV13 extends FrontendSimulationV12 25 | { 26 | public function getTSFE(ServerRequestInterface $originalRequest): TypoScriptFrontendController 27 | { 28 | return GeneralUtility::makeInstance(TypoScriptFrontendController::class); 29 | } 30 | 31 | public function getTypoScriptSetup(ServerRequestInterface $originalRequest, TypoScriptFrontendController $tsfe): array 32 | { 33 | $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); 34 | $routeResult = $siteMatcher->matchRequest($originalRequest); 35 | if ($routeResult instanceof SiteRouteResult) { 36 | $site = $routeResult->getSite(); 37 | if ($site instanceof Site) { 38 | try { 39 | /** @var Context $context */ 40 | $context = GeneralUtility::makeInstance(Context::class); 41 | $context->setAspect('frontend.preview', new PreviewAspect()); 42 | 43 | $cacheInstruction = $originalRequest->getAttribute('frontend.cache.instruction', new CacheInstruction()); 44 | $originalRequest = $originalRequest->withAttribute('frontend.cache.instruction', $cacheInstruction); 45 | 46 | $pageArguments = $site->getRouter()->matchRequest($originalRequest, $routeResult); 47 | $originalRequest = $originalRequest->withAttribute('routing', $pageArguments); 48 | 49 | $pageInformationFactory = GeneralUtility::makeInstance(PageInformationFactory::class); 50 | $pageInformation = $pageInformationFactory->create($originalRequest); 51 | $originalRequest = $originalRequest->withAttribute('frontend.page.information', $pageInformation); 52 | 53 | $expressionMatcherVariables = $this->getExpressionMatcherVariables($site, $originalRequest, $tsfe); 54 | /** @var CacheManager $cacheManager */ 55 | $cacheManager = GeneralUtility::makeInstance(CacheManager::class); 56 | /** @var PhpFrontend $cache */ 57 | $cache = $cacheManager->getCache('typoscript'); 58 | 59 | $frontendTypoScriptFactory = GeneralUtility::makeInstance(FrontendTypoScriptFactory::class); 60 | $frontendTypoScript = $frontendTypoScriptFactory->createSettingsAndSetupConditions( 61 | $site, 62 | $pageInformation->getSysTemplateRows(), 63 | // $originalRequest does not contain site ... 64 | $expressionMatcherVariables, 65 | $cache, 66 | ); 67 | $frontendTypoScript = $frontendTypoScriptFactory->createSetupConfigOrFullSetup( 68 | true, 69 | $frontendTypoScript, 70 | $site, 71 | $pageInformation->getSysTemplateRows(), 72 | $expressionMatcherVariables, 73 | '0', 74 | $cache, 75 | null 76 | ); 77 | 78 | return $frontendTypoScript->getSetupArray(); 79 | } catch (RouteNotFoundException) { 80 | } 81 | } 82 | } 83 | throw new InvalidArgumentException('Failed to build TypoScript'); 84 | } 85 | 86 | protected function getExpressionMatcherVariables( 87 | SiteInterface $site, 88 | ServerRequestInterface $request, 89 | TypoScriptFrontendController $controller 90 | ): array { 91 | $pageInformation = $request->getAttribute('frontend.page.information'); 92 | $topDownRootLine = $pageInformation->getRootLine(); 93 | $localRootline = $pageInformation->getLocalRootLine(); 94 | ksort($topDownRootLine); 95 | return [ 96 | 'request' => $request, 97 | 'pageId' => $pageInformation->getId(), 98 | 'page' => $pageInformation->getPageRecord(), 99 | 'fullRootLine' => $topDownRootLine, 100 | 'localRootLine' => $localRootline, 101 | 'site' => $site, 102 | 'siteLanguage' => $request->getAttribute('language'), 103 | 'tsfe' => $controller, 104 | ]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_db.xlf: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> 3 | <file source-language="en" original="messages" datatype="plaintext" product-name="oidc" date="2025-06-05T00:33:23+02:00"> 4 | <body> 5 | <trans-unit id="fe_groups.tx_oidc_pattern"> 6 | <source>Case-insensitive pattern to match OpenID Connect role names ("*" matches every character, "|" to separate expressions)</source> 7 | </trans-unit> 8 | <trans-unit id="fe_users.tx_oidc"> 9 | <source>OpenID Connect Identifier</source> 10 | </trans-unit> 11 | <trans-unit id="settings.authenticationServicePriority"> 12 | <source>Authentication service priority: This defines the order of the OIDC authentication service in relation to other authentication services. Higher number wins.</source> 13 | </trans-unit> 14 | <trans-unit id="settings.authenticationServiceQuality"> 15 | <source>Authentication service Quality: This is used by TYPO3 if two authentication services have the same priority. Higher number wins.</source> 16 | </trans-unit> 17 | <trans-unit id="settings.authenticationUrlRoute"> 18 | <source>Route for retrieving the authentication URL of the Identity Provider</source> 19 | </trans-unit> 20 | <trans-unit id="settings.enableCodeVerifier"> 21 | <source>Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along.</source> 22 | </trans-unit> 23 | <trans-unit id="settings.enableFrontendAuthentication"> 24 | <source>Frontend Authentication: Enable OpenID Connect authentication for the frontend.</source> 25 | </trans-unit> 26 | <trans-unit id="settings.enablePasswordCredentials"> 27 | <source>Enable username/password authentication: Enable authentication with username/password via OpenID Connect. e.g. via felogin</source> 28 | </trans-unit> 29 | <trans-unit id="settings.frontendUserMustExistLocally"> 30 | <source>Frontend User Must Exist: If ticked, only Frontend Users who are present locally in TYPO3 will be able to authenticate with OpenID Connect. You may need to watch logs to find users who could not authenticate.</source> 31 | </trans-unit> 32 | <trans-unit id="settings.oauthProviderFactory"> 33 | <source>OAuth Provider Factory: Fully qualified class name (empty for generic provider).</source> 34 | </trans-unit> 35 | <trans-unit id="settings.oidcAuthorizeLanguageParameter"> 36 | <source>Authorize request language parameter name</source> 37 | </trans-unit> 38 | <trans-unit id="settings.oidcClientKey"> 39 | <source>Client Key</source> 40 | </trans-unit> 41 | <trans-unit id="settings.oidcClientScopes"> 42 | <source>Client Scopes:</source> 43 | </trans-unit> 44 | <trans-unit id="settings.oidcClientScopeSeparator"> 45 | <source>Client Scope Separator (empty = ' ' [space]):</source> 46 | </trans-unit> 47 | <trans-unit id="settings.oidcClientSecret"> 48 | <source>Client Secret:</source> 49 | </trans-unit> 50 | <trans-unit id="settings.oidcDisableCSRFProtection"> 51 | <source>Disable CSRF attack mitigation: CAUTION! This is a security protection which checks the return state with the expected value. Disable this protection at your own risk.</source> 52 | </trans-unit> 53 | <trans-unit id="settings.oidcEndpointAuthorize"> 54 | <source>Endpoint URI for authorization</source> 55 | </trans-unit> 56 | <trans-unit id="settings.oidcEndpointLogout"> 57 | <source>Endpoint URI for logout</source> 58 | </trans-unit> 59 | <trans-unit id="settings.oidcEndpointRevoke"> 60 | <source>Endpoint URI for revoking the token</source> 61 | </trans-unit> 62 | <trans-unit id="settings.oidcEndpointToken"> 63 | <source>Endpoint URI for retrieving a token</source> 64 | </trans-unit> 65 | <trans-unit id="settings.oidcEndpointUserInfo"> 66 | <source>Endpoint URI for fetching user information</source> 67 | </trans-unit> 68 | <trans-unit id="settings.oidcRedirectUri"> 69 | <source>Redirect URI: The authentication server callback will point to this URI.</source> 70 | </trans-unit> 71 | <trans-unit id="settings.oidcUseRequestPathAuthentication"> 72 | <source>Use Request Path Authentication: When ticked, this value will use Request Path Authentication instead of standard Password Grant.</source> 73 | </trans-unit> 74 | <trans-unit id="settings.reEnableFrontendUsers"> 75 | <source>Re-enable Frontend Users: If ticked, will automatically re-enable Frontend users marked as "disabled" upon successful authentication.</source> 76 | </trans-unit> 77 | <trans-unit id="settings.revokeAccessTokenAfterLogin"> 78 | <source>Revoke TYPO3's access token at the end of the login process</source> 79 | </trans-unit> 80 | <trans-unit id="settings.undeleteFrontendUsers"> 81 | <source>Undelete Frontend Users: If ticked, will automatically restore Frontend users marked as "deleted" upon successful authentication.</source> 82 | </trans-unit> 83 | <trans-unit id="settings.usersDefaultGroup"> 84 | <source>Default user group(s) (comma-separated list of UIDs)</source> 85 | </trans-unit> 86 | <trans-unit id="settings.usersStoragePid"> 87 | <source>Storage Pid: Comma-separated list of page UIDs where fe_users are located. The first UID is used to store new users.</source> 88 | </trans-unit> 89 | <trans-unit id="tt_content.oidc_login"> 90 | <source>OIDC Login</source> 91 | </trans-unit> 92 | </body> 93 | </file> 94 | </xliff> 95 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | /** 4 | * Extend PhpStorms code completion capabilities by providing a meta file 5 | * 6 | * @link https://www.jetbrains.com/help/phpstorm/ide-advanced-metadata.html 7 | */ 8 | 9 | namespace PHPSTORM_META { 10 | expectedArguments( 11 | \TYPO3\CMS\Core\Context\Context::getAspect(), 12 | 0, 13 | 'date', 14 | 'visibility', 15 | 'backend.user', 16 | 'frontend.user', 17 | 'workspace', 18 | 'language', 19 | 'typoscript' 20 | ); 21 | override(\TYPO3\CMS\Core\Context\Context::getAspect(), map([ 22 | 'date' => \TYPO3\CMS\Core\Context\DateTimeAspect::class, 23 | 'visibility' => \TYPO3\CMS\Core\Context\VisibilityAspect::class, 24 | 'backend.user' => \TYPO3\CMS\Core\Context\UserAspect::class, 25 | 'frontend.user' => \TYPO3\CMS\Core\Context\UserAspect::class, 26 | 'workspace' => \TYPO3\CMS\Core\Context\WorkspaceAspect::class, 27 | 'language' => \TYPO3\CMS\Core\Context\LanguageAspect::class, 28 | 'typoscript' => \TYPO3\CMS\Core\Context\TypoScriptAspect::class, 29 | ])); 30 | expectedArguments( 31 | \TYPO3\CMS\Core\Context\DateTimeAspect::get(), 32 | 0, 33 | 'timestamp', 34 | 'iso', 35 | 'timezone', 36 | 'full', 37 | 'accessTime' 38 | ); 39 | expectedArguments( 40 | \TYPO3\CMS\Core\Context\VisibilityAspect::get(), 41 | 0, 42 | 'includeHiddenPages', 43 | 'includeHiddenContent', 44 | 'includeDeletedRecords' 45 | ); 46 | expectedArguments( 47 | \TYPO3\CMS\Core\Context\UserAspect::get(), 48 | 0, 49 | 'id', 50 | 'username', 51 | 'isLoggedIn', 52 | 'isAdmin', 53 | 'groupIds', 54 | 'groupNames' 55 | ); 56 | expectedArguments( 57 | \TYPO3\CMS\Core\Context\WorkspaceAspect::get(), 58 | 0, 59 | 'id', 60 | 'isLive', 61 | 'isOffline' 62 | ); 63 | expectedArguments( 64 | \TYPO3\CMS\Core\Context\LanguageAspect::get(), 65 | 0, 66 | 'id', 67 | 'contentId', 68 | 'fallbackChain', 69 | 'overlayType', 70 | 'legacyLanguageMode', 71 | 'legacyOverlayType' 72 | ); 73 | expectedArguments( 74 | \TYPO3\CMS\Core\Context\TypoScriptAspect::get(), 75 | 0, 76 | 'forcedTemplateParsing' 77 | ); 78 | 79 | expectedArguments( 80 | \Psr\Http\Message\ServerRequestInterface::getAttribute(), 81 | 0, 82 | 'frontend.user', 83 | 'normalizedParams', 84 | 'site', 85 | 'language', 86 | 'routing', 87 | 'module', 88 | 'moduleData', 89 | 'frontend.controller', 90 | 'frontend.typoscript', 91 | ); 92 | override(\Psr\Http\Message\ServerRequestInterface::getAttribute(), map([ 93 | 'frontend.user' => \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication::class, 94 | 'normalizedParams' => \TYPO3\CMS\Core\Http\NormalizedParams::class, 95 | 'site' => \TYPO3\CMS\Core\Site\Entity\SiteInterface::class, 96 | 'language' => \TYPO3\CMS\Core\Site\Entity\SiteLanguage::class, 97 | 'routing' => '\TYPO3\CMS\Core\Routing\SiteRouteResult|\TYPO3\CMS\Core\Routing\PageArguments', 98 | 'module' => \TYPO3\CMS\Backend\Module\ModuleInterface::class, 99 | 'moduleData' => \TYPO3\CMS\Backend\Module\ModuleData::class, 100 | 'frontend.controller' => \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::class, 101 | 'frontend.typoscript' => \TYPO3\CMS\Core\TypoScript\FrontendTypoScript::class, 102 | ])); 103 | 104 | expectedArguments( 105 | \TYPO3\CMS\Core\Http\ServerRequest::getAttribute(), 106 | 0, 107 | 'frontend.user', 108 | 'normalizedParams', 109 | 'site', 110 | 'language', 111 | 'routing', 112 | 'module', 113 | 'moduleData' 114 | ); 115 | override(\TYPO3\CMS\Core\Http\ServerRequest::getAttribute(), map([ 116 | 'frontend.user' => \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication::class, 117 | 'normalizedParams' => \TYPO3\CMS\Core\Http\NormalizedParams::class, 118 | 'site' => \TYPO3\CMS\Core\Site\Entity\SiteInterface::class, 119 | 'language' => \TYPO3\CMS\Core\Site\Entity\SiteLanguage::class, 120 | 'routing' => '\TYPO3\CMS\Core\Routing\SiteRouteResult|\TYPO3\CMS\Core\Routing\PageArguments', 121 | 'module' => \TYPO3\CMS\Backend\Module\ModuleInterface::class, 122 | 'moduleData' => \TYPO3\CMS\Backend\Module\ModuleData::class, 123 | ])); 124 | 125 | override(\TYPO3\CMS\Core\Routing\SiteMatcher::matchRequest(), type( 126 | \TYPO3\CMS\Core\Routing\SiteRouteResult::class, 127 | \TYPO3\CMS\Core\Routing\RouteResultInterface::class, 128 | ) 129 | ); 130 | 131 | override(\TYPO3\CMS\Core\Routing\PageRouter::matchRequest(), type( 132 | \TYPO3\CMS\Core\Routing\PageArguments::class, 133 | \TYPO3\CMS\Core\Routing\RouteResultInterface::class, 134 | )); 135 | 136 | override(\Psr\Container\ContainerInterface::get(0), map([ 137 | '' => '@', 138 | ])); 139 | 140 | override(\Psr\EventDispatcher\EventDispatcherInterface::dispatch(0), map([ 141 | '' => '@', 142 | ])); 143 | 144 | override(\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(0), map([ 145 | '' => '@' 146 | ])); 147 | } 148 | -------------------------------------------------------------------------------- /Build/docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: t3ext-oidc 2 | services: 3 | v12: 4 | build: 5 | context: typo3 6 | additional_contexts: 7 | - certs=./certs 8 | - oidc=.. 9 | - typo3-version=./typo3/typo3-v12 10 | networks: 11 | default: 12 | aliases: 13 | - v12.t3ext-oidc.test 14 | depends_on: 15 | db-v12: 16 | condition: service_healthy 17 | env_file: 18 | - typo3/oidc.env 19 | environment: 20 | - SERVER_NAME=v12.t3ext-oidc.test 21 | - TYPO3_CONTEXT=Development 22 | 23 | - TYPO3_DB_HOST=db-v12 24 | - TYPO3_DB_PORT=3306 25 | - TYPO3_DB_USERNAME=app 26 | - TYPO3_DB_PASSWORD=app 27 | - TYPO3_DB_DBNAME=app 28 | 29 | - TYPO3_OIDC_OIDC_REDIRECT_URI=https://v12.t3ext-oidc.test/login/redirect 30 | healthcheck: 31 | test: ["CMD", "curl", "-f", "https://v12.t3ext-oidc.test/"] 32 | interval: 3s 33 | 34 | db-v12: 35 | image: mariadb:10.11.10 36 | environment: 37 | MARIADB_ROOT_PASSWORD: root 38 | MARIADB_USER: app 39 | MARIADB_PASSWORD: app 40 | MARIADB_DATABASE: app 41 | healthcheck: 42 | test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] 43 | start_period: 10s 44 | interval: 3s 45 | timeout: 5s 46 | retries: 3 47 | 48 | v13: 49 | build: 50 | context: typo3 51 | additional_contexts: 52 | - certs=./certs 53 | - oidc=.. 54 | - typo3-version=./typo3/typo3-v13 55 | networks: 56 | default: 57 | aliases: 58 | - v13.t3ext-oidc.test 59 | depends_on: 60 | db-v13: 61 | condition: service_healthy 62 | env_file: 63 | - typo3/oidc.env 64 | environment: 65 | - SERVER_NAME=v13.t3ext-oidc.test 66 | - TYPO3_CONTEXT=Development 67 | 68 | - TYPO3_DB_HOST=db-v13 69 | - TYPO3_DB_PORT=3306 70 | - TYPO3_DB_USERNAME=app 71 | - TYPO3_DB_PASSWORD=app 72 | - TYPO3_DB_DBNAME=app 73 | 74 | - TYPO3_OIDC_OIDC_REDIRECT_URI=https://v13.t3ext-oidc.test/login/redirect 75 | healthcheck: 76 | test: ["CMD", "curl", "-f", "https://v13.t3ext-oidc.test/"] 77 | interval: 3s 78 | 79 | db-v13: 80 | image: mariadb:10.11.10 81 | environment: 82 | MARIADB_ROOT_PASSWORD: root 83 | MARIADB_USER: app 84 | MARIADB_PASSWORD: app 85 | MARIADB_DATABASE: app 86 | healthcheck: 87 | test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] 88 | start_period: 10s 89 | interval: 3s 90 | timeout: 5s 91 | retries: 3 92 | 93 | oidc-server-mock: 94 | platform: linux/amd64 95 | container_name: oidc-server-mock 96 | image: ghcr.io/soluto/oidc-server-mock:0.12.1 97 | networks: 98 | default: 99 | aliases: 100 | - oidc.t3ext-oidc.test 101 | healthcheck: 102 | test: ["CMD", "curl", "-f", "http://localhost/"] 103 | environment: 104 | ASPNETCORE_HTTP_PORTS: 80 105 | ASPNETCORE_URLS: http://+:80 106 | ASPNETCORE_ENVIRONMENT: Development 107 | SERVER_OPTIONS_INLINE: | 108 | { 109 | "AccessTokenJwtType": "JWT", 110 | "Discovery": { 111 | "ShowKeySet": true 112 | }, 113 | "Authentication": { 114 | "CookieSameSiteMode": "Lax", 115 | "CheckSessionCookieSameSiteMode": "Lax" 116 | } 117 | } 118 | LOGIN_OPTIONS_INLINE: | 119 | { 120 | "AllowRememberLogin": false 121 | } 122 | LOGOUT_OPTIONS_INLINE: | 123 | { 124 | "AutomaticRedirectAfterSignOut": true 125 | } 126 | API_SCOPES_INLINE: | 127 | - Name: some-app-scope-1 128 | - Name: some-app-scope-2 129 | API_RESOURCES_INLINE: | 130 | - Name: some-app 131 | Scopes: 132 | - some-app-scope-1 133 | - some-app-scope-2 134 | USERS_CONFIGURATION_INLINE: | 135 | [ 136 | { 137 | "SubjectId":"1", 138 | "Username":"User1", 139 | "Password":"pwd", 140 | "Claims": [ 141 | { 142 | "Type": "name", 143 | "Value": "Sam Tailor", 144 | "ValueType": "string" 145 | }, 146 | { 147 | "Type": "email", 148 | "Value": "sam.tailor@gmail.com", 149 | "ValueType": "string" 150 | }, 151 | { 152 | "Type": "some-api-resource-claim", 153 | "Value": "Sam's Api Resource Custom Claim", 154 | "ValueType": "string" 155 | }, 156 | { 157 | "Type": "some-api-scope-claim", 158 | "Value": "Sam's Api Scope Custom Claim", 159 | "ValueType": "string" 160 | }, 161 | { 162 | "Type": "some-identity-resource-claim", 163 | "Value": "Sam's Identity Resource Custom Claim", 164 | "ValueType": "string" 165 | } 166 | ] 167 | } 168 | ] 169 | CLIENTS_CONFIGURATION_PATH: /tmp/config/clients-config.json 170 | ASPNET_SERVICES_OPTIONS_INLINE: | 171 | { 172 | "ForwardedHeadersOptions": { 173 | "ForwardedHeaders" : "All" 174 | } 175 | } 176 | volumes: 177 | - ./oidc-server-mock:/tmp/config:ro 178 | 179 | playwright: 180 | build: 181 | context: playwright 182 | environment: 183 | - DISPLAY=vnc:1 184 | working_dir: /e2e 185 | depends_on: 186 | v12: 187 | condition: service_healthy 188 | v13: 189 | condition: service_healthy 190 | oidc-server-mock: 191 | condition: service_healthy 192 | vnc: 193 | condition: service_healthy 194 | 195 | vnc: 196 | image: consol/ubuntu-xfce-vnc 197 | environment: 198 | - VNC_PW=password 199 | command: /bin/bash -c "xhost + && tail -f /dev/null" 200 | healthcheck: 201 | test: bash -c 'echo > /dev/tcp/localhost/5901' 202 | start_period: 10s 203 | interval: 3s 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenID Connect for TYPO3 frontend login 2 | 3 | This extension lets you authenticate frontend users against an OpenID Connect 4 | provider. 5 | 6 | Examples of such identity provider software or services are: 7 | 8 | - Microsoft EntraID 9 | - Google 10 | - GitHub 11 | - ID Austria 12 | - WSO2 Identity Server 13 | - Keycloak 14 | - Authentik 15 | 16 | ## Direct OIDC Login 17 | 18 | If OpenID Connect is your only means of frontend login, you can use the included 19 | "OIDC Login" plugin. Add it to your login page, where you would normally add the 20 | felogin box. After adding the OIDC Login plugin, requests to the login page will 21 | immediately be redirected to the identity provider. 22 | 23 | After the login process, the user will be redirected: 24 | 25 | - The OIDC Login supports the same `redirect_url` parameter as the felogin box 26 | - If no parameter is set, OIDC Login will redirect the user to the page 27 | configured at `plugin.tx_oidc_login.defaultRedirectPid`. 28 | - If that configuration is not set either, the user will be redirected to '/'. 29 | 30 | ## PKCE (Proof of Key for Code Exchange) 31 | 32 | If your OIDC Login supports _Proof of Key for Code Exchange_ you can enable it 33 | by checking `enableCodeVerifier` in the extension configuration. A shared secret 34 | will be sent along preventing _Authorization Code Interception Attacks_. See 35 | https://tools.ietf.org/html/rfc7636 for details. 36 | 37 | ## Configuration 38 | 39 | ### Mapping Frontend User Fields 40 | 41 | - Configuration is done through TypoScript within 42 | `plugin.tx_oidc.mapping.fe_users` 43 | - OIDC attributes will be recognized by the specific characters `<>`: 44 | 45 | ``` 46 | email = <mail> 47 | ``` 48 | 49 | - You may combine multiple markers as well, e.g., 50 | 51 | ``` 52 | name = <family_name>, <given_name> 53 | ``` 54 | 55 | - Support for [stdWrap](https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Functions/Stdwrap.html) in 56 | field definition, e.g., 57 | 58 | ``` 59 | name = <name> 60 | name.wrap = |-OIDC 61 | ``` 62 | 63 | - Support for [TypoScript "split"](https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Functions/Stdwrap.html#data) 64 | (`//`). This will check multiple field names and return the first one yielding 65 | some non-empty value. E.g., 66 | 67 | ``` 68 | username = <sub> // <contact_number> // <emailaddress> // <benutzername> 69 | ``` 70 | 71 | ### Mapping Frontend User Groups 72 | 73 | - Create your groups within TYPO3 74 | - Use the additional pattern to relate it to roles within OpenID Connect 75 | - Local TYPO3 groups (not related to some role) will be kept upon authenticating 76 | - Default TYPO3 group(s) as configured in Extension Manager will always be added 77 | 78 | ### OIDC Login 79 | 80 | - `plugin.tx_oidc_login.defaultRedirectPid` UID of the page that users will be 81 | redirected to, if no `redirect_url` parameter is set. 82 | 83 | ## Logging 84 | 85 | This extension makes use of the Logging system introduced in TYPO3 CMS 6.0. It 86 | is far more flexible than the old one writing to the "sys_log" table. Technical 87 | details may be found in the [TYPO3 Core API](https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Logging/Index.html#logging). 88 | 89 | As an administrator, what you should know is that the TYPO3 Logger forwards log 90 | records to "Writers", which persist the log record. 91 | 92 | By default, with a vanilla TYPO3 installation, messages are written to the 93 | default log file (`var/log/typo3_*.log`). 94 | 95 | 96 | ### Dedicated Log File for OpenID Connect 97 | 98 | If you want to redirect every logging information from this extension to 99 | `var/log/oidc.log` and send log entries with level "WARNING" or above to the 100 | system log, you may add following configuration to 101 | `typo3conf/AdditionalConfiguration.php`: 102 | 103 | ``` 104 | $GLOBALS['TYPO3_CONF_VARS']['LOG']['Causal']['Oidc']['writerConfiguration'] = [ 105 | \TYPO3\CMS\Core\Log\LogLevel::DEBUG => [ 106 | \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [ 107 | 'logFileInfix' => 'oidc' 108 | ], 109 | ], 110 | 111 | // Configuration for WARNING severity, including all 112 | // levels with higher severity (ERROR, CRITICAL, EMERGENCY) 113 | \TYPO3\CMS\Core\Log\LogLevel::WARNING => [ 114 | \TYPO3\CMS\Core\Log\Writer\SyslogWriter::class => [], 115 | ], 116 | ]; 117 | ``` 118 | 119 | **Hint:** Be sure to read 120 | [Configuration of the Logging system](https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Logging/Configuration/Index.html#logging-configuration) 121 | to fine-tune your configuration on any production website. 122 | 123 | 124 | ## Using additional identity provider packages 125 | 126 | The underlying PHP library for OAuth2 can be extended for specific 127 | identity providers by adding additional packages. 128 | 129 | Example: For Microsoft EntraID (Azure) the package is [thenetworg/oauth2-azure](https://packagist.org/packages/thenetworg/oauth2-azure) 130 | 131 | In order to use these kinds of packages, one needs to implement a custom 132 | `OAuth2ProviderFactory`, which takes care of initializing the specific provider. 133 | 134 | Here is an example for the aforementioned Azure package: 135 | 136 | ```php 137 | <?php 138 | 139 | declare(strict_types=1); 140 | 141 | namespace Reelworx\Sitesetup\Authentication; 142 | 143 | use Causal\Oidc\Factory\OAuthProviderFactoryInterface; 144 | use League\OAuth2\Client\Provider\AbstractProvider; 145 | use TheNetworg\OAuth2\Client\Provider\Azure; 146 | use TYPO3\CMS\Core\Utility\GeneralUtility; 147 | 148 | /* requires some ENV variables to be set, see below */ 149 | final class OAuth2ProviderFactory implements OAuthProviderFactoryInterface 150 | { 151 | public function create(array $settings): AbstractProvider 152 | { 153 | $options = [ 154 | 'clientId' => $settings['oidcClientKey'], 155 | 'redirectUri' => $settings['oidcRedirectUri'], 156 | 'urlAuthorize' => $settings['oidcEndpointAuthorize'], 157 | 'urlAccessToken' => $settings['oidcEndpointToken'], 158 | 'urlResourceOwnerDetails' => $settings['oidcEndpointUserInfo'], 159 | 'scopes' => GeneralUtility::trimExplode(',', $settings['oidcClientScopes'], true), 160 | 'defaultEndPointVersion' => Azure::ENDPOINT_VERSION_2_0, 161 | 'tenant' => getenv('AZURE_OAUTH_CLIENT_TENANT'), 162 | ]; 163 | if ($settings['oidcClientSecret']) { 164 | $options['clientSecret'] = $settings['oidcClientSecret']; 165 | } else { 166 | // https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials 167 | // PEM certificate (newline potentially encoded as '\n' 168 | $options['clientCertificatePrivateKey'] = getenv('AZURE_OAUTH_CLIENT_CERTIFICATE'); 169 | // SHA-1 thumbprint of the X.509 certificate's DER encoding. 170 | $options['clientCertificateThumbprint'] = getenv('AZURE_OAUTH_CLIENT_CERTIFICATE_THUMBPRINT'); 171 | } 172 | return new Azure($options); 173 | } 174 | } 175 | ``` 176 | 177 | ## Run acceptance tests 178 | The `Build` folder contains a docker compose test environment for this oidc extension. It contains: 179 | * TYPO3 v12 instance with ext-oidc installed 180 | * TYPO3 v13 instance with ext-oidc installed 181 | * mock oidc server 182 | * Playwright test runner to run acceptance tests 183 | * VNC Server to watch the playwright tests 184 | 185 | To build the test environment and run the playwright tests run the following command: 186 | ```bash 187 | cd Build 188 | docker compose up --build --exit-code-from playwright && echo "Success" || echo "Fail" 189 | ``` 190 | 191 | ## Credits 192 | 193 | This TYPO3 extension is created and maintained by: 194 | - Xavier Perseguers (https://www.causal.ch/) 195 | - Markus Klein (https://reelworx.at/) 196 | 197 | A big "Thanks" goes out to all contributors. 198 | 199 | -------------------------------------------------------------------------------- /Resources/Private/Language/de.locallang_db.xlf: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> 3 | <file source-language="en" target-language="de" original="messages" datatype="plaintext" product-name="oidc" date="2025-06-05T00:33:23+02:00"> 4 | <body> 5 | <trans-unit id="fe_groups.tx_oidc_pattern"> 6 | <source>Case-insensitive pattern to match OpenID Connect role names ("*" matches every character, "|" to separate expressions)</source> 7 | <target>Groß-/Kleinschreibung-unempfindliches Muster zum Abgleichen von OpenID Connect-Rollennamen ("*" passt zu jedem Zeichen, "|" zu getrennten Ausdrücken)</target> 8 | </trans-unit> 9 | <trans-unit id="fe_users.tx_oidc"> 10 | <source>OpenID Connect Identifier</source> 11 | <target>OpenID Connect Identifier</target> 12 | </trans-unit> 13 | <trans-unit id="settings.authenticationServicePriority"> 14 | <source>Authentication service priority: This defines the order of the OIDC authentication service in relation to other authentication services. Higher number wins.</source> 15 | <target>Authentifizierungs-Service Priorität: Definiert die Reihenfolge des OIDC Authentifizierungs-Service in Bezug auf andere Authentifizierungs-Services. Höhere Zahl gewinnt.</target> 16 | </trans-unit> 17 | <trans-unit id="settings.authenticationServiceQuality"> 18 | <source>Authentication service Quality: This is used by TYPO3 if two authentication services have the same priority. Higher number wins.</source> 19 | <target>Authentifizierungs-Service Qualität: TYPO3 nutzt diesen Wert, wenn zwei Authentifizierungs-Services die gleiche Priorität besitzen. Höhere Zahl gewinnt.</target> 20 | </trans-unit> 21 | <trans-unit id="settings.authenticationUrlRoute"> 22 | <source>Route for retrieving the authentication URL of the Identity Provider</source> 23 | <target>Route um die Authentifzierungs-URL des Identitäts-Anbieters zu erhalten</target> 24 | </trans-unit> 25 | <trans-unit id="settings.enableCodeVerifier"> 26 | <source>Enable PKCE: Enable PKCE flow. Code challenge and code verifier will be sent along.</source> 27 | <target>PKCE aktivieren: Aktiviert den PKCE-Fluss. Code-Challenge und Code-Verifier werden mitgeschickt.</target> 28 | </trans-unit> 29 | <trans-unit id="settings.enableFrontendAuthentication"> 30 | <source>Frontend Authentication: Enable OpenID Connect authentication for the frontend.</source> 31 | <target>Frontend-Authentifizierung: Aktivieren Sie die OpenID Connect-Authentifizierung für das Frontend.</target> 32 | </trans-unit> 33 | <trans-unit id="settings.enablePasswordCredentials"> 34 | <source>Enable username/password authentication: Enable authentication with username/password via OpenID Connect. e.g. via felogin</source> 35 | <target>Authentifizierung mittels Benutzername/Passwort aktivieren: Aktiviere Authentifizierung mittels Benutzername/Passwort über OpenID Connect. z.B. über felogin</target> 36 | </trans-unit> 37 | <trans-unit id="settings.frontendUserMustExistLocally"> 38 | <source>Frontend User Must Exist: If ticked, only Frontend Users who are present locally in TYPO3 will be able to authenticate with OpenID Connect. You may need to watch logs to find users who could not authenticate.</source> 39 | <target>Frontend-Benutzer muss vorhanden sein: Wenn dieses Häkchen gesetzt ist, können sich nur Frontend User, die lokal in TYPO3 vorhanden sind, mit OpenID Connect authentifizieren. Möglicherweise müssen Sie die Logs beobachten, um Benutzer zu finden, die sich nicht authentifizieren konnten.</target> 40 | </trans-unit> 41 | <trans-unit id="settings.oauthProviderFactory"> 42 | <source>OAuth Provider Factory: Fully qualified class name (empty for generic provider).</source> 43 | <target>OAuth Provider Factory: Voll qualifizierter Klassenname (leer für generic provider)</target> 44 | </trans-unit> 45 | <trans-unit id="settings.oidcAuthorizeLanguageParameter"> 46 | <source>Authorize request language parameter name</source> 47 | <target>Name des Sprachparameters für die Autorisierungsanfrage</target> 48 | </trans-unit> 49 | <trans-unit id="settings.oidcClientKey"> 50 | <source>Client Key</source> 51 | <target>Client ID</target> 52 | </trans-unit> 53 | <trans-unit id="settings.oidcClientScopes"> 54 | <source>Client Scopes:</source> 55 | <target>Client Scopes:</target> 56 | </trans-unit> 57 | <trans-unit id="settings.oidcClientSecret"> 58 | <source>Client Secret:</source> 59 | <target>Client-Secret:</target> 60 | </trans-unit> 61 | <trans-unit id="settings.oidcDisableCSRFProtection"> 62 | <source>Disable CSRF attack mitigation: CAUTION! This is a security protection which checks the return state with the expected value. Disable this protection at your own risk.</source> 63 | <target>Deaktivieren Sie die Abschwächung von CSRF-Angriffen: ACHTUNG! Dies ist ein Sicherheitsschutz, der den Rückgabewert mit dem erwarteten Wert abgleicht. Deaktivieren Sie diesen Schutz auf Ihr eigenes Risiko.</target> 64 | </trans-unit> 65 | <trans-unit id="settings.oidcEndpointAuthorize"> 66 | <source>Endpoint URI for authorization</source> 67 | <target>Endpunkt-URI für Autorisierung</target> 68 | </trans-unit> 69 | <trans-unit id="settings.oidcEndpointLogout"> 70 | <source>Endpoint URI for logout</source> 71 | <target>Endpunkt-URI für Logout</target> 72 | </trans-unit> 73 | <trans-unit id="settings.oidcEndpointRevoke"> 74 | <source>Endpoint URI for revoking the token</source> 75 | <target>Endpunkt-URI für das Widerrufen des Tokens</target> 76 | </trans-unit> 77 | <trans-unit id="settings.oidcEndpointToken"> 78 | <source>Endpoint URI for retrieving a token</source> 79 | <target>Endpunkt-URI zum Abrufen eines Tokens</target> 80 | </trans-unit> 81 | <trans-unit id="settings.oidcEndpointUserInfo"> 82 | <source>Endpoint URI for fetching user information</source> 83 | <target>Endpunkt-URI für das Abrufen von Benutzerinformationen</target> 84 | </trans-unit> 85 | <trans-unit id="settings.oidcRedirectUri"> 86 | <source>Redirect URI: The authentication server callback will point to this URI.</source> 87 | <target>Redirect URI: Der Callback des Authentifizierungsservers wird auf diese URI verweisen.</target> 88 | </trans-unit> 89 | <trans-unit id="settings.oidcUseRequestPathAuthentication"> 90 | <source>Use Request Path Authentication: When ticked, this value will use Request Path Authentication instead of standard Password Grant.</source> 91 | <target>Anfragepfad-Authentifizierung verwenden: Wenn dieser Wert angekreuzt ist, wird die Request Path Authentication anstelle der standardmäßigen Password Grant verwendet.</target> 92 | </trans-unit> 93 | <trans-unit id="settings.reEnableFrontendUsers"> 94 | <source>Re-enable Frontend Users: If ticked, will automatically re-enable Frontend users marked as "disabled" upon successful authentication.</source> 95 | <target>Frontend-Benutzer wieder aktivieren: Wenn angekreuzt, werden als "deaktiviert" markierte Frontend-Benutzer nach erfolgreicher Authentifizierung automatisch wieder aktiviert.</target> 96 | </trans-unit> 97 | <trans-unit id="settings.revokeAccessTokenAfterLogin"> 98 | <source>Revoke TYPO3's access token at the end of the login process</source> 99 | <target>Zugriffstoken von TYPO3 am Ende des Anmeldevorgangs widerrufen</target> 100 | </trans-unit> 101 | <trans-unit id="settings.undeleteFrontendUsers"> 102 | <source>Undelete Frontend Users: If ticked, will automatically restore Frontend users marked as "deleted" upon successful authentication.</source> 103 | <target>Frontend-Benutzer wiederherstellen: Wenn angekreuzt, werden als "gelöscht" markierte Frontend-Benutzer nach erfolgreicher Authentifizierung automatisch wiederhergestellt.</target> 104 | </trans-unit> 105 | <trans-unit id="settings.usersDefaultGroup"> 106 | <source>Default user group(s) (comma-separated list of UIDs)</source> 107 | <target>Standard-Benutzergruppen (komma-separierte Liste von UIDs)</target> 108 | </trans-unit> 109 | <trans-unit id="settings.usersStoragePid"> 110 | <source>Storage Pid: Comma-separated list of page UIDs where fe_users are located. The first UID is used to store new users.</source> 111 | <target>Ablage Pid: Komma-separierte Liste der Seiten UIDs, wo fe_users gespeichert werden. Die erste UID wird zur Speicherung neuer Benutzer verwendet.</target> 112 | </trans-unit> 113 | <trans-unit id="tt_content.oidc_login"> 114 | <source>OIDC Login</source> 115 | <target>OIDC-Anmeldung</target> 116 | </trans-unit> 117 | </body> 118 | </file> 119 | </xliff> 120 | -------------------------------------------------------------------------------- /Classes/Service/OpenIdConnectService.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace Causal\Oidc\Service; 6 | 7 | use Causal\Oidc\AuthenticationContext; 8 | use Causal\Oidc\OidcConfiguration; 9 | use InvalidArgumentException; 10 | use Psr\Http\Message\ServerRequestInterface; 11 | use Psr\Http\Message\UriInterface; 12 | use Psr\Log\LoggerAwareInterface; 13 | use Psr\Log\LoggerAwareTrait; 14 | use TYPO3\CMS\Core\Http\NormalizedParams; 15 | use TYPO3\CMS\Core\Http\RedirectResponse; 16 | use TYPO3\CMS\Core\Http\Uri; 17 | use TYPO3\CMS\Core\Site\Entity\SiteLanguage; 18 | use TYPO3\CMS\Core\Utility\GeneralUtility; 19 | 20 | class OpenIdConnectService implements LoggerAwareInterface 21 | { 22 | use LoggerAwareTrait; 23 | 24 | public function __construct( 25 | protected OAuthService $OAuthService, 26 | protected AuthenticationContextService $authenticationContextService, 27 | protected OidcConfiguration $config 28 | ) {} 29 | 30 | public function isAuthenticationRequest(ServerRequestInterface $request): bool 31 | { 32 | /** @var SiteLanguage $language */ 33 | $language = $request->getAttribute('language'); 34 | return $language && $request->getUri()->getPath() === $this->getAuthenticationUrlRoutePath($language); 35 | } 36 | 37 | public function getAuthenticationRequestUrl(): ?UriInterface 38 | { 39 | $request = $GLOBALS['TYPO3_REQUEST'] ?? null; 40 | if ($request) { 41 | $loginUrl = GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'); 42 | $redirectUrl = $request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? ''; 43 | 44 | $query = GeneralUtility::implodeArrayForUrl('', [ 45 | 'login_url' => $loginUrl, 46 | 'redirect_url' => $redirectUrl, 47 | 'validation_hash' => $this->calculateUrlHash($loginUrl . $redirectUrl), 48 | ]); 49 | 50 | $language = $request->getAttribute('language', $request->getAttribute('site')->getDefaultLanguage()); 51 | return $language->getBase() 52 | ->withPath($this->getAuthenticationUrlRoutePath($language)) 53 | ->withQuery($query); 54 | } 55 | return null; 56 | } 57 | 58 | /** 59 | * Generate an authentication context for a given frontend request 60 | * The login URL has to be provided as login_url query parameter in the 61 | * given request. 62 | * A redirect URL may be provided either as part of the login URL or as 63 | * a separate redirect_url query parameter. If the login URL contains a 64 | * redirect URL already, the separate redirect_url query parameter will 65 | * not get evaluated. 66 | * If the login URL does not contain a redirect_url query parameter and 67 | * a separate redirect_url is provided within the requet, the redirect 68 | * URL will be added to the login URL. There will be no cHash though. 69 | * 70 | * The login URL and the optional redirect URL need to be signed with a 71 | * validation hash, provided as the validation_hash parameter of the 72 | * given request. 73 | */ 74 | public function generateAuthenticationContext(ServerRequestInterface $request, array $authorizationUrlOptions = []): AuthenticationContext 75 | { 76 | if (!$this->config->oidcClientKey 77 | || !$this->config->oidcClientSecret 78 | || !$this->config->endpointAuthorize 79 | || !$this->config->endpointToken 80 | ) { 81 | throw new InvalidArgumentException('Missing extension configuration', 1715775147); 82 | } 83 | 84 | $loginUrl = $request->getQueryParams()['login_url'] ?? ''; 85 | if (!GeneralUtility::isValidUrl($loginUrl)) { 86 | throw new InvalidArgumentException('Missing or invalid login_url: ' . $loginUrl, 1759845557572); 87 | } 88 | $redirectUrl = $request->getQueryParams()['redirect_url'] ?? ''; 89 | $hash = $request->getQueryParams()['validation_hash'] ?? ''; 90 | 91 | if ($this->calculateUrlHash($loginUrl . $redirectUrl) !== $hash) { 92 | throw new InvalidArgumentException('Invalid query string', 1719003567); 93 | } 94 | 95 | // Add logintype to login URL 96 | $loginUrlParams = ['logintype' => 'login']; 97 | if ($redirectUrl != '' && !str_contains($loginUrl, 'redirect_url=')) { 98 | $loginUrlParams['redirect_url'] = $redirectUrl; 99 | } 100 | $loginUrl = \GuzzleHttp\Psr7\Uri::withQueryValues(new Uri($loginUrl), $loginUrlParams)->__toString(); 101 | 102 | $authContext = $this->buildAuthenticationContext($request, $authorizationUrlOptions, $loginUrl); 103 | $this->logger->debug('Generated new Authentication Context', ['authContext' => $authContext]); 104 | 105 | return $authContext; 106 | } 107 | 108 | public function buildAuthenticationContext( 109 | ServerRequestInterface $request, 110 | array $authorizationUrlOptions = [], 111 | string $loginUrl = '', 112 | ): AuthenticationContext { 113 | $requestId = $this->getUniqueId(); 114 | $codeVerifier = null; 115 | if ($this->config->enableCodeVerifier) { 116 | $codeVerifier = $this->generateCodeVerifier(); 117 | $codeChallenge = $this->convertVerifierToChallenge($codeVerifier); 118 | $authorizationUrlOptions = array_merge($authorizationUrlOptions, $this->getCodeChallengeOptions($codeChallenge)); 119 | } 120 | 121 | $authorizationUrl = $this->OAuthService->getAuthorizationUrl($request, $authorizationUrlOptions); 122 | $state = $this->OAuthService->getState(); 123 | 124 | $normalizedParams = $request->getAttribute('normalizedParams'); 125 | $isHttps = $normalizedParams instanceof NormalizedParams && $normalizedParams->isHttps(); 126 | 127 | return new AuthenticationContext( 128 | $state, 129 | $loginUrl, 130 | $authorizationUrl, 131 | $requestId, 132 | $isHttps, 133 | $codeVerifier 134 | ); 135 | } 136 | 137 | public function getAuthorizationRedirect(AuthenticationContext $authContext) 138 | { 139 | $url = new Uri($authContext->authorizationUrl); 140 | $cookie = $this->authenticationContextService->getCookieForAuthenticationContext($authContext); 141 | return GeneralUtility::makeInstance(RedirectResponse::class, $url) 142 | ->withAddedHeader('Set-Cookie', (string)$cookie); 143 | } 144 | 145 | public function getFinalLoginUrl(AuthenticationContext $authenticationContext, string $code): UriInterface 146 | { 147 | $loginUrl = new Uri($authenticationContext->loginUrl); 148 | return \GuzzleHttp\Psr7\Uri::withQueryValue($loginUrl, 'tx_oidc[code]', $code); 149 | } 150 | 151 | /** 152 | * Returns a unique ID for the current processed request. 153 | * 154 | * This is supposed to be independent of the actual web server (Nginx or Apache) and 155 | * the way PHP was built and unique enough for our use case, as opposed to using: 156 | * 157 | * - zend_thread_id() which requires PHP to be built with Zend Thread Safety - ZTS - support and debug mode 158 | * - apache_getenv('UNIQUE_ID') which requires Apache as web server and mod_unique_id 159 | * 160 | * @return string 161 | */ 162 | protected function getUniqueId(): string 163 | { 164 | return sprintf('%08x', abs(crc32($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_TIME'] . $_SERVER['REMOTE_PORT']))); 165 | } 166 | 167 | protected function generateCodeVerifier(): string 168 | { 169 | return bin2hex(random_bytes(64)); 170 | } 171 | 172 | protected function convertVerifierToChallenge($codeVerifier): string 173 | { 174 | return rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '='); 175 | } 176 | 177 | protected function getCodeChallengeOptions($codeChallenge): array 178 | { 179 | return [ 180 | 'code_challenge' => $codeChallenge, 181 | 'code_challenge_method' => 'S256', 182 | ]; 183 | } 184 | 185 | protected function getAuthenticationUrlRoutePath(SiteLanguage $language): string 186 | { 187 | return $language->getBase()->getPath() . $this->config->authenticationUrlRoute; 188 | } 189 | 190 | protected function calculateUrlHash(string $value): string 191 | { 192 | if (class_exists(\TYPO3\CMS\Core\Crypto\HashService::class)) { 193 | // TYPO3 v13 194 | $calculatedHash = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Crypto\HashService::class)->hmac($value, 'oidc'); 195 | } else { 196 | // TYPO3 v12 197 | $calculatedHash = GeneralUtility::hmac($value, 'oidc'); 198 | } 199 | return $calculatedHash; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Classes/Service/OAuthService.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | /* 6 | * This file is part of the TYPO3 CMS project. 7 | * 8 | * It is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License, either version 2 10 | * of the License, or any later version. 11 | * 12 | * For the full copyright and license information, please read the 13 | * LICENSE.txt file that was distributed with this source code. 14 | * 15 | * The TYPO3 project - inspiring people to share! 16 | */ 17 | 18 | namespace Causal\Oidc\Service; 19 | 20 | use Causal\Oidc\Event\GetAuthorizationUrlEvent; 21 | use Causal\Oidc\Factory\OAuthProviderFactoryInterface; 22 | use Causal\Oidc\OidcConfiguration; 23 | use GuzzleHttp\RequestOptions; 24 | use League\OAuth2\Client\Grant\AuthorizationCode; 25 | use League\OAuth2\Client\Grant\ClientCredentials; 26 | use League\OAuth2\Client\Grant\Password; 27 | use League\OAuth2\Client\Grant\RefreshToken; 28 | use League\OAuth2\Client\Provider\AbstractProvider; 29 | use League\OAuth2\Client\Provider\Exception\IdentityProviderException; 30 | use League\OAuth2\Client\Provider\ResourceOwnerInterface; 31 | use League\OAuth2\Client\Token\AccessToken; 32 | use League\OAuth2\Client\Token\AccessTokenInterface; 33 | use Psr\EventDispatcher\EventDispatcherInterface; 34 | use Psr\Http\Message\ServerRequestInterface; 35 | use RuntimeException; 36 | use TYPO3\CMS\Core\Http\RequestFactory; 37 | use TYPO3\CMS\Core\Utility\GeneralUtility; 38 | 39 | /** 40 | * Class OAuthService. 41 | */ 42 | class OAuthService 43 | { 44 | protected ?AbstractProvider $provider = null; 45 | 46 | public function __construct( 47 | private readonly EventDispatcherInterface $eventDispatcher, 48 | protected OidcConfiguration $settings 49 | ) {} 50 | 51 | /** 52 | * Returns the authorization URL. 53 | * 54 | * @param ServerRequestInterface|null $request 55 | * @param array $options 56 | * @return string 57 | */ 58 | public function getAuthorizationUrl(?ServerRequestInterface $request, array $options = []): string 59 | { 60 | $event = $this->eventDispatcher->dispatch(new GetAuthorizationUrlEvent($request, $this->settings, $options)); 61 | $options = $event->options; 62 | return $this->getProvider()->getAuthorizationUrl($options); 63 | } 64 | 65 | /** 66 | * Returns the state generated for us. 67 | * 68 | * @return string 69 | * @see getAuthorizationUrl() 70 | */ 71 | public function getState(): string 72 | { 73 | return $this->getProvider()->getState(); 74 | } 75 | 76 | /** 77 | * Returns an AccessToken using either authorization code grant or resource owner password 78 | * credentials grant. 79 | * 80 | * @param string $codeOrUsername Either a code or the username (if password is provided) 81 | * @param string|null $password Optional parameter if authenticating with authorization code grant 82 | * @param string|null $codeVerifier Code verifier for PKCE 83 | * @return AccessToken 84 | * @throws IdentityProviderException 85 | */ 86 | public function getAccessToken( 87 | string $codeOrUsername, 88 | #[\SensitiveParameter] 89 | ?string $password = null, 90 | #[\SensitiveParameter] 91 | ?string $codeVerifier = null 92 | ): AccessToken { 93 | if ($password === null) { 94 | $options = [ 95 | 'code' => $codeOrUsername, 96 | ]; 97 | if ($codeVerifier !== null) { 98 | $options['code_verifier'] = $codeVerifier; 99 | } 100 | $grant = new AuthorizationCode(); 101 | } else { 102 | $options = [ 103 | 'username' => $codeOrUsername, 104 | 'password' => $password, 105 | ]; 106 | $grant = new Password(); 107 | } 108 | return $this->getProvider()->getAccessToken($grant, $options); 109 | } 110 | 111 | /** 112 | * @throws IdentityProviderException 113 | */ 114 | public function getAccessTokenForClient(): AccessTokenInterface 115 | { 116 | return $this->getProvider()->getAccessToken(new ClientCredentials()); 117 | } 118 | 119 | /** 120 | * Returns an AccessToken using request path authentication. 121 | * 122 | * This non-standard behaviour is described on 123 | * https://docs.wso2.com/display/IS530/Try+Password+Grant 124 | * 125 | * @param string $username 126 | * @param string $password 127 | * @return AccessToken|null 128 | * @throws IdentityProviderException 129 | */ 130 | public function getAccessTokenWithRequestPathAuthentication(string $username, #[\SensitiveParameter] string $password): ?AccessToken 131 | { 132 | $url = $this->settings->endpointAuthorize . '?' . http_build_query([ 133 | 'response_type' => 'code', 134 | 'client_id' => $this->settings->oidcClientKey, 135 | 'scope' => $this->settings->oidcClientScopes, 136 | 'redirect_uri' => $this->getRedirectUrl(), 137 | ]); 138 | 139 | $result = GeneralUtility::makeInstance(RequestFactory::class)->request( 140 | 'GET', 141 | $url, 142 | [ 143 | RequestOptions::AUTH => [$username, $password], 144 | RequestOptions::ALLOW_REDIRECTS => false, 145 | ] 146 | ); 147 | 148 | if ($result->getStatusCode() < 300 || $result->getStatusCode() >= 400) { 149 | throw new RuntimeException('Request failed', 1510049345); 150 | } 151 | 152 | if ($result->getHeader('Location')) { 153 | $targetUrl = $result->getHeader('Location')[0]; 154 | $query = parse_url($targetUrl, PHP_URL_QUERY); 155 | parse_str($query, $queryParams); 156 | if (isset($queryParams['code'])) { 157 | return $this->getAccessToken($queryParams['code']); 158 | } 159 | } 160 | 161 | return null; 162 | } 163 | 164 | /** 165 | * Returns the resource owner. 166 | * 167 | * @param AccessToken $token 168 | * @return ResourceOwnerInterface 169 | * @throws IdentityProviderException May be thrown by provider 170 | */ 171 | public function getResourceOwner(AccessToken $token): ResourceOwnerInterface 172 | { 173 | return $this->getProvider()->getResourceOwner($token); 174 | } 175 | 176 | /** 177 | * Revokes the access token. 178 | * 179 | * @param AccessToken $token 180 | * @return bool 181 | * @throws IdentityProviderException 182 | */ 183 | public function revokeToken(AccessToken $token): bool 184 | { 185 | if (!$this->settings->endpointRevoke) { 186 | return false; 187 | } 188 | 189 | $provider = $this->getProvider(); 190 | $request = $provider->getRequest( 191 | AbstractProvider::METHOD_POST, 192 | $this->settings->endpointRevoke, 193 | [ 194 | 'headers' => [ 195 | 'Authorization' => 'Basic ' . base64_encode($this->settings->oidcClientKey . ':' . $this->settings->oidcClientSecret), 196 | 'Content-Type' => 'application/x-www-form-urlencoded', 197 | ], 198 | 'body' => 'token=' . $token->getToken(), 199 | ] 200 | ); 201 | 202 | $response = $provider->getParsedResponse($request); 203 | // TODO error handling? 204 | 205 | return true; 206 | } 207 | 208 | protected function getProvider(): AbstractProvider 209 | { 210 | if ($this->provider === null) { 211 | if (!is_a($this->settings->oauthProviderFactory, OAuthProviderFactoryInterface::class, true)) { 212 | throw new RuntimeException('OAuth provider factory class must implement the OAuthProviderFactoryInterface', 1652689564769); 213 | } 214 | 215 | $settings = $this->settings; 216 | $settings->oidcRedirectUri = $this->getRedirectUrl(); 217 | 218 | $factory = GeneralUtility::makeInstance($this->settings->oauthProviderFactory); 219 | $this->provider = $factory->create($settings); 220 | } 221 | 222 | return $this->provider; 223 | } 224 | 225 | public function getFreshAccessToken(string $serializedToken): ?AccessToken 226 | { 227 | $options = json_decode($serializedToken, true); 228 | if (empty($serializedToken) || empty($options)) { 229 | // Invalid token 230 | return null; 231 | } 232 | $accessToken = new AccessToken($options); 233 | 234 | if (!$accessToken->hasExpired()) { 235 | return $accessToken; 236 | } 237 | 238 | try { 239 | $newAccessToken = $this->getProvider()->getAccessToken(new RefreshToken(), [ 240 | 'refresh_token' => $accessToken->getRefreshToken(), 241 | ]); 242 | return $newAccessToken; 243 | } catch (IdentityProviderException $e) { 244 | // TODO: log problem 245 | return null; 246 | } 247 | 248 | return $accessToken; 249 | } 250 | 251 | protected function getRedirectUrl(): string 252 | { 253 | return $this->settings->oidcRedirectUri ?: GeneralUtility::getIndpEnv('TYPO3_SITE_URL'); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | <one line to give the program's name and a brief idea of what it does.> 294 | Copyright (C) <year> <name of author> 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | <signature of Ty Coon>, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------