diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7b014174d20d5beb398fcd253bb194de310a7f97..e07faec43ccbce9bdc842db27ca8ec68611f13f5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,11 +1,11 @@
-image: docker:19.03.1
+image: docker:19.03.12
 
 variables:
   DOCKER_TLS_CERTDIR: "/certs"
-  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+  IMAGE_VER: 4.2.1-oidc-4.0.0
 
 services:
-  - docker:19.03.1-dind
+  - docker:19.03.12-dind
 
 before_script:
   - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
@@ -13,5 +13,7 @@ before_script:
 build:
   stage: build
   script:
-    - docker build -t $IMAGE_TAG .
-    - docker push $IMAGE_TAG
+    - docker pull $CI_REGISTRY_IMAGE:latest || true
+    - docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$IMAGE_VER --tag $CI_REGISTRY_IMAGE:latest .
+    - docker push $CI_REGISTRY_IMAGE:$IMAGE_VER
+    - docker push $CI_REGISTRY_IMAGE:latest
diff --git a/Dockerfile b/Dockerfile
index 2674695ebe7902e6a53ca7c923262515020061b0..3c16b97e8e5ff62ad0ca031546bbdf3a7d242f51 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM matomo:3.14.0
+FROM matomo:4.2.1
 MAINTAINER Andrej Ramašeuski <andrej.ramaseuski@pirati.cz>
 
 COPY LoginOIDC /var/www/html/plugins/LoginOIDC
diff --git a/LoginOIDC/Auth.php b/LoginOIDC/Auth.php
new file mode 100644
index 0000000000000000000000000000000000000000..0644ad136a7730611dc2888ab8f67af1f32fb34e
--- /dev/null
+++ b/LoginOIDC/Auth.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\LoginOIDC;
+
+use Piwik\AuthResult;
+use Piwik\Plugins\UsersManager\Model;
+
+class Auth extends \Piwik\Plugins\Login\Auth
+{
+    /**
+     * Forces a successful login.
+     *
+     * @var bool
+     */
+    protected $forceLogin;
+
+    /**
+     * @var Model
+     */
+    private $userModel;
+
+    /**
+     * Constructor.
+     */
+    public function __construct()
+    {
+        parent::__construct();
+        $this->userModel = new Model();
+    }
+
+    /**
+     * Authenticates user.
+     *
+     * @return AuthResult
+     */
+    public function authenticate()
+    {
+        if ($this->forceLogin && !empty($this->login)) {
+            $user = $this->userModel->getUser($this->login);
+            return $this->authenticationSuccess($user);
+        }
+        return parent::authenticate();
+    }
+
+    /**
+     * Returns positive AuthResult for a specific user.
+     * See: {@link \Piwik\Plugins\Login\Auth::authenticationSuccess()} method.
+     *
+     * @return AuthResult
+     */
+    private function authenticationSuccess(array $user)
+    {
+        if (empty($this->token_auth)) {
+            $this->token_auth = $this->userModel->generateRandomTokenAuth();
+            // we generated one randomly which will then be stored in the session and used across the session
+        }
+
+        $isSuperUser = (int) $user['superuser_access'];
+        $code = $isSuperUser ? AuthResult::SUCCESS_SUPERUSER_AUTH_CODE : AuthResult::SUCCESS;
+
+        return new AuthResult($code, $user['login'], $this->token_auth);
+    }
+
+    /**
+     * Returns if forceful login is enabled.
+     *
+     * @return bool
+     */
+    public function getForceLogin()
+    {
+        return $this->forceLogin;
+    }
+
+    /**
+     * Sets the forceful login.
+     *
+     * @param bool $forceLogin true if authentication should succeed.
+     */
+    public function setForceLogin(bool $forceLogin)
+    {
+        $this->forceLogin = $forceLogin;
+    }
+}
diff --git a/LoginOIDC/CHANGELOG.md b/LoginOIDC/CHANGELOG.md
index 70b22a184d795f7870bb2c1dd9dba46688312600..929bdbd47378ed47a9646ccd5ddbf5bcce612551 100644
--- a/LoginOIDC/CHANGELOG.md
+++ b/LoginOIDC/CHANGELOG.md
@@ -1,7 +1,21 @@
 ## Changelog
 
+### 4.0.0
+* Prepare plugin for Matomo 4.
+* Linking accounts has been moved to the users security settings.
+
+### 3.0.1
+* Hotfix saving plugin system settings with empty domain whitelist (#34).
+
+### 3.0.0
+* Align version number with Matomo major release version.
+* Support embedding login button on third-party sites.
+* Restrict account creation to specified domains.
+* Support [OIDC Logout URLs](https://openid.net/specs/openid-connect-session-1_0-17.html#RPLogout).
+* Support Matomos regular password verification (currently requires modification of plugins/Login/templates/confirmPassword.twig)
+
 ### 0.1.5
-* Add option to bypass second factor when sign in with OIDC
+* Add option to bypass second factor when sign in with OIDC.
 
 ### 0.1.4
 
diff --git a/LoginOIDC/Controller.php b/LoginOIDC/Controller.php
index f2a93f2d346e156cd9e6473457538ed55f8b6814..b10e7e1bf13f7d06884b2d0a9b5fb5f5dbcda006 100644
--- a/LoginOIDC/Controller.php
+++ b/LoginOIDC/Controller.php
@@ -13,14 +13,15 @@ use Exception;
 use Piwik\Access;
 use Piwik\Auth;
 use Piwik\Common;
+use Piwik\Config;
 use Piwik\Container\StaticContainer;
 use Piwik\Db;
 use Piwik\Nonce;
 use Piwik\Piwik;
 use Piwik\Plugins\UsersManager\API as UsersManagerAPI;
 use Piwik\Plugins\UsersManager\Model;
-use Piwik\Session\SessionInitializer;
 use Piwik\Session\SessionFingerprint;
+use Piwik\Session\SessionInitializer;
 use Piwik\Url;
 use Piwik\View;
 
@@ -48,6 +49,13 @@ class Controller extends \Piwik\Plugin\Controller
      */
     protected $sessionInitializer;
 
+    /**
+     * Revalidates user authentication.
+     *
+     * @var PasswordVerifier
+     */
+    protected $passwordVerify;
+
     /**
      * Constructor.
      *
@@ -59,7 +67,7 @@ class Controller extends \Piwik\Plugin\Controller
         parent::__construct();
 
         if (empty($auth)) {
-            $auth = StaticContainer::get("Piwik\Auth");
+            $auth = StaticContainer::get("Piwik\Plugins\LoginOIDC\Auth");
         }
         $this->auth = $auth;
 
@@ -67,6 +75,11 @@ class Controller extends \Piwik\Plugin\Controller
             $sessionInitializer = new SessionInitializer();
         }
         $this->sessionInitializer = $sessionInitializer;
+
+        if (empty($passwordVerify)) {
+            $passwordVerify = StaticContainer::get("Piwik\Plugins\Login\PasswordVerifier");
+        }
+        $this->passwordVerify = $passwordVerify;
     }
 
     /**
@@ -98,6 +111,17 @@ class Controller extends \Piwik\Plugin\Controller
         ));
     }
 
+    /**
+     * Render the oauth login button when current user is linked to a remote user.
+     *
+     * @return string|null
+     */
+    public function confirmPasswordMod() : ?string
+    {
+        $providerUser = $this->getProviderUser("oidc");
+        return empty($providerUser) ? null : $this->loginMod();
+    }
+
     /**
      * Remove link between the currently signed user and the remote user.
      *
@@ -114,7 +138,7 @@ class Controller extends \Piwik\Plugin\Controller
         $sql = "DELETE FROM " . Common::prefixTable("loginoidc_provider") . " WHERE user=? AND provider=?";
         $bind = array(Piwik::getCurrentUserLogin(), "oidc");
         Db::query($sql, $bind);
-        $this->redirectToIndex("UsersManager", "userSettings");
+        $this->redirectToIndex("UsersManager", "userSecurity");
     }
 
     /**
@@ -124,13 +148,21 @@ class Controller extends \Piwik\Plugin\Controller
      */
     public function signin()
     {
-        if ($_SERVER["REQUEST_METHOD"] !== "POST") {
+        $settings = new \Piwik\Plugins\LoginOIDC\SystemSettings();
+
+        $allowedMethods = array("POST");
+        if (!$settings->disableDirectLoginUrl->getValue()) {
+            array_push($allowedMethods, "GET");
+        }
+        if (!in_array($_SERVER["REQUEST_METHOD"], $allowedMethods)) {
             throw new Exception(Piwik::translate("LoginOIDC_MethodNotAllowed"));
         }
-        // csrf protection
-        Nonce::checkNonce(self::OIDC_NONCE, $_POST["form_nonce"]);
 
-        $settings = new \Piwik\Plugins\LoginOIDC\SystemSettings();
+        if ($_SERVER["REQUEST_METHOD"] === "POST") {
+            // csrf protection
+            Nonce::checkNonce(self::OIDC_NONCE, $_POST["form_nonce"]);
+        }
+
         if (!$this->isPluginSetup($settings)) {
             throw new Exception(Piwik::translate("LoginOIDC_ExceptionNotConfigured"));
         }
@@ -202,6 +234,9 @@ class Controller extends \Piwik\Plugin\Controller
             throw new Exception(Piwik::translate("LoginOIDC_ExceptionInvalidResponse"));
         }
 
+        $_SESSION['loginoidc_idtoken'] = empty($result->id_token) ? null : $result->id_token;
+        $_SESSION['loginoidc_auth'] = true;
+
         $curl = curl_init();
         curl_setopt($curl, CURLOPT_HTTPHEADER, array(
             "Authorization: Bearer " . $result->access_token,
@@ -225,33 +260,13 @@ class Controller extends \Piwik\Plugin\Controller
         $user = $this->getUserByRemoteId("oidc", $providerUserId);
 
         if (empty($user)) {
-            // user with the remote id is currently not in our database
             if (Piwik::isUserIsAnonymous()) {
-                if ($settings->allowSignup->getValue()) {
-                    if (empty($result->email)) {
-                        throw new Exception(Piwik::translate("LoginOIDC_ExceptionUserNotFoundAndNoEmail"));
-                    }
-
-                    $matomoUserLogin = $result->preferred_username;
-                    // Set an invalid pre-hashed password, to block the user from logging in by password
-                    Access::getInstance()->doAsSuperUser(function () use ($matomoUserLogin, $result) {
-                        UsersManagerApi::getInstance()->addUser($matomoUserLogin,
-                                                                "(disallow password login)",
-                                                                $result->email,
-                                                                /* $alias = */ false,
-                                                                /* $_isPasswordHashed = */ true);
-                    });
-                    $userModel = new Model();
-                    $user = $userModel->getUser($matomoUserLogin);
-                    $this->linkAccount($providerUserId, $matomoUserLogin);
-                    $this->signinAndRedirect($user, $settings);
-                } else {
-                    throw new Exception(Piwik::translate("LoginOIDC_ExceptionUserNotFoundAndSignupDisabled"));
-                }
+                // user with the remote id is currently not in our database
+                $this->signupUser($settings, $providerUserId, $result->email);
             } else {
                 // link current user with the remote user
                 $this->linkAccount($providerUserId);
-                $this->redirectToIndex("UsersManager", "userSettings");
+                $this->redirectToIndex("UsersManager", "userSecurity");
             }
         } else {
             // users identity has been successfully confirmed by the remote oidc server
@@ -262,7 +277,12 @@ class Controller extends \Piwik\Plugin\Controller
                     $this->signinAndRedirect($user, $settings);
                 }
             } else {
-                Url::redirectToUrl("index.php");
+                if (Piwik::getCurrentUserLogin() === $user["login"]) {
+                    $this->passwordVerify->setPasswordVerifiedCorrectly();
+                    return;
+                } else {
+                    throw new Exception(Piwik::translate("LoginOIDC_ExceptionAlreadyLinkedToDifferentAccount"));
+                }
             }
         }
     }
@@ -299,6 +319,49 @@ class Controller extends \Piwik\Plugin\Controller
             && !empty($settings->clientSecret->getValue());
     }
 
+    /**
+     * Sign up a new user and link him with a given remote user id.
+     *
+     * @param  SystemSettings  $settings
+     * @param  string          $providerUserId   Remote user id
+     * @param  string          $matomoUserLogin  Users email address, will be used as username as well
+     * @return void
+     */
+    private function signupUser($settings, string $providerUserId, string $matomoUserLogin = null)
+    {
+        // only sign up user if setting is enabled
+        if ($settings->allowSignup->getValue()) {
+            // verify response contains email address
+            if (empty($matomoUserLogin)) {
+                throw new Exception(Piwik::translate("LoginOIDC_ExceptionUserNotFoundAndNoEmail"));
+            }
+
+            // verify email address domain is allowed to sign up
+            if (!empty($settings->allowedSignupDomains->getValue())) {
+                $signupDomain = substr($matomoUserLogin, strpos($matomoUserLogin, "@") + 1);
+                $allowedDomains = explode("\n", $settings->allowedSignupDomains->getValue());
+                if (!in_array($signupDomain, $allowedDomains)) {
+                    throw new Exception(Piwik::translate("LoginOIDC_ExceptionAllowedSignupDomainsDenied"));
+                }
+            }
+
+            // set an invalid pre-hashed password, to block the user from logging in by password
+            Access::getInstance()->doAsSuperUser(function () use ($matomoUserLogin, $result) {
+                UsersManagerApi::getInstance()->addUser($matomoUserLogin,
+                                                        "(disallow password login)",
+                                                        $matomoUserLogin,
+                                                        /* $_isPasswordHashed = */ true,
+                                                        /* $initialIdSite = */ null);
+            });
+            $userModel = new Model();
+            $user = $userModel->getUser($matomoUserLogin);
+            $this->linkAccount($providerUserId, $matomoUserLogin);
+            $this->signinAndRedirect($user, $settings);
+        } else {
+            throw new Exception(Piwik::translate("LoginOIDC_ExceptionUserNotFoundAndSignupDisabled"));
+        }
+    }
+
     /**
      * Sign in the given user and redirect to the front page.
      *
@@ -308,7 +371,7 @@ class Controller extends \Piwik\Plugin\Controller
     private function signinAndRedirect(array $user, SystemSettings $settings)
     {
         $this->auth->setLogin($user["login"]);
-        $this->auth->setTokenAuth($user["token_auth"]);
+        $this->auth->setForceLogin(true);
         $this->sessionInitializer->initSession($this->auth);
         if ($settings->bypassTwoFa->getValue()) {
             $sessionFingerprint = new SessionFingerprint();
diff --git a/LoginOIDC/LoginOIDC.php b/LoginOIDC/LoginOIDC.php
index 26174946bc09b9fa3249902dd037d728726596a1..57bbf0be493ba5babcdfe630c51fe29601f71148 100644
--- a/LoginOIDC/LoginOIDC.php
+++ b/LoginOIDC/LoginOIDC.php
@@ -11,8 +11,12 @@ namespace Piwik\Plugins\LoginOIDC;
 
 use Exception;
 use Piwik\Common;
+use Piwik\Config;
 use Piwik\Db;
 use Piwik\FrontController;
+use Piwik\Plugins\LoginOIDC\SystemSettings;
+use Piwik\Plugins\LoginOIDC\Url;
+use Piwik\Session;
 
 class LoginOIDC extends \Piwik\Plugin
 {
@@ -25,12 +29,42 @@ class LoginOIDC extends \Piwik\Plugin
     public function registerEvents() : array
     {
         return array(
+            "Session.beforeSessionStart" => "beforeSessionStart",
             "AssetManager.getStylesheetFiles" => "getStylesheetFiles",
-            "Template.userSettings.afterTokenAuth" => "renderLoginOIDCUserSettings",
-            "Template.loginNav" => "renderLoginOIDCMod"
+            "Template.userSecurity.afterPassword" => "renderLoginOIDCUserSettings",
+            "Template.loginNav" => "renderLoginOIDCMod",
+            "Template.confirmPasswordContent" => "renderConfirmPasswordMod",
+            "Login.logout" => "logoutMod"
         );
     }
 
+    /**
+     * Create RememberMe cookie.
+     * @see \Piwik\Plugins\Login::beforeSessionStart
+     *
+     * @return void
+     */
+    public function beforeSessionStart() : void
+    {
+        if (!$this->shouldHandleRememberMe()) {
+            return;
+        }
+        Session::rememberMe(Config::getInstance()->General["login_cookie_expire"]);
+    }
+
+    /**
+     * Decide if RememberMe cookie should be handled by the plugin.
+     * @see \Piwik\Plugins\Login::shouldHandleRememberMe
+     *
+     * @return bool
+     */
+    private function shouldHandleRememberMe() : bool
+    {
+        $module = Common::getRequestVar("module", false);
+        $action = Common::getRequestVar("action", false);
+        return ($module == "LoginOIDC") && ($action == "callback");
+    }
+
     /**
      * Append additional stylesheets.
      *
@@ -73,6 +107,45 @@ class LoginOIDC extends \Piwik\Plugin
         }
     }
 
+    /**
+     * Append login oauth button layout.
+     *
+     * @param  string       $out
+     * @param  string|null  $payload
+     * @return void
+     */
+    public function renderConfirmPasswordMod(string &$out, string $payload = null)
+    {
+        if (!empty($payload) && $payload === "bottom") {
+            $content = FrontController::getInstance()->dispatch("LoginOIDC", "confirmPasswordMod");
+            if (!empty($content)) {
+                $out .= $content;
+            }
+        }
+    }
+
+    /**
+     * Temporarily override logout url to the oidc provider end user session endpoint.
+     *
+     * @return void
+     */
+    public function logoutMod()
+    {
+        $settings = new SystemSettings();
+        $endSessionUrl = $settings->endSessionUrl->getValue();
+        if (!empty($endSessionUrl) && $_SESSION["loginoidc_auth"]) {
+            $endSessionUrl = new Url($endSessionUrl);
+            if (isset($_SESSION[loginoidc_idtoken])) {
+                $endSessionUrl->setQueryParameter("id_token_hint", $_SESSION[loginoidc_idtoken]);
+            }
+            $originalLogoutUrl = Config::getInstance()->General['login_logout_url'];
+            if ($originalLogoutUrl) {
+                $endSessionUrl->setQueryParameter("post_logout_redirect_uri", $originalLogoutUrl);
+            }
+            Config::getInstance()->General['login_logout_url'] = $endSessionUrl->buildString();
+        }
+    }
+
     /**
      * Extend database.
      *
@@ -90,7 +163,7 @@ class LoginOIDC extends \Piwik\Plugin
                 PRIMARY KEY ( provider_user, provider ),
                 UNIQUE KEY user_provider ( user, provider ),
                 FOREIGN KEY ( user ) REFERENCES " . Common::prefixTable("user") . " ( login ) ON DELETE CASCADE
-                ) ENGINE=InnoDB DEFAULT CHARSET=utf8";
+                ) ENGINE=InnoDB";
             Db::exec($sql);
         } catch(Exception $e) {
             // ignore error if table already exists (1050 code is for 'table already exists')
diff --git a/LoginOIDC/README.md b/LoginOIDC/README.md
index 6db1a5d5d9dd946070d2d3b3d786a2757a9e6897..b5e18f7fbd8ec3c216632a8637ebd0a796920d24 100644
--- a/LoginOIDC/README.md
+++ b/LoginOIDC/README.md
@@ -4,7 +4,7 @@
 
 Login via third party authentication services.
 
-Easily add a "Login with Github" button your Matomo instance. You can also setup any other service to do the authentication for you.
+Easily add a "Login with GitHub" button your Matomo instance. You can also setup any other service to do the authentication for you.
 
 ## Installation
 
diff --git a/LoginOIDC/SystemSettings.php b/LoginOIDC/SystemSettings.php
index b6bc42b43f61d4a7a0acf20dc29d9edd3d109ce6..f99c3063439517327da50817bcd67e682f645dae 100644
--- a/LoginOIDC/SystemSettings.php
+++ b/LoginOIDC/SystemSettings.php
@@ -9,6 +9,7 @@
 
 namespace Piwik\Plugins\LoginOIDC;
 
+use Exception;
 use Piwik\Piwik;
 use Piwik\Settings\FieldConfig;
 use Piwik\Settings\Plugin\SystemSetting;
@@ -26,6 +27,13 @@ class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
      */
     public $disableSuperuser;
 
+    /**
+     * Whether the login procedure has to be initiated from the Matomo login page
+     *
+     * @var bool
+     */
+    public $disableDirectLoginUrl;
+
     /**
      * Whether new Matomo accounts should be created for unknown users
      *
@@ -66,6 +74,13 @@ class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
      */
     public $userinfoUrl;
 
+    /**
+     * The url where the OIDC provider will invalidate the users session.
+     *
+     * @var string
+     */
+    public $endSessionUrl;
+
     /**
      * The name of the unique user id field in $userinfoUrl response.
      *
@@ -101,6 +116,13 @@ class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
      */
     public $redirectUriOverride;
 
+    /**
+     * The domains which are allowed to create accounts.
+     *
+     * @var string
+     */
+    public $allowedSignupDomains;
+
     /**
      * Initialize the plugin settings.
      *
@@ -109,17 +131,20 @@ class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
     protected function init()
     {
         $this->disableSuperuser = $this->createDisableSuperuserSetting();
+        $this->disableDirectLoginUrl = $this->createDisableDirectLoginUrlSetting();
         $this->allowSignup = $this->createAllowSignupSetting();
         $this->bypassTwoFa = $this->createBypassTwoFaSetting();
         $this->authenticationName = $this->createAuthenticationNameSetting();
         $this->authorizeUrl = $this->createAuthorizeUrlSetting();
         $this->tokenUrl = $this->createTokenUrlSetting();
         $this->userinfoUrl = $this->createUserinfoUrlSetting();
+        $this->endSessionUrl = $this->createEndSessionUrlSetting();
         $this->userinfoId = $this->createUserinfoIdSetting();
         $this->clientId = $this->createClientIdSetting();
         $this->clientSecret = $this->createClientSecretSetting();
         $this->scope = $this->createScopeSetting();
         $this->redirectUriOverride = $this->createRedirectUriOverrideSetting();
+        $this->allowedSignupDomains = $this->createAllowedSignupDomainsSetting();
     }
 
     /**
@@ -136,6 +161,20 @@ class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
         });
     }
 
+    /**
+     * Add disable direct login url setting.
+     *
+     * @return SystemSetting
+     */
+    private function createDisableDirectLoginUrlSetting() : SystemSetting
+    {
+        return $this->makeSetting("disableDirectLoginUrl", $default = true, FieldConfig::TYPE_BOOL, function(FieldConfig $field) {
+            $field->title = Piwik::translate("LoginOIDC_SettingDisableDirectLoginUrl");
+            $field->description = Piwik::translate("LoginOIDC_SettingDisableDirectLoginUrlHelp");
+            $field->uiControl = FieldConfig::UI_CONTROL_CHECKBOX;
+        });
+    }
+
     /**
      * Add allowSignup setting.
      *
@@ -223,6 +262,20 @@ class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
         });
     }
 
+    /**
+     * Add end session url setting.
+     *
+     * @return SystemSetting
+     */
+    private function createEndSessionUrlSetting() : SystemSetting
+    {
+        return $this->makeSetting("endSessionUrl", $default = "", FieldConfig::TYPE_STRING, function(FieldConfig $field) {
+            $field->title = Piwik::translate("LoginOIDC_SettingEndSessionUrl");
+            $field->description = Piwik::translate("LoginOIDC_SettingEndSessionUrlHelp");
+            $field->uiControl = FieldConfig::UI_CONTROL_URL;
+        });
+    }
+
     /**
      * Add userinfo id setting.
      *
@@ -293,4 +346,31 @@ class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
             $field->uiControl = FieldConfig::UI_CONTROL_URL;
         });
     }
+
+    /**
+     * Add allowed signup domains setting.
+     *
+     * @return SystemSetting
+     */
+    private function createAllowedSignupDomainsSetting() : SystemSetting
+    {
+        return $this->makeSetting("allowedSignupDomains", $default = "", FieldConfig::TYPE_STRING, function(FieldConfig $field) {
+            $field->title = Piwik::translate("LoginOIDC_SettingAllowedSignupDomains");
+            $field->description = Piwik::translate("LoginOIDC_SettingAllowedSignupDomainsHelp");
+            $field->uiControl = FieldConfig::UI_CONTROL_TEXTAREA;
+            $field->validate = function ($value, $setting) {
+                if (empty($value)) {
+                    return;
+                }
+                $domainPattern = "/^(((?!-))(xn--|_{1,1})?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})$/";
+                $domains = explode("\n", $value);
+                foreach($domains as $domain) {
+                    $isValidDomain = preg_match($domainPattern, $domain);
+                    if (!$isValidDomain) {
+                        throw new Exception(Piwik::translate("LoginOIDC_ExceptionAllowedSignupDomainsValidationFailed"));
+                    }
+                }
+            };
+        });
+    }
 }
diff --git a/LoginOIDC/Url.php b/LoginOIDC/Url.php
new file mode 100644
index 0000000000000000000000000000000000000000..d97dc43e07fd43f1f0128744d1f3b0efcff947e9
--- /dev/null
+++ b/LoginOIDC/Url.php
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\LoginOIDC;
+
+class Url
+{
+    /**
+     * Scheme of the url.
+     *
+     * @var string
+     */
+    protected $scheme;
+
+    /**
+     * Username as part of the url.
+     *
+     * @var string
+     */
+    protected $username;
+
+    /**
+     * Password as part of the url.
+     *
+     * @var string
+     */
+    protected $password;
+
+    /**
+     * Full host of the url.
+     *
+     * @var string
+     */
+    protected $host;
+
+    /**
+     * Port of the url.
+     *
+     * @var int
+     */
+    protected $port;
+
+    /**
+     * Path of the url.
+     *
+     * @var string
+     */
+    protected $path;
+
+    /**
+     * Query parameters of the url.
+     *
+     * @var array
+     */
+    protected $query;
+
+    /**
+     * Fragment identifier of the url.
+     *
+     * @var string
+     */
+    protected $fragment;
+
+    /**
+     * Constructor.
+     *
+     * @param string  $url full URL as string
+     */
+    public function __construct(string $url)
+    {
+        $urlParts = parse_url($url);
+
+        $this->scheme = $urlParts["scheme"];
+        $this->host = $urlParts["host"];
+        $this->path = $urlParts["path"];
+
+        if (isset($urlParts["query"])) { 
+            parse_str($urlParts["query"], $this->query);
+        }
+    }
+
+    /**
+     * Build a full url string based on the parts.
+     * 
+     * @return string
+     */
+    public function buildString() : string
+    {
+        $url = $this->scheme . "://";
+        if (!empty($this->username) || !empty($this->password)) {
+            $url .= $this->username . ":" . $this->password . "@";
+        }
+        $url .= $this->host;
+        if (!empty($this->port)) {
+            $url .= ":" . $this->port;
+        }
+        $url .= $this->path;
+        if (!empty($this->query)) {
+            $url .= "?" . http_build_query($this->query);
+        }
+        if (!empty($this->fragment)) {
+            $url .= "#" . $this->fragment;
+        }
+        return $url;
+    }
+
+    /**
+     * Get value of a given query parameter.
+     *
+     * @return string
+     */
+    public function getQueryParameter(string $parameter) : string
+    {
+        return $this->query[$parameter];
+    }
+
+    /**
+     * Set value of a given query parameter.
+     *
+     * @return void
+     */
+    public function setQueryParameter(string $parameter, string $value)
+    {
+        $this->query[$parameter] = $value;
+    }
+}
diff --git a/LoginOIDC/docs/faq.md b/LoginOIDC/docs/faq.md
index 05f38e7f357800beaf9ff1b805530243fc4e09be..a74eeee7e94c7d07e6c7ecd24f2409593490314e 100644
--- a/LoginOIDC/docs/faq.md
+++ b/LoginOIDC/docs/faq.md
@@ -2,12 +2,12 @@
 
 **What is the callback url?**
 
-http(s)://<YOUR_MATOMO_URL>/index.php?module=LoginOIDC&action=callback&provider=oidc
+`http(s)://<YOUR_MATOMO_URL>/index.php?module=LoginOIDC&action=callback&provider=oidc`
 
 **Which providers can I use?**
 
-I tested the plugin with Auth0, Github and Keycloak, which work fine.
-If your provider does not seem to work, leave an issue on Github.
+I tested the plugin with Auth0, GitHub and Keycloak, which work fine.
+If your provider does not seem to work, leave an issue on GitHub.
 
 **How can I unlink all users?**
 
@@ -16,6 +16,11 @@ Otherwise you can delete data from `matomo_loginoidc_provider` in your sql datab
 
 If you change the OAuth provider and there could be user id collisions, you should make sure to unlink all users beforehand.
 
+**Can I embed the Login button on another website?**
+
+You have to uncheck the `Disable direct login url` option in the settings.
+Afterwards you can link to `http(s)://<YOUR_MATOMO_URL>/index.php?module=LoginOIDC&action=signin&provider=oidc` and Matomo will redirect the client accordingly.
+
 **Can I setup more than one provider?**
 
 Currently that is **not** possible.
@@ -29,7 +34,7 @@ https://matomo.org/faq/troubleshooting/faq_25610/
 
 **What are the settings for ...?**
 
-- Github:
+- GitHub:
 
   - Authorize URL: `https://github.com/login/oauth/authorize`
   - Token URL: `https://github.com/login/oauth/access_token`
@@ -43,25 +48,26 @@ https://matomo.org/faq/troubleshooting/faq_25610/
   - Token URL: `https://<USERNAME>.eu.auth0.com/oauth/token`
   - Userinfo URL: `https://<USERNAME>.eu.auth0.com/userinfo`
   - Userinfo ID: `sub`
-  - OAuth Scopes: `openid`
+  - OAuth Scopes: `openid email`
 
 - Keycloak:
 
-  - Authorize URL: `http(s)://<YOUR_KEYCLOAK_INSTALLATION>/auth/realms/<REALM>/protocol/openid-connect/auth`
-  - Token URL: `http(s)://<YOUR_KEYCLOAK_INSTALLATION>/auth/realms/<REALM>/protocol/openid-connect/token`
-  - Userinfo URL: `http(s)://<YOUR_KEYCLOAK_INSTALLATION>/auth/realms/<REALM>/protocol/openid-connect/userinfo`
+  - Authorize URL: `http(s)://<YOUR_KEYCLOAK_URL>/auth/realms/<REALM>/protocol/openid-connect/auth`
+  - Token URL: `http(s)://<YOUR_KEYCLOAK_URL>/auth/realms/<REALM>/protocol/openid-connect/token`
+  - Userinfo URL: `http(s)://<YOUR_KEYCLOAK_URL>/auth/realms/<REALM>/protocol/openid-connect/userinfo`
+  - Logout URL: `http(s)://<YOUR_KEYCLOAK_URL>/auth/realms/<REALM>/protocol/openid-connect/logout?redirect_uri=<MATOMO_URL>`
   - Userinfo ID: `sub`
-  - OAuth Scopes: `openid`
+  - OAuth Scopes: `openid email`
 
-- Gitlab (self-hosted Community Edition 12.6.2)
+- Gitlab (self-hosted Community Edition 12.6.2):
 
-  - Authorize URL: `http(s)://<YOUR_GIT_DOMAIN>/oauth/authorize`
-  - Token URL: `http(s)://<YOUR_GIT_DOMAIN>/oauth/token`
-  - Userinfo URL: `http(s)://<YOUR_GIT_DOMAIN>/oauth/userinfo`
+  - Authorize URL: `http(s)://<YOUR_GITLAB_URL>/oauth/authorize`
+  - Token URL: `http(s)://<YOUR_GITLAB_URL>/oauth/token`
+  - Userinfo URL: `http(s)://<YOUR_GITLAB_URL>/oauth/userinfo`
   - Userinfo ID: `sub`
   - OAuth Scopes: `openid email`
 
-- [Unikname Connect](https://unikname.com)
+- Unikname Connect:
 
   - Name: `Connect with your private @unikname`
   - Authorize URL: `https://connect.unikname.com/oidc/authorize`
@@ -71,19 +77,8 @@ https://matomo.org/faq/troubleshooting/faq_25610/
   - OAuth Scopes: `openid email`
 
 - Microsoft Azure AD
-  - Authorize URL: `https://login.microsoftonline.com/{tenant_id}/oauth2/authorize`
-  - Token URL: `https://login.microsoftonline.com/{tenant_id}/oauth2/token`
-  - Userinfo URL: `https://login.microsoftonline.com/{tenant_id}/openid/userinfo`
+  - Authorize URL: `https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/authorize`
+  - Token URL: `https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token`
+  - Userinfo URL: `https://graph.microsoft.com/oidc/userinfo`
   - Userinfo ID: `sub`
-  - OAuth Scopes: `openid`
-  - Redirect URI Override\*: `http(s)://<YOUR_MATOMO_INSTALLATION>/oidc/callback`
-
-\*because Microsoft Azure AD does not allow query parameters in the redirect URI we also have to edit our nginx configuration to work around this limitation:
-
-```nginx
-server {
-    # ...
-    rewrite ^/oidc/callback /index.php?module=LoginOIDC&action=callback&provider=oidc redirect;
-    # ...
-}
-```
+  - OAuth Scopes: `openid email`
diff --git a/LoginOIDC/lang/de.json b/LoginOIDC/lang/de.json
index 2169ccba809555c61435ddde803f762787f77136..d54a5624148078c7f86122bae1e4fa3fad1138d5 100644
--- a/LoginOIDC/lang/de.json
+++ b/LoginOIDC/lang/de.json
@@ -1,8 +1,10 @@
 {
     "LoginOIDC": {
-        "SettingDisableSuperuser": "Deaktiviere externen Login-Service für Superuser.",
+        "SettingDisableSuperuser": "Deaktiviere externen Login-Service für Superuser",
         "SettingDisableSuperuserHelp": "",
-        "SettingAllowSignup": "Erstelle automatisch einen neuen Account, wenn sich ein unbekannter neuer Benutzer einloggt.",
+        "SettingDisableDirectLoginUrl": "Deaktiviere direkten Login Link",
+        "SettingDisableDirectLoginUrlHelp": "Wenn der Haken gesetzt ist muss der Login über die Login-Seite initiiert werden, anderenfalls lässt sich die Loginschaltfläche auch von dritten Seiten einbinden.",
+        "SettingAllowSignup": "Erstelle automatisch einen neuen Account, wenn sich ein unbekannter neuer Benutzer einloggt",
         "SettingAllowSignupHelp": "",
         "SettingAuthenticationName": "Name",
         "SettingAuthenticationNameHelp": "Name des externen Login-Services, der auf der Login-Seite angezeigt wird.",
@@ -12,6 +14,8 @@
         "SettingTokenUrlHelp": "z.B. https://<USERNAME>.eu.auth0.com/oauth/token",
         "SettingUserinfoUrl": "Userinfo URL",
         "SettingUserinfoUrlHelp": "z.B. https://<USERNAME>.eu.auth0.com/userinfo",
+        "SettingEndSessionUrl": "Logout URL",
+        "SettingEndSessionUrlHelp": "Nach dem Logout wird der Benutzer zu dieser URL weitergeleitet, damit die Session beim Provider beendet wird. Bei Unklarheit sollte dieses Feld freigelassen werden.",
         "SettingUserinfoId": "Userinfo ID",
         "SettingUserinfoIdHelp": "Name des Feldes, in dem die Benutzer-ID enthalten ist. Normalerweise, für OpenID Connect Dienste wie Auth0, ist das 'sub'. Github gibt die eindeutige Benutzer-ID in dem Feld 'id' an.",
         "SettingClientId": "Client ID",
@@ -22,6 +26,8 @@
         "SettingScopeHelp": "z.B. 'openid' oder 'openid email'",
         "SettingRedirectUriOverride": "Benutzerdefinierte Redirect URI",
         "SettingRedirectUriOverrideHelp": "In manchen Fällen ist es nützlich, die Redirect URI, die an den OpenID Connect Provider übergeben wird, zu überschreiben. Bei Unklarheit sollte dieses Feld freigelassen werden.",
+        "SettingAllowedSignupDomains": "Erlaubte Domains für Accounterstellung",
+        "SettingAllowedSignupDomainsHelp": "Wenn das Feld freigelassen wird, können sich Benutzer mit beliebiger E-Mail Adresse registrieren. Mehrere Domains können in separaten Zeilen angegeben werden.",
         "OpenIDConnect": "OpenID Connect",
         "OIDCIntro": "Dies erlaubt es Dir, Dich über einen externen Service bei Matomo einzuloggen.",
         "AccountLinked": "Dein Account ist zur Zeit verknüpft (Entfernte Benutzer-ID: %1$s).",
@@ -34,6 +40,9 @@
         "ExceptionInvalidResponse": "Unerwartete Antwort vom OAuth-Service.",
         "ExceptionUserNotFoundAndSignupDisabled": "Benutzer nicht gefunden. Neue Registrierungen über OAuth werden nicht unterstützt.",
         "ExceptionUserNotFoundAndNoEmail": "Benutzer nicht gefunden. Benutzer konnte nicht erstellt werden, weil der OAuth Service keine E-Mail Adresse zurückgab.",
-        "ExceptionSuperUserOauthDisabled": "OAuth Login für Superuser ist deaktiviert."
+        "ExceptionSuperUserOauthDisabled": "OAuth Login für Superuser ist deaktiviert.",
+        "ExceptionAllowedSignupDomainsValidationFailed": "Die Liste der zugelassenen Domains hat nicht das richtige Format.",
+        "ExceptionAllowedSignupDomainsDenied": "Die verwendete Domain ist nicht für Registrierungen freigeschaltet.",
+        "ExceptionAlreadyLinkedToDifferentAccount": "Der Benutzer beim OAuth-Service ist bereits mit einem anderem Matomo-Nutzer verlinkt."
     }
 }
diff --git a/LoginOIDC/lang/en.json b/LoginOIDC/lang/en.json
index 516a38fea38ee26a18e9f60c0be4258d5ba247f1..aa2136d56329a6d21055b46a925378b8d21b757f 100644
--- a/LoginOIDC/lang/en.json
+++ b/LoginOIDC/lang/en.json
@@ -1,8 +1,10 @@
 {
     "LoginOIDC": {
-        "SettingDisableSuperuser": "Disable external login for superusers.",
+        "SettingDisableSuperuser": "Disable external login for superusers",
         "SettingDisableSuperuserHelp": "",
-        "SettingAllowSignup": "Create new users when users try to log in with unknown OIDC accounts.",
+        "SettingDisableDirectLoginUrl": "Disable direct login url",
+        "SettingDisableDirectLoginUrlHelp": "When checked, users have to inititate the login via the Matomo login page. Otherwise the login button can be embedded in third-party services.",
+        "SettingAllowSignup": "Create new users when users try to log in with unknown OIDC accounts",
         "SettingAllowSignupHelp": "",
         "SettingBypassTwoFa": "Disable second factor when sign in with OIDC",
         "SettingBypassTwoFaHelp": "",
@@ -14,6 +16,8 @@
         "SettingTokenUrlHelp": "e.g. https://<USERNAME>.eu.auth0.com/oauth/token",
         "SettingUserinfoUrl": "Userinfo URL",
         "SettingUserinfoUrlHelp": "e.g. https://<USERNAME>.eu.auth0.com/userinfo",
+        "SettingEndSessionUrl": "Logout URL",
+        "SettingEndSessionUrlHelp": "After logging out, the user is redirected to this URL to end the session at the provider. If you are unsure, just leave this field empty.",
         "SettingUserinfoId": "Userinfo ID",
         "SettingUserinfoIdHelp": "Name of the unique user id field in the userinfo response. Usually for OpenID Connect services like Auth0 this is 'sub'. Github provides the user id in 'id'.",
         "SettingClientId": "Client ID",
@@ -24,6 +28,8 @@
         "SettingScopeHelp": "e.g. 'openid' or 'openid email'",
         "SettingRedirectUriOverride": "Redirect URI override",
         "SettingRedirectUriOverrideHelp": "In some cases it might be useful to manipulate the redirect uri which is given to the OpenID Connect provider. If you are unsure, just leave this field empty.",
+        "SettingAllowedSignupDomains": "Restrict user creation to domains",
+        "SettingAllowedSignupDomainsHelp": "List of email domains which should be allowed to create new accounts. Multiple domains have to be separated by line breaks. When empty, any email domain will be accepted.",
         "OpenIDConnect": "OpenID Connect",
         "OIDCIntro": "This allows you to sign in using an external authentication service.",
         "AccountLinked": "Your account is currently linked (Remote User ID: %1$s).",
@@ -36,6 +42,9 @@
         "ExceptionInvalidResponse": "Unexpected response from OAuth service.",
         "ExceptionUserNotFoundAndSignupDisabled": "User not found. OAuth registrations are disabled.",
         "ExceptionUserNotFoundAndNoEmail": "User not found. User could not be created because the OAuth service did not return an email address.",
-        "ExceptionSuperUserOauthDisabled": "OAuth login disabled for superusers."
+        "ExceptionSuperUserOauthDisabled": "OAuth login disabled for superusers.",
+        "ExceptionAllowedSignupDomainsValidationFailed": "Validation failed for the list of domains allowed for user creation.",
+        "ExceptionAllowedSignupDomainsDenied": "The domain is currently not activated for account creation.",
+        "ExceptionAlreadyLinkedToDifferentAccount": "The remote OAuth user is already linked to another account."
     }
 }
diff --git a/LoginOIDC/lang/fr.json b/LoginOIDC/lang/fr.json
index d96171bdcd7ecdeca57a90e5bc77f46394c7eef4..7820f3b59d4029127e92134d6f160438932ee3ef 100644
--- a/LoginOIDC/lang/fr.json
+++ b/LoginOIDC/lang/fr.json
@@ -1,8 +1,10 @@
 {
     "LoginOIDC": {
-        "SettingDisableSuperuser": "Désactiver la connexion externe des Super Utilisateurs.",
+        "SettingDisableSuperuser": "Désactiver la connexion externe des Super Utilisateurs",
         "SettingDisableSuperuserHelp": "",
-        "SettingAllowSignup": "Créer une nouveau compte utilisateur quand les utilisateurs tentent de se connecter avec un compte OIDC inconnu.",
+        "SettingDisableDirectLoginUrl": "Désactiver l'url de connexion directe",
+        "SettingDisableDirectLoginUrlHelp": "",
+        "SettingAllowSignup": "Créer une nouveau compte utilisateur quand les utilisateurs tentent de se connecter avec un compte OIDC inconnu",
         "SettingAllowSignupHelp": "",
         "SettingBypassTwoFa": "Désactiver le 2è facteur lors d'une connexion avec OIDC",
         "SettingBypassTwoFaHelp": "",
@@ -14,6 +16,8 @@
         "SettingTokenUrlHelp": "ex. https://<USERNAME>.eu.auth0.com/oauth/token",
         "SettingUserinfoUrl": "URL Userinfo",
         "SettingUserinfoUrlHelp": "ex. https://<USERNAME>.eu.auth0.com/userinfo",
+        "SettingEndSessionUrl": "URL Logout",
+        "SettingEndSessionUrlHelp": "",
         "SettingUserinfoId": "ID Userinfo",
         "SettingUserinfoIdHelp": "Nom du champ de l'identifiant unique utilisateur dans la réponse 'userinfo'. Habituellement, pour les services de connexion OpenID Connect comme Auth0, il s'agit de 'sub'. Github fourni l'identifiant utilisateur avec 'id'.",
         "SettingClientId": "Client ID",
@@ -24,6 +28,8 @@
         "SettingScopeHelp": "ex. 'openid' ou 'openid email'",
         "SettingRedirectUriOverride": "Redéfinition de l'URI de redirection",
         "SettingRedirectUriOverrideHelp": "Dans certains cas, il peut être pratique de redéfinir l'URI de redirection qui est transmise au fournisseur OpenID Connect. Si vous êtes n'êtes pas sûr, laissez ce champ vide.",
+        "SettingAllowedSignupDomains": "Limiter la création d'utilisateurs aux domaines",
+        "SettingAllowedSignupDomainsHelp": "",
         "OpenIDConnect": "OpenID Connect",
         "OIDCIntro": "Ceci vous permet de vous connecter à Matomo en utilisant un service d'authentification externe.",
         "AccountLinked": "Votre compte est actuellement lié (Identifiant du compte utilisateur distant : %1$s).",
@@ -36,6 +42,9 @@
         "ExceptionInvalidResponse": "Réponse inattendue du service OAuth.",
         "ExceptionUserNotFoundAndSignupDisabled": "Utilisateur non trouvé. Les nouvelles inscriptions via OAuth sont désactivées.",
         "ExceptionUserNotFoundAndNoEmail": "Utilisateur non trouvé. L'utilisateur n'a pas pu être créé car le service OAuth n'a pas renvoyé d'adresse e-mail.",
-        "ExceptionSuperUserOauthDisabled": "La connexion OAuth pour les Supers Utilisateurs est désactivée."
+        "ExceptionSuperUserOauthDisabled": "La connexion OAuth pour les Supers Utilisateurs est désactivée.",
+        "ExceptionAllowedSignupDomainsValidationFailed": "La validation a échoué pour la liste des domaines autorisés pour la création d'utilisateurs.",
+        "ExceptionAllowedSignupDomainsDenied": "Le domaine n'est pas activé pour la création d'un compte.",
+        "ExceptionAlreadyLinkedToDifferentAccount": "L'utilisateur OAuth distant est déjà lié à un autre compte."
     }
 }
diff --git a/LoginOIDC/plugin.json b/LoginOIDC/plugin.json
index 2e2e2157457b24714760563b4cb817f498edbce8..30ffa9b7a3b2a0b67459effe5298b2375e72dedb 100644
--- a/LoginOIDC/plugin.json
+++ b/LoginOIDC/plugin.json
@@ -1,6 +1,6 @@
 {
     "name": "LoginOIDC",
-    "version": "0.1.5",
+    "version": "4.0.0",
     "description": "Adds support for integrating external authentication services",
     "keywords": ["authentication", "login", "oauth", "openid", "connect", "sso"],
     "license": "GPL v3+",
@@ -13,7 +13,7 @@
         }
     ],
     "require": {
-        "piwik": ">=3.8.0-b4,<4.0.0-b1",
+        "piwik": ">=4.0.0-b1,<5.0.0-b1",
         "php": ">=7.0.0"
     },
     "donate": {