├── .devcontainer ├── activate.sh ├── devcontainer.json ├── local-features │ └── welcome-message │ │ ├── devcontainer-feature.json │ │ └── install.sh └── setup.sh ├── .gitpod.yml ├── .vscode └── tasks.json ├── CHANGELOG.md ├── HOWTO.md ├── README.md ├── SECURITY.md ├── codecov.yml ├── css └── styles-admin.css ├── docker-compose.yml ├── includes ├── functions.php ├── openid-connect-generic-client-wrapper.php ├── openid-connect-generic-client.php ├── openid-connect-generic-login-form.php ├── openid-connect-generic-option-logger.php ├── openid-connect-generic-option-settings.php └── openid-connect-generic-settings-page.php ├── languages └── openid-connect-generic.pot ├── openid-connect-generic.php ├── readme.txt └── wp-cli.yml /.devcontainer/activate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | # Activate the plugin. 6 | cd "/app" 7 | echo "Activating plugin..." 8 | if ! wp plugin is-active daggerhart-openid-connect-generic 2>/dev/null; then 9 | wp plugin activate daggerhart-openid-connect-generic --quiet 10 | fi 11 | 12 | echo "Done!" 13 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, https://containers.dev/implementors/json_reference/. 2 | { 3 | "name": "WordPress Development Environment", 4 | "dockerComposeFile": "../docker-compose.yml", 5 | "service": "app", 6 | "mounts": ["source=dind-var-lib-docker,target=/var/lib/docker,type=volume"], 7 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 8 | 9 | "customizations": { 10 | "vscode": { 11 | // Set *default* container specific settings.json values on container create. 12 | "settings": {}, 13 | 14 | // Add the IDs of extensions you want installed when the container is created. 15 | "extensions": ["ms-azuretools.vscode-docker"] 16 | } 17 | }, 18 | 19 | // Features to add to the dev container. More info: https://containers.dev/features. 20 | "features": { 21 | "./local-features/welcome-message": "latest" 22 | }, 23 | 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | "forwardPorts": [8080, 8081, 8026, 3306], 26 | 27 | // Maps a port number, "host:port" value, range, or regular expression to a set of default options. See port attributes for available options 28 | "portsAttributes": { 29 | "8080": { 30 | "label": "WordPress Development/Testing Site" 31 | }, 32 | "8081": { 33 | "label": "phpMyAdmin" 34 | }, 35 | "8026": { 36 | "label": "MailHog" 37 | }, 38 | "3306": { 39 | "label": "MariaDB" 40 | } 41 | }, 42 | 43 | // Use `onCreateCommand` to run commands as part of the container creation. 44 | //"onCreateCommand": "chmod +x .devcontainer/install.sh && .devcontainer/install.sh", 45 | 46 | // Use 'postCreateCommand' to run commands after the container is created. 47 | "postCreateCommand": "chmod +x .devcontainer/setup.sh && .devcontainer/setup.sh", 48 | 49 | // Use 'postStartCommand' to run commands after the container has started. 50 | "postStartCommand": "chmod +x .devcontainer/activate.sh && .devcontainer/activate.sh", 51 | 52 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 53 | "remoteUser": "wp_php", 54 | 55 | // A set of name-value pairs that sets or overrides environment variables for the devcontainer.json supporting service / tool (or sub-processes like terminals) but not the container as a whole. 56 | "remoteEnv": { "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" } 57 | } 58 | -------------------------------------------------------------------------------- /.devcontainer/local-features/welcome-message/devcontainer-feature.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "welcome-message", 3 | "name": "Install the First Start Welcome Message", 4 | "install": { 5 | "app": "", 6 | "file": "install.sh" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.devcontainer/local-features/welcome-message/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | export DEBIAN_FRONTEND=noninteractive 6 | 7 | # Copy the welcome message 8 | if [ ! -f /usr/local/etc/vscode-dev-containers/first-run-notice.txt ]; then 9 | echo "Installing First Run Notice..." 10 | echo -e "👋 Welcome to \"OpenID Connect for WP Development\" in Dev Containers!\n\n🛠️ Your environment is fully setup with all the required software.\n\n🚀 To get started, wait for the \"postCreateCommand\" to finish setting things up, then open the portforwarded URL and append '/wp/wp-admin'. Login to the WordPress Dashboard using \`admin/password\` for the credentials.\n" | sudo tee /usr/local/etc/vscode-dev-containers/first-run-notice.txt 11 | fi 12 | 13 | echo "Done!" 14 | -------------------------------------------------------------------------------- /.devcontainer/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | # true is shell command and always return 0 6 | # false always return 1 7 | if [ -z "${CODESPACES}" ] ; then 8 | SITE_HOST="http://localhost:8080" 9 | else 10 | SITE_HOST="https://${CODESPACE_NAME}-8080.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" 11 | fi 12 | 13 | PLUGIN_DIR=/workspaces/openid-connect-generic 14 | 15 | # Attempt to make ipv4 traffic have a higher priority than ipv6. 16 | sudo sh -c "echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf" 17 | 18 | # Install Composer dependencies. 19 | cd "${PLUGIN_DIR}" 20 | echo "Installing Composer dependencies..." 21 | COMPOSER_NO_INTERACTION=1 COMPOSER_ALLOW_XDEBUG=0 COMPOSER_MEMORY_LIMIT=-1 composer install --no-progress --quiet 22 | 23 | # Install NPM dependencies. 24 | cd "${PLUGIN_DIR}" 25 | if [ ! -d "node_modules" ]; then 26 | echo "Installing NPM dependencies..." 27 | npm ci 28 | fi 29 | 30 | # Setup the WordPress environment. 31 | cd "/app" 32 | if ! wp core is-installed 2>/dev/null; then 33 | echo "Setting up WordPress at $SITE_HOST" 34 | wp core install --url="$SITE_HOST" --title="OpenID Connect Development" --admin_user="admin" --admin_email="admin@example.com" --admin_password="password" --skip-email --quiet 35 | fi 36 | 37 | echo "Done!" 38 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # List the start up tasks. Learn more https://www.gitpod.io/docs/config-start-tasks/ 2 | tasks: 3 | - name: WordPress Development Environment 4 | init: npm run setup # runs during prebuild 5 | command: | 6 | npm start -- --update 7 | npm stop 8 | npm start -- --update 9 | 10 | # List the ports to expose. Learn more https://www.gitpod.io/docs/config-ports/ 11 | ports: 12 | - port: 8888 13 | onOpen: notify 14 | visibility: public 15 | - port: 8889 16 | onOpen: notify 17 | visibility: public 18 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": [], 12 | "label": "npm: build", 13 | "detail": "npm run grunt build" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # OpenId Connect Generic Changelog 2 | 3 | **3.10.0** 4 | 5 | - Chore: @timnolte - Dependency updates. 6 | - Fix: @drzraf - Prevents running the auth url filter twice. 7 | - Fix: @timnolte - Updates the log cleanup handling to properly retain the configured number of log entries. 8 | - Fix: @timnolte - Updates the log display output to reflect the log retention policy. 9 | - Chore: @timnolte - Adds Unit Testing & New Local Development Environment. 10 | - Feature: @timnolte - Updates logging to allow for tracking processing time. 11 | - Feature: @menno-ll - Adds a remember me feature via a new filter. 12 | - Improvement: @menno-ll - Updates WP Cookie Expiration to Same as Session Length. 13 | 14 | **3.9.1** 15 | 16 | - Improvement: @timnolte - Refactors Composer setup and GitHub Actions. 17 | - Improvement: @timnolte - Bumps WordPress tested version compatibility. 18 | 19 | **3.9.0** 20 | 21 | - Feature: @matchaxnb - Added support for additional configuration constants. 22 | - Feature: @schanzen - Added support for agregated claims. 23 | - Fix: @rkcreation - Fixed access token not updating user metadata after login. 24 | - Fix: @danc1248 - Fixed user creation issue on Multisite Networks. 25 | - Feature: @RobjS - Added plugin singleton to support for more developer customization. 26 | - Feature: @jkouris - Added action hook to allow custom handling of session expiration. 27 | - Fix: @tommcc - Fixed admin CSS loading only on the plugin settings screen. 28 | - Feature: @rkcreation - Added method to refresh the user claim. 29 | - Feature: @Glowsome - Added acr_values support & verification checks that it when defined in options is honored. 30 | - Fix: @timnolte - Fixed regression which caused improper fallback on missing claims. 31 | - Fix: @slykar - Fixed missing query string handling in redirect URL. 32 | - Fix: @timnolte - Fixed issue with some user linking and user creation handling. 33 | - Improvement: @timnolte - Fixed plugin settings typos and screen formatting. 34 | - Security: @timnolte - Updated build tooling security vulnerabilities. 35 | - Improvement: @timnolte - Changed build tooling scripts. 36 | 37 | **3.8.5** 38 | 39 | - Fix: @timnolte - Fixed missing URL request validation before use & ensure proper current page URL is setup for Redirect Back. 40 | - Fix: @timnolte - Fixed Redirect URL Logic to Handle Sub-directory Installs. 41 | - Fix: @timnolte - Fixed issue with redirecting user back when the openid_connect_generic_auth_url shortcode is used. 42 | 43 | **3.8.4** 44 | 45 | - Fix: @timnolte - Fixed invalid State object access for redirection handling. 46 | - Improvement: @timnolte - Fixed local wp-env Docker development environment. 47 | - Improvement: @timnolte - Fixed Composer scripts for linting and static analysis. 48 | 49 | **3.8.3** 50 | 51 | - Fix: @timnolte - Fixed problems with proper redirect handling. 52 | - Improvement: @timnolte - Changes redirect handling to use State instead of cookies. 53 | - Improvement: @timnolte - Refactored additional code to meet coding standards. 54 | 55 | **3.8.2** 56 | 57 | - Fix: @timnolte - Fixed reported XSS vulnerability on WordPress login screen. 58 | 59 | **3.8.1** 60 | 61 | - Fix: @timnolte - Prevent SSO redirect on password protected posts. 62 | - Fix: @timnolte - CI/CD build issues. 63 | - Fix: @timnolte - Invalid redirect handling on logout for Auto Login setting. 64 | 65 | **3.8.0** 66 | 67 | - Feature: @timnolte - Ability to use 6 new constants for setting client configuration instead of storing in the DB. 68 | - Improvement: @timnolte - NPM version requirements for development. 69 | - Improvement: @timnolte - Travis CI build fixes. 70 | - Improvement: @timnolte - GrumPHP configuration updates for code contributions. 71 | - Improvement: @timnolte - Refactored to meet WordPress coding standards. 72 | - Improvement: @timnolte - Refactored to provide localization. 73 | - Improvement: @timnolte - Refactored to provide a Docker-based local development environment. 74 | 75 | **3.7.1** 76 | 77 | - Fix: Release Version Number. 78 | 79 | **3.7.0** 80 | 81 | - Feature: @timnolte - Ability to enable/disable token refresh. Useful for IDPs that don't support token refresh. 82 | - Feature: @timnolte - Support custom redirect URL(`redirect_to`) with the authentication URL & login button shortcodes. 83 | - Supports additional attribute overrides including login `button_text`, `endpoint_login`, `scope`, `redirect_uri`. 84 | 85 | **3.6.0** 86 | 87 | - Improvement: @RobjS - Improved error messages during login state failure. 88 | - Improvement: @RobjS - New developer filter for login form button URL. 89 | - Fix: @cs1m0n - Only increment username during new user creation if the "Link existing user" setting is enabled. 90 | - Fix: @xRy-42 - Allow periods and spaces in usernames to match what WordPress core allows. 91 | - Feature: @benochen - New setting named "Create user if does not exist" determines whether new users are created during login attempts. 92 | - Improvement: @flat235 - Username transliteration and normalization. 93 | 94 | **3.5.1** 95 | 96 | - Fix: @daggerhart - New approach to state management using transients. 97 | 98 | **3.5.0** 99 | 100 | - Readme fix: @thijskh - Fix syntax error in example openid-connect-generic-login-button-text 101 | - Feature: @slavicd - Allow override of the plugin by posting credentials to wp-login.php 102 | - Feature: @gassan - New action on use login 103 | - Fix: @daggerhart - Avoid double question marks in auth url query string 104 | - Fix: @drzraf - wp-cli bootstrap must not inhibit custom rewrite rules 105 | - Syntax change: @mullikine - Change PHP keywords to comply with PSR2 106 | 107 | **3.4.1** 108 | 109 | - Minor documentation update and additional error checking. 110 | 111 | **3.4.0** 112 | 113 | - Feature: @drzraf - New filter hook: ability to filter claim and derived user data before user creation. 114 | - Feature: @anttileppa - State time limit can now be changed on the settings page. 115 | - Fix: @drzraf - Fix PHP notice when using traditional login, $token_response may be empty. 116 | - Fix: @drzraf - Fixed a notice when cookie does not contain expected redirect_url 117 | 118 | **3.3.1** 119 | 120 | - Prefixing classes for more efficient autoloading. 121 | - Avoid altering global wp_remote_post() parameters. 122 | - Minor metadata updates for wp.org 123 | 124 | **3.3.0** 125 | 126 | - Fix: @pjeby - Handle multiple user sessions better by using the `WP_Session_Tokens` object. Predecessor to fixes for multiple other issues: #49, #50, #51 127 | 128 | **3.2.1** 129 | 130 | - Bug fix: @svenvanhal - Exit after issuing redirect. Fixes #46 131 | 132 | **3.2.0** 133 | 134 | - Feature: @robbiepaul - trigger core action `wp_login` when user is logged in through this plugin 135 | - Feature: @moriyoshi - Determine the WP_User display name with replacement tokens on the settings page. Tokens can be any property of the user_claim. 136 | - Feature: New setting to set redirect URL when session expires. 137 | - Feature: @robbiepaul - New filter for modifying authentication URL 138 | - Fix: @cedrox - Adding id_token_hint to logout URL according to spec 139 | - Bug fix: Provide port to the request header when requesting the user_claim 140 | 141 | **3.1.0** 142 | 143 | - Feature: @rwasef1830 - Refresh tokens 144 | - Feature: @rwasef1830 - Integrated logout support with end_session endpoint 145 | - Feature: May use an alternate redirect_uri that doesn't rely on admin-ajax 146 | - Feature: @ahatherly - Support for IDP behind reverse proxy 147 | - Bug fix: @robertstaddon - case insensitive check for Bearer token 148 | - Bug fix: @rwasef1830 - "redirect to origin when auto-sso" cookie issue 149 | - Bug fix: @rwasef1830 - PHP Warnings headers already sent due to attempts to redirect and set cookies during login form message 150 | - Bug fix: @rwasef1830 - expire session when access_token expires if no refresh token found 151 | - UX fix: @rwasef1830 - Show login button on error redirect when using auto-sso 152 | 153 | **3.0.8** 154 | 155 | - Feature: @wgengarelly - Added `openid-connect-generic-update-user-using-current-claim` action hook allowing other plugins/themes 156 | to take action using the fresh claims received when an existing user logs in. 157 | 158 | **3.0.7** 159 | 160 | - Bug fix: @wgengarelly - When requesting userinfo, send the access token using the Authorization header field as recommended in 161 | section 5.3.1 of the specs. 162 | 163 | **3.0.6** 164 | 165 | - Bug fix: @robertstaddon - If "Link Existing Users" is enabled, allow users who login with OpenID Connect to also log in with WordPress credentials 166 | 167 | **3.0.5** 168 | 169 | - Feature: @robertstaddon - Added `[openid_connect_generic_login_button]` shortcode to allow the login button to be placed anywhere 170 | - Feature: @robertstaddon - Added setting to "Redirect Back to Origin Page" after a successful login instead of redirecting to the home page. 171 | 172 | **3.0.4** 173 | 174 | - Feature: @robertstaddon - Added setting to allow linking existing WordPress user accounts with newly-authenticated OpenID Connect login 175 | 176 | **3.0.3** 177 | 178 | - Using WordPresss's is_ssl() for setcookie()'s "secure" parameter 179 | - Bug fix: Incrementing username in case of collision. 180 | - Bug fix: Wrong error sent when missing token body 181 | 182 | **3.0.2** 183 | 184 | - Added http_request_timeout setting 185 | 186 | **3.0.1** 187 | 188 | - Finalizing 3.0.x api 189 | 190 | **3.0** 191 | 192 | - Complete rewrite to separate concerns 193 | - Changed settings keys for clarity (requires updating settings if upgrading from another version) 194 | - Error logging 195 | 196 | **2.1** 197 | 198 | - Working my way closer to spec. Possible breaking change. Now checking for preferred_username as priority. 199 | - New username determination to avoid collisions 200 | 201 | **2.0** 202 | 203 | Complete rewrite 204 | -------------------------------------------------------------------------------- /HOWTO.md: -------------------------------------------------------------------------------- 1 | # OpenID Connect Generic Client 2 | 3 | License: GPLv2 or later 4 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 5 | 6 | A simple client that provides SSO or opt-in authentication against a generic OAuth2 Server implementation. 7 | 8 | ## Description 9 | 10 | This plugin allows to authenticate users against OpenID Connect OAuth2 API with Authorization Code Flow. 11 | Once installed, it can be configured to automatically authenticate users (SSO), or provide a "Login with OpenID Connect" 12 | button on the login form. After consent has been obtained, an existing user is automatically logged into WordPress, while 13 | new users are created in WordPress database. 14 | 15 | Much of the documentation can be found on the Settings > OpenID Connect Generic dashboard page. 16 | 17 | ## Table of Contents 18 | 19 | - [Installation](#installation) 20 | - [Composer](#composer) 21 | - [Frequently Asked Questions](#frequently-asked-questions) 22 | - [What is the client's Redirect URI?](#what-is-the-clients-redirect-uri) 23 | - [Can I change the client's Redirect URI?](#can-i-change-the-clients-redirect-uri) 24 | - [Configuration Environment Variables/Constants](#configuration-environment-variables-constants) 25 | - [Hooks](#hooks) 26 | - [Filters](#filters) 27 | - [openid-connect-generic-alter-request](#openid-connect-generic-alter-request) 28 | - [openid-connect-generic-login-button-text](#openid-connect-generic-login-button-text) 29 | - [openid-connect-generic-auth-url](#openid-connect-generic-auth-url) 30 | - [openid-connect-generic-user-login-test](#openid-connect-generic-user-login-test) 31 | - [openid-connect-generic-user-creation-test](#openid-connect-generic-user-creation-test) 32 | - [openid-connect-generic-alter-user-claim](#openid-connect-generic-alter-user-claim) 33 | - [openid-connect-generic-alter-user-data](#openid-connect-generic-alter-user-data) 34 | - [openid-connect-generic-settings-fields](#openid-connect-generic-settings-fields) 35 | - [Actions](#actions) 36 | - [openid-connect-generic-user-create](#openid-connect-generic-user-create) 37 | - [openid-connect-generic-user-update](#openid-connect-generic-user-update) 38 | - [openid-connect-generic-update-user-using-current-claim](#openid-connect-generic-update-user-using-current-claim) 39 | - [openid-connect-generic-redirect-user-back](#openid-connect-generic-redirect-user-back) 40 | 41 | 42 | ## Installation 43 | 44 | 1. Upload to the `/wp-content/plugins/` directory 45 | 1. Activate the plugin 46 | 1. Visit Settings > OpenID Connect and configure to meet your needs 47 | 48 | ### Composer 49 | 50 | [OpenID Connect Generic on packagist](https://packagist.org/packages/daggerhart/openid-connect-generic) 51 | 52 | Installation: 53 | 54 | `composer require daggerhart/openid-connect-generic` 55 | 56 | 57 | ## Frequently Asked Questions 58 | 59 | ### What is the client's Redirect URI? 60 | 61 | Most OAuth2 servers should require a whitelist of redirect URIs for security purposes. The Redirect URI provided 62 | by this client is like so: `https://example.com/wp-admin/admin-ajax.php?action=openid-connect-authorize` 63 | 64 | Replace `example.com` with your domain name and path to WordPress. 65 | 66 | ### Can I change the client's Redirect URI? 67 | 68 | Some OAuth2 servers do not allow for a client redirect URI to contain a query string. The default URI provided by 69 | this module leverages WordPress's `admin-ajax.php` endpoint as an easy way to provide a route that does not include 70 | HTML, but this will naturally involve a query string. Fortunately, this plugin provides a setting that will make use of 71 | an alternate redirect URI that does not include a query string. 72 | 73 | On the settings page for this plugin (Dashboard > Settings > OpenID Connect Generic) there is a checkbox for 74 | **Alternate Redirect URI**. When checked, the plugin will use the Redirect URI 75 | `https://example.com/openid-connect-authorize`. 76 | 77 | ## Configuration Environment Variables/Constants 78 | 79 | - Client ID: `OIDC_CLIENT_ID` 80 | - Client Secret Key: `OIDC_CLIENT_SECRET` 81 | - Login Endpoint URL: `OIDC_ENDPOINT_LOGIN_URL` 82 | - Userinfo Endpoint URL: `OIDC_ENDPOINT_USERINFO_URL` 83 | - Token Validation Endpoint URL: `OIDC_ENDPOINT_TOKEN_URL` 84 | - End Session Endpoint URL: `OIDC_ENDPOINT_LOGOUT_URL` 85 | - OpenID scope: `OIDC_CLIENT_SCOPE` (space separated) 86 | - OpenID login type: `OIDC_LOGIN_TYPE` ('button' or 'auto') 87 | - Enforce privacy: `OIDC_ENFORCE_PRIVACY` (boolean) 88 | - Create user if they do not exist: `OIDC_CREATE_IF_DOES_NOT_EXIST` (boolean) 89 | - Link existing user: `OIDC_LINK_EXISTING_USERS` (boolean) 90 | - Redirect user back to origin page: `OIDC_REDIRECT_USER_BACK` (boolean) 91 | - Redirect on logout: `OIDC_REDIRECT_ON_LOGOUT` (boolean) 92 | 93 | ## Hooks 94 | 95 | This plugin provides a number of hooks to allow for a significant amount of customization of the plugin operations from 96 | elsewhere in the WordPress system. 97 | 98 | ### Filters 99 | 100 | Filters are WordPress hooks that are used to modify data. The first argument in a filter hook is always expected to be 101 | returned at the end of the hook. 102 | 103 | WordPress filters API - [`add_filter()`](https://developer.wordpress.org/reference/functions/add_filter/) and 104 | [`apply_filters()`](https://developer.wordpress.org/reference/functions/apply_filters/). 105 | 106 | Most often you'll only need to use `add_filter()` to hook into this plugin's code. 107 | 108 | #### `openid-connect-generic-alter-request` 109 | 110 | Hooks directly into client before requests are sent to the OpenID Server. 111 | 112 | Provides 2 arguments: the request array being sent to the server, and the operation currently being executed by this 113 | plugin. 114 | 115 | Possible operations: 116 | 117 | - get-authentication-token 118 | - refresh-token 119 | - get-userinfo 120 | 121 | ``` 122 | add_filter('openid-connect-generic-alter-request', function( $request, $operation ) { 123 | if ( $operation == 'get-authentication-token' ) { 124 | $request['some_key'] = 'modified value'; 125 | } 126 | 127 | return $request; 128 | }, 10, 2); 129 | ``` 130 | 131 | #### `openid-connect-generic-login-button-text` 132 | 133 | Modify the login button text. Default value is `__( 'Login with OpenID Connect' )`. 134 | 135 | Provides 1 argument: the current login button text. 136 | 137 | ``` 138 | add_filter('openid-connect-generic-login-button-text', function( $text ) { 139 | $text = __('Login to my super cool IDP server'); 140 | 141 | return $text; 142 | }); 143 | ``` 144 | 145 | #### `openid-connect-generic-auth-url` 146 | 147 | Modify the authentication URL before presented to the user. This is the URL that will send the user to the IDP server 148 | for login. 149 | 150 | Provides 1 argument: the plugin generated URL. 151 | 152 | ``` 153 | add_filter('openid-connect-generic-auth-url', function( $url ) { 154 | // Add some custom data to the url. 155 | $url.= '&my_custom_data=123abc'; 156 | return $url; 157 | }); 158 | ``` 159 | 160 | #### `openid-connect-generic-user-login-test` 161 | 162 | Determine whether or not the user should be logged into WordPress. 163 | 164 | Provides 2 arguments: the boolean result of the test (default `TRUE`), and the `$user_claim` array from the server. 165 | 166 | ``` 167 | add_filter('openid-connect-generic-user-login-test', function( $result, $user_claim ) { 168 | // Don't let Terry login. 169 | if ( $user_claim['email'] == 'terry@example.com' ) { 170 | $result = FALSE; 171 | } 172 | 173 | return $result; 174 | }, 10, 2); 175 | ``` 176 | 177 | #### `openid-connect-generic-user-creation-test` 178 | 179 | Determine whether or not the user should be created. This filter is called when a new user is trying to login and they 180 | do not currently exist within WordPress. 181 | 182 | Provides 2 arguments: the boolean result of the test (default `TRUE`), and the `$user_claim` array from the server. 183 | 184 | ``` 185 | add_filter('', function( $result, $user_claim ) { 186 | // Don't let anyone from example.com create an account. 187 | $email_array = explode( '@', $user_claim['email'] ); 188 | if ( $email_array[1] == 'example.com' ) { 189 | $result = FALSE; 190 | } 191 | 192 | return $result; 193 | }, 10, 2) 194 | ``` 195 | 196 | #### `openid-connect-generic-alter-user-claim` 197 | 198 | Modify the `$user_claim` before the plugin builds the `$user_data` array for new user created. 199 | 200 | **Deprecated** - This filter is not very useful due to some changes that were added later. Recommend not using this 201 | filter, and using the `openid-connect-generic-alter-user-data` filter instead. Practically, you can only change the 202 | user's `first_name` and `last_name` values with this filter, but you could easily do that in 203 | `openid-connect-generic-alter-user-data` as well. 204 | 205 | Provides 1 argument: the `$user_claim` from the server. 206 | 207 | ``` 208 | // Not a great example because the hook isn't very useful. 209 | add_filter('openid-connect-generic-alter-user-claim', function( $user_claim ) { 210 | // Use the beginning of the user's email address as the user's first name. 211 | if ( empty( $user_claim['given_name'] ) ) { 212 | $email_array = explode( '@', $user_claim['email'] ); 213 | $user_claim['given_name'] = $email_array[0]; 214 | } 215 | 216 | return $user_claim; 217 | }); 218 | ``` 219 | 220 | #### `openid-connect-generic-alter-user-data` 221 | 222 | Modify a new user's data immediately before the user is created. 223 | 224 | Provides 2 arguments: the `$user_data` array that will be sent to `wp_insert_user()`, and the `$user_claim` from the 225 | server. 226 | 227 | ``` 228 | add_filter('openid-connect-generic-alter-user-data', function( $user_data, $user_claim ) { 229 | // Don't register any user with their real email address. Create a fake internal address. 230 | if ( !empty( $user_data['user_email'] ) ) { 231 | $email_array = explode( '@', $user_data['user_email'] ); 232 | $email_array[1] = 'my-fake-domain.co'; 233 | $user_data['user_email'] = implode( '@', $email_array ); 234 | } 235 | 236 | return $user_data; 237 | }, 10, 2); 238 | ``` 239 | 240 | #### `openid-connect-generic-settings-fields` 241 | 242 | For extending the plugin with a new setting field (found on Dashboard > Settings > OpenID Connect Generic) that the site 243 | administrator can modify. Also useful to alter the existing settings fields. 244 | 245 | See `/includes/openid-connect-generic-settings-page.php` for how fields are constructed. 246 | 247 | New settings fields will be automatically saved into the wp_option for this plugin's settings, and will be available in 248 | the `\OpenID_Connect_Generic_Option_Settings` object this plugin uses. 249 | 250 | **Note:** It can be difficult to get a copy of the settings from within other hooks. The easiest way to make use of 251 | settings in your custom hooks is to call 252 | `$settings = get_option('openid_connect_generic_settings', array());`. 253 | 254 | Provides 1 argument: the existing fields array. 255 | 256 | ``` 257 | add_filter('openid-connect-generic-settings-fields', function( $fields ) { 258 | 259 | // Modify an existing field's title. 260 | $fields['endpoint_userinfo']['title'] = __('User information endpoint url'); 261 | 262 | // Add a new field that is a simple checkbox. 263 | $fields['block_terry'] = array( 264 | 'title' => __('Block Terry'), 265 | 'description' => __('Prevent Terry from logging in'), 266 | 'type' => 'checkbox', 267 | 'section' => 'authorization_settings', 268 | ); 269 | 270 | // A select field that provides options. 271 | 272 | $fields['deal_with_terry'] = array( 273 | 'title' => __('Manage Terry'), 274 | 'description' => __('How to deal with Terry when he tries to log in.'), 275 | 'type' => 'select', 276 | 'options' => array( 277 | 'allow' => __('Allow login'), 278 | 'block' => __('Block'), 279 | 'redirect' => __('Redirect'), 280 | ), 281 | 'section' => 'authorization_settings', 282 | ); 283 | 284 | return $fields; 285 | }); 286 | ``` 287 | "Sections" are where your setting appears on the admin settings page. Keys for settings sections: 288 | 289 | - client_settings 290 | - user_settings 291 | - authorization_settings 292 | - log_settings 293 | 294 | Field types: 295 | 296 | - text 297 | - checkbox 298 | - select (requires an array of "options") 299 | 300 | ### Actions 301 | 302 | WordPress actions are generic events that other plugins can react to. 303 | 304 | Actions API: [`add_action`](https://developer.wordpress.org/reference/functions/add_action/) and [`do_actions`](https://developer.wordpress.org/reference/functions/do_action/) 305 | 306 | You'll probably only ever want to use `add_action` when hooking into this plugin. 307 | 308 | #### `openid-connect-generic-user-create` 309 | 310 | React to a new user being created by this plugin. 311 | 312 | Provides 2 arguments: the `\WP_User` object that was created, and the `$user_claim` from the IDP server. 313 | 314 | ``` 315 | add_action('openid-connect-generic-user-create', function( $user, $user_claim ) { 316 | // Send the user an email when their account is first created. 317 | wp_mail( 318 | $user->user_email, 319 | __('Welcome to my web zone'), 320 | "Hi {$user->first_name},\n\nYour account has been created at my cool website.\n\n Enjoy!" 321 | ); 322 | }, 10, 2); 323 | ``` 324 | 325 | #### `openid-connect-generic-user-update` 326 | 327 | React to the user being updated after login. This is the event that happens when a user logins and they already exist as 328 | a user in WordPress, as opposed to a new WordPress user being created. 329 | 330 | Provides 1 argument: the user's WordPress user ID. 331 | 332 | ``` 333 | add_action('openid-connect-generic-user-update', function( $uid ) { 334 | // Keep track of the number of times the user has logged into the site. 335 | $login_count = get_user_meta( $uid, 'my-user-login-count', TRUE); 336 | $login_count += 1; 337 | add_user_meta( $uid, 'my-user-login-count', $login_count, TRUE); 338 | }); 339 | ``` 340 | 341 | #### `openid-connect-generic-update-user-using-current-claim` 342 | 343 | React to an existing user logging in (after authentication and authorization). 344 | 345 | Provides 2 arguments: the `WP_User` object, and the `$user_claim` provided by the IDP server. 346 | 347 | ``` 348 | add_action('openid-connect-generic-update-user-using-current-claim', function( $user, $user_claim) { 349 | // Based on some data in the user_claim, modify the user. 350 | if ( !empty( $user_claim['wp_user_role'] ) ) { 351 | if ( $user_claim['wp_user_role'] == 'should-be-editor' ) { 352 | $user->set_role( 'editor' ); 353 | } 354 | } 355 | }, 10, 2); 356 | ``` 357 | 358 | #### `openid-connect-generic-redirect-user-back` 359 | 360 | React to a user being redirected after a successful login. This hook is the last hook that will fire when a user logs 361 | in. It will only fire if the plugin setting "Redirect Back to Origin Page" is enabled at Dashboard > Settings > 362 | OpenID Connect Generic. It will fire for both new and existing users. 363 | 364 | Provides 2 arguments: the url where the user will be redirected, and the `WP_User` object. 365 | 366 | ``` 367 | add_action('openid-connect-generic-redirect-user-back', function( $redirect_url, $user ) { 368 | // Take over the redirection complete. Send users somewhere special based on their capabilities. 369 | if ( $user->has_cap( 'edit_users' ) ) { 370 | wp_redirect( admin_url( 'users.php' ) ); 371 | exit(); 372 | } 373 | }, 10, 2); 374 | ``` 375 | 376 | ### User Meta Data 377 | 378 | This plugin stores meta data about the user for both practical and debugging purposes. 379 | 380 | * `openid-connect-generic-subject-identity` - The identity of the user provided by the IDP server. 381 | * `openid-connect-generic-last-id-token-claim` - The user's most recent `id_token` claim, decoded and stored as an array. 382 | * `openid-connect-generic-last-user-claim` - The user's most recent `user_claim`, stored as an array. 383 | * `openid-connect-generic-last-token-response` - The user's most recent `token_response`, stored as an array. 384 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenID Connect Generic Client # 2 | **Contributors:** [daggerhart](https://profiles.wordpress.org/daggerhart/), [tnolte](https://profiles.wordpress.org/tnolte/) 3 | **Donate link:** http://www.daggerhart.com/ 4 | **Tags:** security, login, oauth2, openidconnect, apps, authentication, autologin, sso 5 | **Requires at least:** 5.0 6 | **Tested up to:** 6.4.3 7 | **Stable tag:** 3.10.0 8 | **Requires PHP:** 7.4 9 | **License:** GPLv2 or later 10 | **License URI:** http://www.gnu.org/licenses/gpl-2.0.html 11 | 12 | A simple client that provides SSO or opt-in authentication against a generic OAuth2 Server implementation. 13 | 14 | ## Description ## 15 | 16 | This plugin allows to authenticate users against OpenID Connect OAuth2 API with Authorization Code Flow. 17 | Once installed, it can be configured to automatically authenticate users (SSO), or provide a "Login with OpenID Connect" 18 | button on the login form. After consent has been obtained, an existing user is automatically logged into WordPress, while 19 | new users are created in WordPress database. 20 | 21 | Much of the documentation can be found on the Settings > OpenID Connect Generic dashboard page. 22 | 23 | Please submit issues to the Github repo: https://github.com/daggerhart/openid-connect-generic 24 | 25 | ## Installation ## 26 | 27 | 1. Upload to the `/wp-content/plugins/` directory 28 | 1. Activate the plugin 29 | 1. Visit Settings > OpenID Connect and configure to meet your needs 30 | 31 | ## Frequently Asked Questions ## 32 | 33 | ### What is the client's Redirect URI? ### 34 | 35 | Most OAuth2 servers will require whitelisting a set of redirect URIs for security purposes. The Redirect URI provided 36 | by this client is like so: https://example.com/wp-admin/admin-ajax.php?action=openid-connect-authorize 37 | 38 | Replace `example.com` with your domain name and path to WordPress. 39 | 40 | ### Can I change the client's Redirect URI? ### 41 | 42 | Some OAuth2 servers do not allow for a client redirect URI to contain a query string. The default URI provided by 43 | this module leverages WordPress's `admin-ajax.php` endpoint as an easy way to provide a route that does not include 44 | HTML, but this will naturally involve a query string. Fortunately, this plugin provides a setting that will make use of 45 | an alternate redirect URI that does not include a query string. 46 | 47 | On the settings page for this plugin (Dashboard > Settings > OpenID Connect Generic) there is a checkbox for 48 | **Alternate Redirect URI**. When checked, the plugin will use the Redirect URI 49 | `https://example.com/openid-connect-authorize`. 50 | 51 | 52 | ## Changelog ## 53 | 54 | ### 3.10.0 ### 55 | 56 | * Chore: @timnolte - Dependency updates. 57 | * Fix: @drzraf - Prevents running the auth url filter twice. 58 | * Fix: @timnolte - Updates the log cleanup handling to properly retain the configured number of log entries. 59 | * Fix: @timnolte - Updates the log display output to reflect the log retention policy. 60 | * Chore: @timnolte - Adds Unit Testing & New Local Development Environment. 61 | * Feature: @timnolte - Updates logging to allow for tracking processing time. 62 | * Feature: @menno-ll - Adds a remember me feature via a new filter. 63 | * Improvement: @menno-ll - Updates WP Cookie Expiration to Same as Session Length. 64 | 65 | ### 3.9.1 ### 66 | 67 | * Improvement: @timnolte - Refactors Composer setup and GitHub Actions. 68 | * Improvement: @timnolte - Bumps WordPress tested version compatibility. 69 | 70 | ### 3.9.0 ### 71 | 72 | * Feature: @matchaxnb - Added support for additional configuration constants. 73 | * Feature: @schanzen - Added support for agregated claims. 74 | * Fix: @rkcreation - Fixed access token not updating user metadata after login. 75 | * Fix: @danc1248 - Fixed user creation issue on Multisite Networks. 76 | * Feature: @RobjS - Added plugin singleton to support for more developer customization. 77 | * Feature: @jkouris - Added action hook to allow custom handling of session expiration. 78 | * Fix: @tommcc - Fixed admin CSS loading only on the plugin settings screen. 79 | * Feature: @rkcreation - Added method to refresh the user claim. 80 | * Feature: @Glowsome - Added acr_values support & verification checks that it when defined in options is honored. 81 | * Fix: @timnolte - Fixed regression which caused improper fallback on missing claims. 82 | * Fix: @slykar - Fixed missing query string handling in redirect URL. 83 | * Fix: @timnolte - Fixed issue with some user linking and user creation handling. 84 | * Improvement: @timnolte - Fixed plugin settings typos and screen formatting. 85 | * Security: @timnolte - Updated build tooling security vulnerabilities. 86 | * Improvement: @timnolte - Changed build tooling scripts. 87 | 88 | ### 3.8.5 ### 89 | 90 | * Fix: @timnolte - Fixed missing URL request validation before use & ensure proper current page URL is setup for Redirect Back. 91 | * Fix: @timnolte - Fixed Redirect URL Logic to Handle Sub-directory Installs. 92 | * Fix: @timnolte - Fixed issue with redirecting user back when the openid_connect_generic_auth_url shortcode is used. 93 | 94 | ### 3.8.4 ### 95 | 96 | * Fix: @timnolte - Fixed invalid State object access for redirection handling. 97 | * Improvement: @timnolte - Fixed local wp-env Docker development environment. 98 | * Improvement: @timnolte - Fixed Composer scripts for linting and static analysis. 99 | 100 | ### 3.8.3 ### 101 | 102 | * Fix: @timnolte - Fixed problems with proper redirect handling. 103 | * Improvement: @timnolte - Changes redirect handling to use State instead of cookies. 104 | * Improvement: @timnolte - Refactored additional code to meet coding standards. 105 | 106 | ### 3.8.2 ### 107 | 108 | * Fix: @timnolte - Fixed reported XSS vulnerability on WordPress login screen. 109 | 110 | ### 3.8.1 ### 111 | 112 | * Fix: @timnolte - Prevent SSO redirect on password protected posts. 113 | * Fix: @timnolte - CI/CD build issues. 114 | * Fix: @timnolte - Invalid redirect handling on logout for Auto Login setting. 115 | 116 | ### 3.8.0 ### 117 | 118 | * Feature: @timnolte - Ability to use 6 new constants for setting client configuration instead of storing in the DB. 119 | * Improvement: @timnolte - Plugin development & contribution updates. 120 | * Improvement: @timnolte - Refactored to meet WordPress coding standards. 121 | * Improvement: @timnolte - Refactored to provide localization. 122 | 123 | -------- 124 | 125 | [See the previous changelogs here](https://github.com/oidc-wp/openid-connect-generic/blob/main/CHANGELOG.md#changelog) 126 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We follow the [WordPress Core style of versioning](https://make.wordpress.org/core/handbook/about/release-cycle/version-numbering/) rather than traditional [SemVer](https://semver.org/). This means that a move from version 3.9 to 4.0 is no different from a move from version 3.8 to 3.9. When a **PATCH** version is released it represents a bug fix, or non-code, only change. 6 | 7 | The latest version released is the only version that will receive security updates, generally as a **PATCH** release unless a security issue requires a functionality change in which requires a minor/major version bump. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | For security reasons, the following are acceptable options for reporting all security issues. 12 | 13 | 1. Via Keybase secure message to [timnolte](https://keybase.io/timnolte/chat) or [daggerhart](https://keybase.io/daggerhart/chat). 14 | 2. Send a DM via the [WordPress Slack](https://make.wordpress.org/chat/) to `tnolte`. 15 | 3. Via a private [security advisory](https://github.com/oidc-wp/openid-connect-generic/security/advisories) notice. 16 | 17 | Please disclose responsibly and not via public GitHub Issues (which allows for exploiting issues in the wild before the patch is released). 18 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 0.5% 7 | patch: off 8 | 9 | comment: 10 | require_changes: true 11 | -------------------------------------------------------------------------------- /css/styles-admin.css: -------------------------------------------------------------------------------- 1 | #logger-table .col-data { 2 | width: 85% 3 | } 4 | 5 | #logger-table .col-data pre { 6 | margin: 0; 7 | white-space: pre; /* CSS 2.0 */ 8 | white-space: pre-wrap; /* CSS 2.1 */ 9 | white-space: pre-line; /* CSS 3.0 */ 10 | white-space: -pre-wrap; /* Opera 4-6 */ 11 | white-space: -o-pre-wrap; /* Opera 7 */ 12 | white-space: -moz-pre-wrap; /* Mozilla */ 13 | white-space: -hp-pre-wrap; /* HP Printers */ 14 | word-wrap: break-word; /* IE 5+ */ 15 | } 16 | 17 | #logger-table .col-details { 18 | width: 200px; 19 | } 20 | 21 | #logger-table .col-details div { 22 | padding: 4px 0; 23 | border-bottom: 1px solid #bbb; 24 | } 25 | 26 | #logger-table .col-details div:last-child { 27 | border-bottom: none; 28 | } 29 | 30 | #logger-table .col-details label { 31 | font-weight: bold; 32 | } 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This is the Compose file for command-line services. 2 | # Anything that doesn't need to be run as part of the main `docker-compose up' 3 | # command should reside in here and be invoked by a helper script. 4 | version: "3.7" 5 | 6 | services: 7 | app: 8 | image: ghcr.io/ndigitals/wp-dev-container:php-8.0-node-16 9 | restart: always 10 | depends_on: 11 | - db 12 | - phpmyadmin 13 | - web 14 | - mailhog 15 | working_dir: /workspaces/openid-connect-generic 16 | environment: &env 17 | WORDPRESS_DB_HOST: db 18 | WORDPRESS_DB_NAME: wordpress 19 | WORDPRESS_DB_USER: wordpress 20 | WORDPRESS_DB_PASSWORD: wordpress 21 | WORDPRESS_TEST_DB_NAME: wordpress_test 22 | CODESPACES: "${CODESPACES}" 23 | CODESPACE_NAME: "${CODESPACE_NAME}" 24 | GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN: "${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" 25 | volumes: 26 | - .:/workspaces/openid-connect-generic:cached 27 | - ./tools/local-env:/app:cached 28 | - ./tools/php/php-cli.ini:/usr/local/etc/php/php-cli.ini:ro,cached 29 | - .:/app/wp-content/plugins/daggerhart-openid-connect-generic:ro,cached 30 | - ~/.composer:/root/.composer:cached 31 | - ~/.npm:/root/.npm:cached 32 | networks: 33 | - oidcwp-net 34 | 35 | web: 36 | image: httpd 37 | restart: unless-stopped 38 | depends_on: 39 | - db 40 | ports: 41 | - 8080:80 42 | environment: 43 | <<: *env 44 | volumes: 45 | - ./tools/local-env:/app:cached 46 | - .:/app/wp-content/plugins/daggerhart-openid-connect-generic:ro,cached 47 | - ./tools/apache/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro,cached 48 | networks: 49 | - oidcwp-net 50 | 51 | db: 52 | image: mariadb 53 | restart: unless-stopped 54 | ports: 55 | - 3306:3306 56 | environment: 57 | MYSQL_ROOT_PASSWORD: password 58 | MYSQL_DATABASE: wordpress 59 | MYSQL_USER: wordpress 60 | MYSQL_PASSWORD: wordpress 61 | volumes: 62 | - db:/var/lib/mysql 63 | - ./tests/db-wordpress_test.sql:/docker-entrypoint-initdb.d/db-wordpress_test.sql 64 | networks: 65 | - oidcwp-net 66 | 67 | phpmyadmin: 68 | image: phpmyadmin 69 | restart: unless-stopped 70 | depends_on: 71 | - db 72 | ports: 73 | - 8081:8081 74 | environment: 75 | PMA_HOST: db 76 | APACHE_PORT: 8081 77 | networks: 78 | - oidcwp-net 79 | 80 | ## SMTP Server + Web Interface for viewing and testing emails during development. 81 | mailhog: 82 | image: mailhog/mailhog 83 | restart: unless-stopped 84 | ports: 85 | - 1025:1025 # smtp server 86 | - 8026:8025 # web ui 87 | networks: 88 | - oidcwp-net 89 | 90 | volumes: 91 | db: 92 | 93 | networks: 94 | oidcwp-net: 95 | 96 | -------------------------------------------------------------------------------- /includes/functions.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015-2020 daggerhart 8 | * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+ 9 | */ 10 | 11 | /** 12 | * Return a single use authentication URL. 13 | * 14 | * @return string 15 | */ 16 | function oidcg_get_authentication_url() { 17 | return \OpenID_Connect_Generic::instance()->client_wrapper->get_authentication_url(); 18 | } 19 | 20 | /** 21 | * Refresh a user claim and update the user metadata. 22 | * 23 | * @param WP_User $user The user object. 24 | * @param array $token_response The token response. 25 | * 26 | * @return WP_Error|array 27 | */ 28 | function oidcg_refresh_user_claim( $user, $token_response ) { 29 | return \OpenID_Connect_Generic::instance()->client_wrapper->refresh_user_claim( $user, $token_response ); 30 | } 31 | -------------------------------------------------------------------------------- /includes/openid-connect-generic-client-wrapper.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2015-2020 daggerhart 9 | * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+ 10 | */ 11 | 12 | /** 13 | * OpenID_Connect_Generic_Client_Wrapper class. 14 | * 15 | * Plugin OIDC/oAuth client wrapper class. 16 | * 17 | * @package OpenID_Connect_Generic 18 | * @category Authentication 19 | */ 20 | class OpenID_Connect_Generic_Client_Wrapper { 21 | 22 | /** 23 | * The user redirect cookie key. 24 | * 25 | * @deprecated Redirection should be done via state transient and not cookies. 26 | * 27 | * @var string 28 | */ 29 | const COOKIE_REDIRECT_KEY = 'openid-connect-generic-redirect'; 30 | 31 | /** 32 | * The token refresh info cookie key. 33 | * 34 | * @var string 35 | */ 36 | const COOKIE_TOKEN_REFRESH_KEY = 'openid-connect-generic-refresh'; 37 | 38 | /** 39 | * The client object instance. 40 | * 41 | * @var OpenID_Connect_Generic_Client 42 | */ 43 | private $client; 44 | 45 | /** 46 | * The settings object instance. 47 | * 48 | * @var OpenID_Connect_Generic_Option_Settings 49 | */ 50 | private $settings; 51 | 52 | /** 53 | * The logger object instance. 54 | * 55 | * @var OpenID_Connect_Generic_Option_Logger 56 | */ 57 | private $logger; 58 | 59 | /** 60 | * The return error onject. 61 | * 62 | * @example WP_Error if there was a problem, or false if no error 63 | * 64 | * @var bool|WP_Error 65 | */ 66 | private $error = false; 67 | 68 | /** 69 | * Inject necessary objects and services into the client. 70 | * 71 | * @param OpenID_Connect_Generic_Client $client A plugin client object instance. 72 | * @param OpenID_Connect_Generic_Option_Settings $settings A plugin settings object instance. 73 | * @param OpenID_Connect_Generic_Option_Logger $logger A plugin logger object instance. 74 | */ 75 | public function __construct( OpenID_Connect_Generic_Client $client, OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ) { 76 | $this->client = $client; 77 | $this->settings = $settings; 78 | $this->logger = $logger; 79 | } 80 | 81 | /** 82 | * Hook the client into WordPress. 83 | * 84 | * @param \OpenID_Connect_Generic_Client $client The plugin client instance. 85 | * @param \OpenID_Connect_Generic_Option_Settings $settings The plugin settings instance. 86 | * @param \OpenID_Connect_Generic_Option_Logger $logger The plugin logger instance. 87 | * 88 | * @return \OpenID_Connect_Generic_Client_Wrapper 89 | */ 90 | public static function register( OpenID_Connect_Generic_Client $client, OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ) { 91 | $client_wrapper = new self( $client, $settings, $logger ); 92 | 93 | // Integrated logout. 94 | if ( $settings->endpoint_end_session ) { 95 | add_filter( 'allowed_redirect_hosts', array( $client_wrapper, 'update_allowed_redirect_hosts' ), 99, 1 ); 96 | add_filter( 'logout_redirect', array( $client_wrapper, 'get_end_session_logout_redirect_url' ), 99, 3 ); 97 | } 98 | 99 | // Alter the requests according to settings. 100 | add_filter( 'openid-connect-generic-alter-request', array( $client_wrapper, 'alter_request' ), 10, 2 ); 101 | 102 | if ( is_admin() ) { 103 | /* 104 | * Use the ajax url to handle processing authorization without any html output 105 | * this callback will occur when then IDP returns with an authenticated value 106 | */ 107 | add_action( 'wp_ajax_openid-connect-authorize', array( $client_wrapper, 'authentication_request_callback' ) ); 108 | add_action( 'wp_ajax_nopriv_openid-connect-authorize', array( $client_wrapper, 'authentication_request_callback' ) ); 109 | } 110 | 111 | if ( $settings->alternate_redirect_uri ) { 112 | // Provide an alternate route for authentication_request_callback. 113 | add_rewrite_rule( '^openid-connect-authorize/?', 'index.php?openid-connect-authorize=1', 'top' ); 114 | add_rewrite_tag( '%openid-connect-authorize%', '1' ); 115 | add_action( 'parse_request', array( $client_wrapper, 'alternate_redirect_uri_parse_request' ) ); 116 | } 117 | 118 | return $client_wrapper; 119 | } 120 | 121 | /** 122 | * Implements WordPress parse_request action. 123 | * 124 | * @param WP_Query $query The WordPress query object. 125 | * 126 | * @return void 127 | */ 128 | public function alternate_redirect_uri_parse_request( $query ) { 129 | if ( isset( $query->query_vars['openid-connect-authorize'] ) && 130 | '1' === $query->query_vars['openid-connect-authorize'] ) { 131 | $this->authentication_request_callback(); 132 | exit; 133 | } 134 | } 135 | 136 | /** 137 | * Get the client login redirect. 138 | * 139 | * @return string 140 | */ 141 | public function get_redirect_to() { 142 | /* 143 | * @var WP $wp 144 | */ 145 | global $wp; 146 | 147 | if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' == $GLOBALS['pagenow'] && isset( $_GET['action'] ) && 'logout' === $_GET['action'] ) { 148 | return ''; 149 | } 150 | 151 | // Default redirect to the homepage. 152 | $redirect_url = home_url(); 153 | 154 | // If using the login form, default redirect to the admin dashboard. 155 | if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' == $GLOBALS['pagenow'] ) { 156 | $redirect_url = admin_url(); 157 | } 158 | 159 | // Honor Core WordPress & other plugin redirects. 160 | if ( isset( $_REQUEST['redirect_to'] ) ) { 161 | $redirect_url = esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ); 162 | } 163 | 164 | // Capture the current URL if set to redirect back to origin page. 165 | if ( $this->settings->redirect_user_back ) { 166 | if ( ! empty( $wp->query_string ) ) { 167 | $redirect_url = home_url( '?' . $wp->query_string ); 168 | } 169 | if ( ! empty( $wp->request ) ) { 170 | $redirect_url = home_url( add_query_arg( null, null ) ); 171 | // @phpstan-ignore-next-line 172 | if ( $wp->did_permalink ) { 173 | $redirect_url = home_url( add_query_arg( $_GET, trailingslashit( $wp->request ) ) ); 174 | } 175 | } 176 | } 177 | 178 | // This hook is being deprecated with the move away from cookies. 179 | $redirect_url = apply_filters_deprecated( 180 | 'openid-connect-generic-cookie-redirect-url', 181 | array( $redirect_url ), 182 | '3.8.2', 183 | 'openid-connect-generic-client-redirect-to' 184 | ); 185 | 186 | // This is the new hook to use with the transients version of redirection. 187 | return apply_filters( 'openid-connect-generic-client-redirect-to', $redirect_url ); 188 | } 189 | 190 | /** 191 | * Create a single use authentication url 192 | * 193 | * @param array $atts An optional array of override/feature attributes. 194 | * 195 | * @return string 196 | */ 197 | public function get_authentication_url( $atts = array() ) { 198 | 199 | $atts = shortcode_atts( 200 | array( 201 | 'endpoint_login' => $this->settings->endpoint_login, 202 | 'scope' => $this->settings->scope, 203 | 'client_id' => $this->settings->client_id, 204 | 'redirect_uri' => $this->client->get_redirect_uri(), 205 | 'redirect_to' => $this->get_redirect_to(), 206 | 'acr_values' => $this->settings->acr_values, 207 | ), 208 | $atts, 209 | 'openid_connect_generic_auth_url' 210 | ); 211 | 212 | // Validate the redirect to value to prevent a redirection attack. 213 | if ( ! empty( $atts['redirect_to'] ) ) { 214 | $atts['redirect_to'] = wp_validate_redirect( $atts['redirect_to'], home_url() ); 215 | } 216 | 217 | $separator = '?'; 218 | if ( stripos( $this->settings->endpoint_login, '?' ) !== false ) { 219 | $separator = '&'; 220 | } 221 | 222 | $url_format = '%1$s%2$sresponse_type=code&scope=%3$s&client_id=%4$s&state=%5$s&redirect_uri=%6$s'; 223 | if ( ! empty( $atts['acr_values'] ) ) { 224 | $url_format .= '&acr_values=%7$s'; 225 | } 226 | 227 | $url = sprintf( 228 | $url_format, 229 | $atts['endpoint_login'], 230 | $separator, 231 | rawurlencode( $atts['scope'] ), 232 | rawurlencode( $atts['client_id'] ), 233 | $this->client->new_state( $atts['redirect_to'] ), 234 | rawurlencode( $atts['redirect_uri'] ), 235 | rawurlencode( $atts['acr_values'] ) 236 | ); 237 | 238 | $url = apply_filters( 'openid-connect-generic-auth-url', $url ); 239 | $this->logger->log( $url, 'make_authentication_url' ); 240 | return $url; 241 | } 242 | 243 | /** 244 | * Handle retrieval and validation of refresh_token. 245 | * 246 | * @return void 247 | */ 248 | public function ensure_tokens_still_fresh() { 249 | if ( ! is_user_logged_in() ) { 250 | return; 251 | } 252 | 253 | $user_id = wp_get_current_user()->ID; 254 | $last_token_response = get_user_meta( $user_id, 'openid-connect-generic-last-token-response', true ); 255 | 256 | if ( ! empty( $last_token_response['expires_in'] ) && ! empty( $last_token_response['time'] ) ) { 257 | /* 258 | * @var int $expiration_time 259 | */ 260 | $expiration_time = intval( $last_token_response['time'] ) + intval( $last_token_response['expires_in'] ); 261 | if ( time() < $expiration_time ) { 262 | // Access token is not expired so don't attempt to refresh. 263 | return; 264 | } 265 | } 266 | 267 | $manager = WP_Session_Tokens::get_instance( $user_id ); 268 | $token = wp_get_session_token(); 269 | $session = $manager->get( $token ); 270 | 271 | if ( ! isset( $session[ self::COOKIE_TOKEN_REFRESH_KEY ] ) ) { 272 | // Not an OpenID-based session. 273 | return; 274 | } 275 | 276 | $refresh_token_info = $session[ self::COOKIE_TOKEN_REFRESH_KEY ]; 277 | 278 | $refresh_token = $refresh_token_info['refresh_token'] ?? null; 279 | if ( empty( $refresh_token ) ) { 280 | // No valid refresh token. 281 | return; 282 | } 283 | 284 | $token_result = $this->client->request_new_tokens( $refresh_token ); 285 | 286 | if ( is_wp_error( $token_result ) ) { 287 | wp_logout(); 288 | $this->error_redirect( $token_result ); 289 | } 290 | 291 | $token_response = $this->client->get_token_response( $token_result ); 292 | if ( is_wp_error( $token_response ) ) { 293 | wp_logout(); 294 | $this->error_redirect( $token_response ); 295 | } 296 | 297 | // Capture the time so that access token expiration can be calculated later. 298 | $token_response[] = time(); 299 | 300 | update_user_meta( $user_id, 'openid-connect-generic-last-token-response', $token_response ); 301 | $this->save_refresh_token( $manager, $token, $token_response ); 302 | } 303 | 304 | /** 305 | * Handle errors by redirecting the user to the login form along with an 306 | * error code 307 | * 308 | * @param WP_Error $error A WordPress error object. 309 | * 310 | * @return void 311 | */ 312 | public function error_redirect( $error ) { 313 | $this->logger->log( $error ); 314 | 315 | // Redirect user back to login page. 316 | wp_redirect( 317 | wp_login_url() . 318 | '?login-error=' . $error->get_error_code() . 319 | '&message=' . urlencode( $error->get_error_message() ) 320 | ); 321 | exit; 322 | } 323 | 324 | /** 325 | * Get the current error state. 326 | * 327 | * @return bool|WP_Error 328 | */ 329 | public function get_error() { 330 | return $this->error; 331 | } 332 | 333 | /** 334 | * Add the end_session endpoint to WordPress core's whitelist of redirect hosts. 335 | * 336 | * @param array $allowed The allowed redirect host names. 337 | * 338 | * @return array|bool 339 | */ 340 | public function update_allowed_redirect_hosts( $allowed ) { 341 | $host = parse_url( $this->settings->endpoint_end_session, PHP_URL_HOST ); 342 | if ( ! $host ) { 343 | return false; 344 | } 345 | 346 | $allowed[] = $host; 347 | return $allowed; 348 | } 349 | 350 | /** 351 | * Handle the logout redirect for end_session endpoint. 352 | * 353 | * @param string $redirect_url The requested redirect URL. 354 | * @param string $requested_redirect_to The user login source URL, or configured user redirect URL. 355 | * @param WP_User $user The logged in user object. 356 | * 357 | * @return string 358 | */ 359 | public function get_end_session_logout_redirect_url( $redirect_url, $requested_redirect_to, $user ) { 360 | $url = $this->settings->endpoint_end_session; 361 | $query = parse_url( $url, PHP_URL_QUERY ); 362 | $url .= $query ? '&' : '?'; 363 | 364 | // Prevent redirect back to the IDP when logging out in auto mode. 365 | if ( 'auto' === $this->settings->login_type && strpos( $redirect_url, 'wp-login.php?loggedout=true' ) ) { 366 | // By default redirect back to the site home. 367 | $redirect_url = home_url(); 368 | } 369 | 370 | $token_response = $user->get( 'openid-connect-generic-last-token-response' ); 371 | if ( ! $token_response ) { 372 | // Happens if non-openid login was used. 373 | return $redirect_url; 374 | } else if ( ! parse_url( $redirect_url, PHP_URL_HOST ) ) { 375 | // Convert to absolute url if needed, site_url() to be friendly with non-standard (Bedrock) layout. 376 | $redirect_url = site_url( $redirect_url ); 377 | } 378 | 379 | $claim = $user->get( 'openid-connect-generic-last-id-token-claim' ); 380 | 381 | if ( isset( $claim['iss'] ) && 'https://accounts.google.com' == $claim['iss'] ) { 382 | /* 383 | * Google revoke endpoint 384 | * 1. expects the *access_token* to be passed as "token" 385 | * 2. does not support redirection (post_logout_redirect_uri) 386 | * So just redirect to regular WP logout URL. 387 | * (we would *not* disconnect the user from any Google service even 388 | * if he was initially disconnected to them) 389 | */ 390 | return $redirect_url; 391 | } else { 392 | return $url . sprintf( 'id_token_hint=%s&post_logout_redirect_uri=%s', $token_response['id_token'], urlencode( $redirect_url ) ); 393 | } 394 | } 395 | 396 | /** 397 | * Modify outgoing requests according to settings. 398 | * 399 | * @param array $request The outgoing request array. 400 | * @param string $operation The request operation name. 401 | * 402 | * @return mixed 403 | */ 404 | public function alter_request( $request, $operation ) { 405 | if ( ! empty( $this->settings->http_request_timeout ) ) { 406 | $request['timeout'] = intval( $this->settings->http_request_timeout ); 407 | } 408 | 409 | if ( $this->settings->no_sslverify ) { 410 | $request['sslverify'] = false; 411 | } 412 | 413 | return $request; 414 | } 415 | 416 | /** 417 | * Control the authentication and subsequent authorization of the user when 418 | * returning from the IDP. 419 | * 420 | * @return void 421 | */ 422 | public function authentication_request_callback() { 423 | $client = $this->client; 424 | 425 | // Start the authentication flow. 426 | $authentication_request = $client->validate_authentication_request( $_GET ); 427 | 428 | if ( is_wp_error( $authentication_request ) ) { 429 | $this->error_redirect( $authentication_request ); 430 | } 431 | 432 | // Retrieve the authentication code from the authentication request. 433 | $code = $client->get_authentication_code( $authentication_request ); 434 | 435 | if ( is_wp_error( $code ) ) { 436 | $this->error_redirect( $code ); 437 | } 438 | 439 | // Retrieve the authentication state from the authentication request. 440 | $state = $client->get_authentication_state( $authentication_request ); 441 | 442 | if ( is_wp_error( $state ) ) { 443 | $this->error_redirect( $state ); 444 | } 445 | 446 | // Attempting to exchange an authorization code for an authentication token. 447 | $token_result = $client->request_authentication_token( $code ); 448 | 449 | if ( is_wp_error( $token_result ) ) { 450 | $this->error_redirect( $token_result ); 451 | } 452 | 453 | // Get the decoded response from the authentication request result. 454 | $token_response = $client->get_token_response( $token_result ); 455 | 456 | // Allow for other plugins to alter data before validation. 457 | $token_response = apply_filters( 'openid-connect-modify-token-response-before-validation', $token_response ); 458 | 459 | if ( is_wp_error( $token_response ) ) { 460 | $this->error_redirect( $token_response ); 461 | } 462 | 463 | // Ensure the that response contains required information. 464 | $valid = $client->validate_token_response( $token_response ); 465 | 466 | if ( is_wp_error( $valid ) ) { 467 | $this->error_redirect( $valid ); 468 | } 469 | 470 | /** 471 | * The id_token is used to identify the authenticated user, e.g. for SSO. 472 | * The access_token must be used to prove access rights to protected 473 | * resources e.g. for the userinfo endpoint 474 | */ 475 | $id_token_claim = $client->get_id_token_claim( $token_response ); 476 | 477 | // Allow for other plugins to alter data before validation. 478 | $id_token_claim = apply_filters( 'openid-connect-modify-id-token-claim-before-validation', $id_token_claim ); 479 | 480 | if ( is_wp_error( $id_token_claim ) ) { 481 | $this->error_redirect( $id_token_claim ); 482 | } 483 | 484 | // Validate our id_token has required values. 485 | $valid = $client->validate_id_token_claim( $id_token_claim ); 486 | 487 | if ( is_wp_error( $valid ) ) { 488 | $this->error_redirect( $valid ); 489 | } 490 | 491 | // If userinfo endpoint is set, exchange the token_response for a user_claim. 492 | if ( ! empty( $this->settings->endpoint_userinfo ) && isset( $token_response['access_token'] ) ) { 493 | $user_claim = $client->get_user_claim( $token_response ); 494 | } else { 495 | $user_claim = $id_token_claim; 496 | } 497 | 498 | if ( is_wp_error( $user_claim ) ) { 499 | $this->error_redirect( $user_claim ); 500 | } 501 | 502 | // Validate our user_claim has required values. 503 | $valid = $client->validate_user_claim( $user_claim, $id_token_claim ); 504 | 505 | if ( is_wp_error( $valid ) ) { 506 | $this->error_redirect( $valid ); 507 | } 508 | 509 | /** 510 | * End authorization 511 | * - 512 | * Request is authenticated and authorized - start user handling 513 | */ 514 | $subject_identity = $client->get_subject_identity( $id_token_claim ); 515 | $user = $this->get_user_by_identity( $subject_identity ); 516 | 517 | // A pre-existing IDP mapped user wasn't found. 518 | if ( ! $user ) { 519 | // If linking existing users or creating new ones call the `create_new_user` method which handles both cases. 520 | if ( $this->settings->link_existing_users || $this->settings->create_if_does_not_exist ) { 521 | $user = $this->create_new_user( $subject_identity, $user_claim ); 522 | if ( is_wp_error( $user ) ) { 523 | $this->error_redirect( $user ); 524 | } 525 | } else { 526 | $this->error_redirect( new WP_Error( 'identity-not-map-existing-user', __( 'User identity is not linked to an existing WordPress user.', 'daggerhart-openid-connect-generic' ), $user_claim ) ); 527 | } 528 | } 529 | 530 | // Validate the found / created user. 531 | $valid = $this->validate_user( $user ); 532 | 533 | if ( is_wp_error( $valid ) ) { 534 | $this->error_redirect( $valid ); 535 | } 536 | 537 | // Login the found / created user. 538 | $start_time = microtime( true ); 539 | $this->login_user( $user, $token_response, $id_token_claim, $user_claim, $subject_identity ); 540 | $end_time = microtime( true ); 541 | // Log our success. 542 | $this->logger->log( "Successful login for: {$user->user_login} ({$user->ID})", 'login-success', $end_time - $start_time ); 543 | 544 | // Allow plugins / themes to take action once a user is logged in. 545 | $start_time = microtime( true ); 546 | do_action( 'openid-connect-generic-user-logged-in', $user ); 547 | $end_time = microtime( true ); 548 | $this->logger->log( 'openid-connect-generic-user-logged-in', 'do_action', $end_time - $start_time ); 549 | 550 | // Default redirect to the homepage. 551 | $redirect_url = home_url(); 552 | // Redirect user according to redirect set in state. 553 | $state_object = get_transient( 'openid-connect-generic-state--' . $state ); 554 | // Get the redirect URL stored with the corresponding authentication request state. 555 | if ( ! empty( $state_object ) && ! empty( $state_object[ $state ] ) && ! empty( $state_object[ $state ]['redirect_to'] ) ) { 556 | $redirect_url = $state_object[ $state ]['redirect_to']; 557 | } 558 | 559 | // Provide backwards compatibility for customization using the deprecated cookie method. 560 | if ( ! empty( $_COOKIE[ self::COOKIE_REDIRECT_KEY ] ) ) { 561 | $redirect_url = esc_url_raw( wp_unslash( $_COOKIE[ self::COOKIE_REDIRECT_KEY ] ) ); 562 | } 563 | 564 | // Only do redirect-user-back action hook when the plugin is configured for it. 565 | if ( $this->settings->redirect_user_back ) { 566 | do_action( 'openid-connect-generic-redirect-user-back', $redirect_url, $user ); 567 | } 568 | 569 | wp_redirect( $redirect_url ); 570 | 571 | exit; 572 | } 573 | 574 | /** 575 | * Validate the potential WP_User. 576 | * 577 | * @param WP_User|WP_Error|false $user The user object. 578 | * 579 | * @return true|WP_Error 580 | */ 581 | public function validate_user( $user ) { 582 | // Ensure the found user is a real WP_User. 583 | if ( ! is_a( $user, 'WP_User' ) || ! $user->exists() ) { 584 | return new WP_Error( 'invalid-user', __( 'Invalid user.', 'daggerhart-openid-connect-generic' ), $user ); 585 | } 586 | 587 | return true; 588 | } 589 | 590 | /** 591 | * Refresh user claim. 592 | * 593 | * @param WP_User $user The user object. 594 | * @param array $token_response The token response. 595 | * 596 | * @return WP_Error|array 597 | */ 598 | public function refresh_user_claim( $user, $token_response ) { 599 | $client = $this->client; 600 | 601 | /** 602 | * The id_token is used to identify the authenticated user, e.g. for SSO. 603 | * The access_token must be used to prove access rights to protected 604 | * resources e.g. for the userinfo endpoint 605 | */ 606 | $id_token_claim = $client->get_id_token_claim( $token_response ); 607 | 608 | // Allow for other plugins to alter data before validation. 609 | $id_token_claim = apply_filters( 'openid-connect-modify-id-token-claim-before-validation', $id_token_claim ); 610 | 611 | if ( is_wp_error( $id_token_claim ) ) { 612 | return $id_token_claim; 613 | } 614 | 615 | // Validate our id_token has required values. 616 | $valid = $client->validate_id_token_claim( $id_token_claim ); 617 | 618 | if ( is_wp_error( $valid ) ) { 619 | return $valid; 620 | } 621 | 622 | // If userinfo endpoint is set, exchange the token_response for a user_claim. 623 | if ( ! empty( $this->settings->endpoint_userinfo ) && isset( $token_response['access_token'] ) ) { 624 | $user_claim = $client->get_user_claim( $token_response ); 625 | } else { 626 | $user_claim = $id_token_claim; 627 | } 628 | 629 | if ( is_wp_error( $user_claim ) ) { 630 | return $user_claim; 631 | } 632 | 633 | // Validate our user_claim has required values. 634 | $valid = $client->validate_user_claim( $user_claim, $id_token_claim ); 635 | 636 | if ( is_wp_error( $valid ) ) { 637 | $this->error_redirect( $valid ); 638 | return $valid; 639 | } 640 | 641 | // Store the tokens for future reference. 642 | update_user_meta( $user->ID, 'openid-connect-generic-last-token-response', $token_response ); 643 | update_user_meta( $user->ID, 'openid-connect-generic-last-id-token-claim', $id_token_claim ); 644 | update_user_meta( $user->ID, 'openid-connect-generic-last-user-claim', $user_claim ); 645 | 646 | return $user_claim; 647 | } 648 | 649 | /** 650 | * Record user meta data, and provide an authorization cookie. 651 | * 652 | * @param WP_User $user The user object. 653 | * @param array $token_response The token response. 654 | * @param array $id_token_claim The ID token claim. 655 | * @param array $user_claim The authenticated user claim. 656 | * @param string $subject_identity The subject identity from the IDP. 657 | * 658 | * @return void 659 | */ 660 | public function login_user( $user, $token_response, $id_token_claim, $user_claim, $subject_identity ): void { 661 | // Store the tokens for future reference. 662 | update_user_meta( $user->ID, 'openid-connect-generic-last-token-response', $token_response ); 663 | update_user_meta( $user->ID, 'openid-connect-generic-last-id-token-claim', $id_token_claim ); 664 | update_user_meta( $user->ID, 'openid-connect-generic-last-user-claim', $user_claim ); 665 | // Allow plugins / themes to take action using current claims on existing user (e.g. update role). 666 | do_action( 'openid-connect-generic-update-user-using-current-claim', $user, $user_claim ); 667 | 668 | // Determine the amount of days before the cookie expires. 669 | $remember_me = apply_filters( 'openid-connect-generic-remember-me', false, $user, $token_response, $id_token_claim, $user_claim, $subject_identity ); 670 | $wp_expiration_days = $remember_me ? 14 : 2; 671 | 672 | // Create the WP session, so we know its token. 673 | $expiration = time() + apply_filters( 'auth_cookie_expiration', $wp_expiration_days * DAY_IN_SECONDS, $user->ID, $remember_me ); 674 | $manager = WP_Session_Tokens::get_instance( $user->ID ); 675 | $token = $manager->create( $expiration ); 676 | 677 | // Save the refresh token in the session. 678 | $this->save_refresh_token( $manager, $token, $token_response ); 679 | 680 | // you did great, have a cookie! 681 | wp_set_auth_cookie( $user->ID, $remember_me, '', $token ); 682 | do_action( 'wp_login', $user->user_login, $user ); 683 | } 684 | 685 | /** 686 | * Save refresh token to WP session tokens 687 | * 688 | * @param WP_Session_Tokens $manager A user session tokens manager. 689 | * @param string $token The current users session token. 690 | * @param array|WP_Error|null $token_response The authentication token response. 691 | */ 692 | public function save_refresh_token( $manager, $token, $token_response ): void { 693 | if ( ! $this->settings->token_refresh_enable ) { 694 | return; 695 | } 696 | 697 | $session = $manager->get( $token ); 698 | 699 | $session[ self::COOKIE_TOKEN_REFRESH_KEY ] = array( 700 | 'refresh_token' => $token_response['refresh_token'] ?? false, 701 | ); 702 | 703 | $manager->update( $token, $session ); 704 | return; 705 | } 706 | 707 | /** 708 | * Get the user that has meta data matching a 709 | * 710 | * @param string $subject_identity The IDP identity of the user. 711 | * 712 | * @return false|WP_User 713 | */ 714 | public function get_user_by_identity( $subject_identity ) { 715 | // Look for user by their openid-connect-generic-subject-identity value. 716 | $user_query = new WP_User_Query( 717 | array( 718 | 'meta_query' => array( 719 | array( 720 | 'key' => 'openid-connect-generic-subject-identity', 721 | 'value' => $subject_identity, 722 | ), 723 | ), 724 | // Override the default blog_id (get_current_blog_id) to find users on different sites of a multisite install. 725 | 'blog_id' => 0, 726 | ) 727 | ); 728 | 729 | // If we found existing users, grab the first one returned. 730 | if ( $user_query->get_total() > 0 ) { 731 | $users = $user_query->get_results(); 732 | return $users[0]; 733 | } 734 | 735 | return false; 736 | } 737 | 738 | /** 739 | * Avoid user_login collisions by incrementing. 740 | * 741 | * @param array $user_claim The IDP authenticated user claim data. 742 | * 743 | * @return string|WP_Error 744 | */ 745 | private function get_username_from_claim( $user_claim ) { 746 | 747 | // @var string $desired_username 748 | $desired_username = ''; 749 | 750 | // Allow settings to take first stab at username. 751 | if ( ! empty( $this->settings->identity_key ) && isset( $user_claim[ $this->settings->identity_key ] ) ) { 752 | $desired_username = $user_claim[ $this->settings->identity_key ]; 753 | } 754 | if ( empty( $desired_username ) && isset( $user_claim['preferred_username'] ) && ! empty( $user_claim['preferred_username'] ) ) { 755 | $desired_username = $user_claim['preferred_username']; 756 | } 757 | if ( empty( $desired_username ) && isset( $user_claim['name'] ) && ! empty( $user_claim['name'] ) ) { 758 | $desired_username = $user_claim['name']; 759 | } 760 | if ( empty( $desired_username ) && isset( $user_claim['email'] ) && ! empty( $user_claim['email'] ) ) { 761 | $tmp = explode( '@', $user_claim['email'] ); 762 | $desired_username = $tmp[0]; 763 | } 764 | if ( empty( $desired_username ) ) { 765 | // Nothing to build a name from. 766 | return new WP_Error( 'no-username', __( 'No appropriate username found.', 'daggerhart-openid-connect-generic' ), $user_claim ); 767 | } 768 | 769 | // Don't use the full email address for a username. 770 | $_desired_username = explode( '@', $desired_username ); 771 | $desired_username = $_desired_username[0]; 772 | // Use WordPress Core to sanitize the IDP username. 773 | $sanitized_username = sanitize_user( $desired_username, true ); 774 | if ( empty( $sanitized_username ) ) { 775 | // translators: %1$s is the santitized version of the username from the IDP. 776 | return new WP_Error( 'username-sanitization-failed', sprintf( __( 'Username %1$s could not be sanitized.', 'daggerhart-openid-connect-generic' ), $desired_username ), $desired_username ); 777 | } 778 | 779 | return $sanitized_username; 780 | } 781 | 782 | /** 783 | * Get a nickname. 784 | * 785 | * @param array $user_claim The IDP authenticated user claim data. 786 | * 787 | * @return string|WP_Error|null 788 | */ 789 | private function get_nickname_from_claim( $user_claim ) { 790 | $desired_nickname = null; 791 | // Allow settings to take first stab at nickname. 792 | if ( ! empty( $this->settings->nickname_key ) && isset( $user_claim[ $this->settings->nickname_key ] ) ) { 793 | $desired_nickname = $user_claim[ $this->settings->nickname_key ]; 794 | } 795 | 796 | if ( empty( $desired_nickname ) ) { 797 | // translators: %1$s is the configured User Claim nickname key. 798 | return new WP_Error( 'no-nickname', sprintf( __( 'No nickname found in user claim using key: %1$s.', 'daggerhart-openid-connect-generic' ), $this->settings->nickname_key ), $this->settings->nickname_key ); 799 | } 800 | 801 | return $desired_nickname; 802 | } 803 | 804 | /** 805 | * Checks if $claimname is in the body or _claim_names of the userinfo. 806 | * If yes, returns the claim value. Otherwise, returns false. 807 | * 808 | * @param string $claimname the claim name to look for. 809 | * @param array $userinfo the JSON to look in. 810 | * @param string $claimvalue the source claim value ( from the body of the JWT of the claim source). 811 | * @return true|false 812 | */ 813 | private function get_claim( $claimname, $userinfo, &$claimvalue ) { 814 | /** 815 | * If we find a simple claim, return it. 816 | */ 817 | if ( array_key_exists( $claimname, $userinfo ) ) { 818 | $claimvalue = $userinfo[ $claimname ]; 819 | return true; 820 | } 821 | /** 822 | * If there are no aggregated claims, it is over. 823 | */ 824 | if ( ! array_key_exists( '_claim_names', $userinfo ) || 825 | ! array_key_exists( '_claim_sources', $userinfo ) ) { 826 | return false; 827 | } 828 | $claim_src_ptr = $userinfo['_claim_names']; 829 | if ( ! isset( $claim_src_ptr ) ) { 830 | return false; 831 | } 832 | /** 833 | * No reference found 834 | */ 835 | if ( ! array_key_exists( $claimname, $claim_src_ptr ) ) { 836 | return false; 837 | } 838 | $src_name = $claim_src_ptr[ $claimname ]; 839 | // Reference found, but no corresponding JWT. This is a malformed userinfo. 840 | if ( ! array_key_exists( $src_name, $userinfo['_claim_sources'] ) ) { 841 | return false; 842 | } 843 | $src = $userinfo['_claim_sources'][ $src_name ]; 844 | // Source claim is not a JWT. Abort. 845 | if ( ! array_key_exists( 'JWT', $src ) ) { 846 | return false; 847 | } 848 | /** 849 | * Extract claim from JWT. 850 | * FIXME: We probably want to verify the JWT signature/issuer here. 851 | * For example, using JWKS if applicable. For symmetrically signed 852 | * JWTs (HMAC), we need a way to specify the acceptable secrets 853 | * and each possible issuer in the config. 854 | */ 855 | $jwt = $src['JWT']; 856 | list ( $header, $body, $rest ) = explode( '.', $jwt, 3 ); 857 | $body_str = base64_decode( $body, false ); 858 | if ( ! $body_str ) { 859 | return false; 860 | } 861 | $body_json = json_decode( $body_str, true ); 862 | if ( ! isset( $body_json ) ) { 863 | return false; 864 | } 865 | if ( ! array_key_exists( $claimname, $body_json ) ) { 866 | return false; 867 | } 868 | $claimvalue = $body_json[ $claimname ]; 869 | return true; 870 | } 871 | 872 | 873 | /** 874 | * Build a string from the user claim according to the specified format. 875 | * 876 | * @param string $format The format format of the user identity. 877 | * @param array $user_claim The authorized user claim. 878 | * @param bool $error_on_missing_key Whether to return and error on a missing key. 879 | * 880 | * @return string|WP_Error 881 | */ 882 | private function format_string_with_claim( $format, $user_claim, $error_on_missing_key = false ) { 883 | $matches = null; 884 | $string = ''; 885 | $info = ''; 886 | $i = 0; 887 | if ( preg_match_all( '/\{[^}]*\}/u', $format, $matches, PREG_OFFSET_CAPTURE ) ) { 888 | foreach ( $matches[0] as $match ) { 889 | $key = substr( $match[0], 1, -1 ); 890 | $string .= substr( $format, $i, $match[1] - $i ); 891 | if ( ! $this->get_claim( $key, $user_claim, $info ) ) { 892 | if ( $error_on_missing_key ) { 893 | return new WP_Error( 894 | 'incomplete-user-claim', 895 | __( 'User claim incomplete.', 'daggerhart-openid-connect-generic' ), 896 | array( 897 | 'message' => 'Unable to find key: ' . $key . ' in user_claim', 898 | 'hint' => 'Verify OpenID Scope includes a scope with the attributes you need', 899 | 'user_claim' => $user_claim, 900 | 'format' => $format, 901 | ) 902 | ); 903 | } 904 | } else { 905 | $string .= $info; 906 | } 907 | $i = $match[1] + strlen( $match[0] ); 908 | } 909 | } 910 | $string .= substr( $format, $i ); 911 | return $string; 912 | } 913 | 914 | /** 915 | * Get a displayname. 916 | * 917 | * @param array $user_claim The authorized user claim. 918 | * @param bool $error_on_missing_key Whether to return and error on a missing key. 919 | * 920 | * @return string|null|WP_Error 921 | */ 922 | private function get_displayname_from_claim( $user_claim, $error_on_missing_key = false ) { 923 | if ( ! empty( $this->settings->displayname_format ) ) { 924 | return $this->format_string_with_claim( $this->settings->displayname_format, $user_claim, $error_on_missing_key ); 925 | } 926 | return null; 927 | } 928 | 929 | /** 930 | * Get an email. 931 | * 932 | * @param array $user_claim The authorized user claim. 933 | * @param bool $error_on_missing_key Whether to return and error on a missing key. 934 | * 935 | * @return string|null|WP_Error 936 | */ 937 | private function get_email_from_claim( $user_claim, $error_on_missing_key = false ) { 938 | if ( ! empty( $this->settings->email_format ) ) { 939 | return $this->format_string_with_claim( $this->settings->email_format, $user_claim, $error_on_missing_key ); 940 | } 941 | return null; 942 | } 943 | 944 | /** 945 | * Create a new user from details in a user_claim. 946 | * 947 | * @param string $subject_identity The authenticated user's identity with the IDP. 948 | * @param array $user_claim The authorized user claim. 949 | * 950 | * @return \WP_Error | \WP_User 951 | */ 952 | public function create_new_user( $subject_identity, $user_claim ) { 953 | $start_time = microtime( true ); 954 | $user_claim = apply_filters( 'openid-connect-generic-alter-user-claim', $user_claim ); 955 | 956 | // Default username & email to the subject identity. 957 | $username = $subject_identity; 958 | $email = $subject_identity; 959 | $nickname = $subject_identity; 960 | $displayname = $subject_identity; 961 | $values_missing = false; 962 | 963 | // Allow claim details to determine username, email, nickname and displayname. 964 | $_email = $this->get_email_from_claim( $user_claim, true ); 965 | if ( is_wp_error( $_email ) || empty( $_email ) ) { 966 | $values_missing = true; 967 | } else { 968 | $email = $_email; 969 | } 970 | 971 | $_username = $this->get_username_from_claim( $user_claim ); 972 | if ( is_wp_error( $_username ) || empty( $_username ) ) { 973 | $values_missing = true; 974 | } else { 975 | $username = $_username; 976 | } 977 | 978 | $_nickname = $this->get_nickname_from_claim( $user_claim ); 979 | if ( is_wp_error( $_nickname ) || empty( $_nickname ) ) { 980 | $values_missing = true; 981 | } else { 982 | $nickname = $_nickname; 983 | } 984 | 985 | $_displayname = $this->get_displayname_from_claim( $user_claim, true ); 986 | if ( is_wp_error( $_displayname ) || empty( $_displayname ) ) { 987 | $values_missing = true; 988 | } else { 989 | $displayname = $_displayname; 990 | } 991 | 992 | // Attempt another request for userinfo if some values are missing. 993 | if ( $values_missing && isset( $user_claim['access_token'] ) && ! empty( $this->settings->endpoint_userinfo ) ) { 994 | $user_claim_result = $this->client->request_userinfo( $user_claim['access_token'] ); 995 | 996 | // Make sure we didn't get an error. 997 | if ( is_wp_error( $user_claim_result ) ) { 998 | return new WP_Error( 'bad-user-claim-result', __( 'Bad user claim result.', 'daggerhart-openid-connect-generic' ), $user_claim_result ); 999 | } 1000 | 1001 | $user_claim = json_decode( $user_claim_result['body'], true ); 1002 | } 1003 | 1004 | $_email = $this->get_email_from_claim( $user_claim, true ); 1005 | if ( is_wp_error( $_email ) ) { 1006 | return $_email; 1007 | } 1008 | // Use the email address from the latest userinfo request if not empty. 1009 | if ( ! empty( $_email ) ) { 1010 | $email = $_email; 1011 | } 1012 | 1013 | $_username = $this->get_username_from_claim( $user_claim ); 1014 | if ( is_wp_error( $_username ) ) { 1015 | return $_username; 1016 | } 1017 | // Use the username from the latest userinfo request if not empty. 1018 | if ( ! empty( $_username ) ) { 1019 | $username = $_username; 1020 | } 1021 | 1022 | $_nickname = $this->get_nickname_from_claim( $user_claim ); 1023 | if ( is_wp_error( $_nickname ) ) { 1024 | return $_nickname; 1025 | } 1026 | // Use the username as the nickname if the userinfo request nickname is empty. 1027 | if ( empty( $_nickname ) ) { 1028 | $nickname = $username; 1029 | } 1030 | 1031 | $_displayname = $this->get_displayname_from_claim( $user_claim, true ); 1032 | if ( is_wp_error( $_displayname ) ) { 1033 | return $_displayname; 1034 | } 1035 | // Use the nickname as the displayname if the userinfo request displayname is empty. 1036 | if ( empty( $_displayname ) ) { 1037 | $displayname = $nickname; 1038 | } 1039 | 1040 | // Before trying to create the user, first check if a matching user exists. 1041 | if ( $this->settings->link_existing_users ) { 1042 | $uid = null; 1043 | if ( $this->settings->identify_with_username ) { 1044 | $uid = username_exists( $username ); 1045 | } else { 1046 | $uid = email_exists( $email ); 1047 | } 1048 | if ( ! empty( $uid ) ) { 1049 | $user = $this->update_existing_user( $uid, $subject_identity ); 1050 | do_action( 'openid-connect-generic-update-user-using-current-claim', $user, $user_claim ); 1051 | $end_time = microtime( true ); 1052 | $this->logger->log( "Existing user updated: {$user->user_login} ($uid)", __METHOD__, $end_time - $start_time ); 1053 | return $user; 1054 | } 1055 | } 1056 | 1057 | /** 1058 | * Allow other plugins / themes to determine authorization of new accounts 1059 | * based on the returned user claim. 1060 | */ 1061 | $create_user = apply_filters( 'openid-connect-generic-user-creation-test', $this->settings->create_if_does_not_exist, $user_claim ); 1062 | 1063 | if ( ! $create_user ) { 1064 | return new WP_Error( 'cannot-authorize', __( 'Can not authorize.', 'daggerhart-openid-connect-generic' ), $create_user ); 1065 | } 1066 | 1067 | // Copy the username for incrementing. 1068 | $_username = $username; 1069 | // Ensure prevention of linking usernames & collisions by incrementing the username if it exists. 1070 | // @example Original user gets "name", second user gets "name2", etc. 1071 | $count = 1; 1072 | while ( username_exists( $username ) ) { 1073 | $count++; 1074 | $username = $_username . $count; 1075 | } 1076 | 1077 | $user_data = array( 1078 | 'user_login' => $username, 1079 | 'user_pass' => wp_generate_password( 32, true, true ), 1080 | 'user_email' => $email, 1081 | 'display_name' => $displayname, 1082 | 'nickname' => $nickname, 1083 | 'first_name' => isset( $user_claim['given_name'] ) ? $user_claim['given_name'] : '', 1084 | 'last_name' => isset( $user_claim['family_name'] ) ? $user_claim['family_name'] : '', 1085 | ); 1086 | $user_data = apply_filters( 'openid-connect-generic-alter-user-data', $user_data, $user_claim ); 1087 | 1088 | // Create the new user. 1089 | $uid = wp_insert_user( $user_data ); 1090 | 1091 | // Make sure we didn't fail in creating the user. 1092 | if ( is_wp_error( $uid ) ) { 1093 | return new WP_Error( 'failed-user-creation', __( 'Failed user creation.', 'daggerhart-openid-connect-generic' ), $uid ); 1094 | } 1095 | 1096 | // Retrieve our new user. 1097 | $user = get_user_by( 'id', $uid ); 1098 | 1099 | // Save some meta data about this new user for the future. 1100 | add_user_meta( $user->ID, 'openid-connect-generic-subject-identity', (string) $subject_identity, true ); 1101 | 1102 | // Log the results. 1103 | $end_time = microtime( true ); 1104 | $this->logger->log( "New user created: {$user->user_login} ($uid)", __METHOD__, $end_time - $start_time ); 1105 | 1106 | // Allow plugins / themes to take action on new user creation. 1107 | do_action( 'openid-connect-generic-user-create', $user, $user_claim ); 1108 | 1109 | return $user; 1110 | } 1111 | 1112 | /** 1113 | * Update an existing user with OpenID Connect meta data 1114 | * 1115 | * @param int $uid The WordPress User ID. 1116 | * @param string $subject_identity The subject identity from the IDP. 1117 | * 1118 | * @return WP_Error|WP_User 1119 | */ 1120 | public function update_existing_user( $uid, $subject_identity ) { 1121 | // Add the OpenID Connect meta data. 1122 | update_user_meta( $uid, 'openid-connect-generic-subject-identity', strval( $subject_identity ) ); 1123 | 1124 | // Allow plugins / themes to take action on user update. 1125 | do_action( 'openid-connect-generic-user-update', $uid ); 1126 | 1127 | // Return our updated user. 1128 | return get_user_by( 'id', $uid ); 1129 | } 1130 | } 1131 | -------------------------------------------------------------------------------- /includes/openid-connect-generic-client.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2015-2020 daggerhart 9 | * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+ 10 | */ 11 | 12 | /** 13 | * OpenID_Connect_Generic_Client class. 14 | * 15 | * Plugin OIDC/oAuth client class. 16 | * 17 | * @package OpenID_Connect_Generic 18 | * @category Authentication 19 | */ 20 | class OpenID_Connect_Generic_Client { 21 | 22 | /** 23 | * The OIDC/oAuth client ID. 24 | * 25 | * @see OpenID_Connect_Generic_Option_Settings::client_id 26 | * 27 | * @var string 28 | */ 29 | private $client_id; 30 | 31 | /** 32 | * The OIDC/oAuth client secret. 33 | * 34 | * @see OpenID_Connect_Generic_Option_Settings::client_secret 35 | * 36 | * @var string 37 | */ 38 | private $client_secret; 39 | 40 | /** 41 | * The OIDC/oAuth scopes. 42 | * 43 | * @see OpenID_Connect_Generic_Option_Settings::scope 44 | * 45 | * @var string 46 | */ 47 | private $scope; 48 | 49 | /** 50 | * The OIDC/oAuth authorization endpoint URL. 51 | * 52 | * @see OpenID_Connect_Generic_Option_Settings::endpoint_login 53 | * 54 | * @var string 55 | */ 56 | private $endpoint_login; 57 | 58 | /** 59 | * The OIDC/oAuth User Information endpoint URL. 60 | * 61 | * @see OpenID_Connect_Generic_Option_Settings::endpoint_userinfo 62 | * 63 | * @var string 64 | */ 65 | private $endpoint_userinfo; 66 | 67 | /** 68 | * The OIDC/oAuth token validation endpoint URL. 69 | * 70 | * @see OpenID_Connect_Generic_Option_Settings::endpoint_token 71 | * 72 | * @var string 73 | */ 74 | private $endpoint_token; 75 | 76 | /** 77 | * The login flow "ajax" endpoint URI. 78 | * 79 | * @see OpenID_Connect_Generic_Option_Settings::redirect_uri 80 | * 81 | * @var string 82 | */ 83 | private $redirect_uri; 84 | 85 | /** 86 | * The specifically requested authentication contract at the IDP 87 | * 88 | * @see OpenID_Connect_Generic_Option_Settings::acr_values 89 | * 90 | * @var string 91 | */ 92 | private $acr_values; 93 | 94 | /** 95 | * The state time limit. States are only valid for 3 minutes. 96 | * 97 | * @see OpenID_Connect_Generic_Option_Settings::state_time_limit 98 | * 99 | * @var int 100 | */ 101 | private $state_time_limit = 180; 102 | 103 | /** 104 | * The logger object instance. 105 | * 106 | * @var OpenID_Connect_Generic_Option_Logger 107 | */ 108 | private $logger; 109 | 110 | /** 111 | * Client constructor. 112 | * 113 | * @param string $client_id @see OpenID_Connect_Generic_Option_Settings::client_id for description. 114 | * @param string $client_secret @see OpenID_Connect_Generic_Option_Settings::client_secret for description. 115 | * @param string $scope @see OpenID_Connect_Generic_Option_Settings::scope for description. 116 | * @param string $endpoint_login @see OpenID_Connect_Generic_Option_Settings::endpoint_login for description. 117 | * @param string $endpoint_userinfo @see OpenID_Connect_Generic_Option_Settings::endpoint_userinfo for description. 118 | * @param string $endpoint_token @see OpenID_Connect_Generic_Option_Settings::endpoint_token for description. 119 | * @param string $redirect_uri @see OpenID_Connect_Generic_Option_Settings::redirect_uri for description. 120 | * @param string $acr_values @see OpenID_Connect_Generic_Option_Settings::acr_values for description. 121 | * @param int $state_time_limit @see OpenID_Connect_Generic_Option_Settings::state_time_limit for description. 122 | * @param OpenID_Connect_Generic_Option_Logger $logger The plugin logging object instance. 123 | */ 124 | public function __construct( $client_id, $client_secret, $scope, $endpoint_login, $endpoint_userinfo, $endpoint_token, $redirect_uri, $acr_values, $state_time_limit, $logger ) { 125 | $this->client_id = $client_id; 126 | $this->client_secret = $client_secret; 127 | $this->scope = $scope; 128 | $this->endpoint_login = $endpoint_login; 129 | $this->endpoint_userinfo = $endpoint_userinfo; 130 | $this->endpoint_token = $endpoint_token; 131 | $this->redirect_uri = $redirect_uri; 132 | $this->acr_values = $acr_values; 133 | $this->state_time_limit = $state_time_limit; 134 | $this->logger = $logger; 135 | } 136 | 137 | /** 138 | * Provides the configured Redirect URI supplied to the IDP. 139 | * 140 | * @return string 141 | */ 142 | public function get_redirect_uri() { 143 | return $this->redirect_uri; 144 | } 145 | 146 | /** 147 | * Provide the configured IDP endpoint login URL. 148 | * 149 | * @return string 150 | */ 151 | public function get_endpoint_login_url() { 152 | return $this->endpoint_login; 153 | } 154 | 155 | /** 156 | * Validate the request for login authentication 157 | * 158 | * @param array $request The authentication request results. 159 | * 160 | * @return array|WP_Error 161 | */ 162 | public function validate_authentication_request( $request ) { 163 | // Look for an existing error of some kind. 164 | if ( isset( $request['error'] ) ) { 165 | return new WP_Error( 'unknown-error', 'An unknown error occurred.', $request ); 166 | } 167 | 168 | // Make sure we have a legitimate authentication code and valid state. 169 | if ( ! isset( $request['code'] ) ) { 170 | return new WP_Error( 'no-code', 'No authentication code present in the request.', $request ); 171 | } 172 | 173 | // Check the client request state. 174 | if ( ! isset( $request['state'] ) ) { 175 | do_action( 'openid-connect-generic-no-state-provided' ); 176 | return new WP_Error( 'missing-state', __( 'Missing state.', 'daggerhart-openid-connect-generic' ), $request ); 177 | } 178 | 179 | if ( ! $this->check_state( $request['state'] ) ) { 180 | return new WP_Error( 'invalid-state', __( 'Invalid state.', 'daggerhart-openid-connect-generic' ), $request ); 181 | } 182 | 183 | return $request; 184 | } 185 | 186 | /** 187 | * Get the authorization code from the request 188 | * 189 | * @param array|WP_Error $request The authentication request results. 190 | * 191 | * @return string|WP_Error 192 | */ 193 | public function get_authentication_code( $request ) { 194 | if ( ! isset( $request['code'] ) ) { 195 | return new WP_Error( 'missing-authentication-code', __( 'Missing authentication code.', 'daggerhart-openid-connect-generic' ), $request ); 196 | } 197 | 198 | return $request['code']; 199 | } 200 | 201 | /** 202 | * Using the authorization_code, request an authentication token from the IDP. 203 | * 204 | * @param string|WP_Error $code The authorization code. 205 | * 206 | * @return array|WP_Error 207 | */ 208 | public function request_authentication_token( $code ) { 209 | 210 | // Add Host header - required for when the openid-connect endpoint is behind a reverse-proxy. 211 | $parsed_url = parse_url( $this->endpoint_token ); 212 | $host = $parsed_url['host']; 213 | 214 | $request = array( 215 | 'body' => array( 216 | 'code' => $code, 217 | 'client_id' => $this->client_id, 218 | 'client_secret' => $this->client_secret, 219 | 'redirect_uri' => $this->redirect_uri, 220 | 'grant_type' => 'authorization_code', 221 | 'scope' => $this->scope, 222 | ), 223 | 'headers' => array( 'Host' => $host ), 224 | ); 225 | 226 | if ( ! empty( $this->acr_values ) ) { 227 | $request['body'] += array( 'acr_values' => $this->acr_values ); 228 | } 229 | 230 | // Allow modifications to the request. 231 | $request = apply_filters( 'openid-connect-generic-alter-request', $request, 'get-authentication-token' ); 232 | 233 | // Call the server and ask for a token. 234 | $start_time = microtime( true ); 235 | $response = wp_remote_post( $this->endpoint_token, $request ); 236 | $end_time = microtime( true ); 237 | $this->logger->log( $this->endpoint_token, 'request_authentication_token', $end_time - $start_time ); 238 | 239 | if ( is_wp_error( $response ) ) { 240 | $response->add( 'request_authentication_token', __( 'Request for authentication token failed.', 'daggerhart-openid-connect-generic' ) ); 241 | } 242 | 243 | return $response; 244 | } 245 | 246 | /** 247 | * Using the refresh token, request new tokens from the idp 248 | * 249 | * @param string $refresh_token The refresh token previously obtained from token response. 250 | * 251 | * @return array|WP_Error 252 | */ 253 | public function request_new_tokens( $refresh_token ) { 254 | $request = array( 255 | 'body' => array( 256 | 'refresh_token' => $refresh_token, 257 | 'client_id' => $this->client_id, 258 | 'client_secret' => $this->client_secret, 259 | 'grant_type' => 'refresh_token', 260 | ), 261 | ); 262 | 263 | // Allow modifications to the request. 264 | $request = apply_filters( 'openid-connect-generic-alter-request', $request, 'refresh-token' ); 265 | 266 | // Call the server and ask for new tokens. 267 | $start_time = microtime( true ); 268 | $response = wp_remote_post( $this->endpoint_token, $request ); 269 | $end_time = microtime( true ); 270 | $this->logger->log( $this->endpoint_token, 'request_new_tokens', $end_time - $start_time ); 271 | 272 | if ( is_wp_error( $response ) ) { 273 | $response->add( 'refresh_token', __( 'Refresh token failed.', 'daggerhart-openid-connect-generic' ) ); 274 | } 275 | 276 | return $response; 277 | } 278 | 279 | /** 280 | * Extract and decode the token body of a token response 281 | * 282 | * @param array|WP_Error $token_result The token response. 283 | * 284 | * @return array|WP_Error|null 285 | */ 286 | public function get_token_response( $token_result ) { 287 | if ( ! isset( $token_result['body'] ) ) { 288 | return new WP_Error( 'missing-token-body', __( 'Missing token body.', 'daggerhart-openid-connect-generic' ), $token_result ); 289 | } 290 | 291 | // Extract the token response from token. 292 | $token_response = json_decode( $token_result['body'], true ); 293 | 294 | // Check that the token response body was able to be parsed. 295 | if ( is_null( $token_response ) ) { 296 | return new WP_Error( 'invalid-token', __( 'Invalid token.', 'daggerhart-openid-connect-generic' ), $token_result ); 297 | } 298 | 299 | if ( isset( $token_response['error'] ) ) { 300 | $error = $token_response['error']; 301 | $error_description = $error; 302 | if ( isset( $token_response['error_description'] ) ) { 303 | $error_description = $token_response['error_description']; 304 | } 305 | return new WP_Error( $error, $error_description, $token_result ); 306 | } 307 | 308 | return $token_response; 309 | } 310 | 311 | /** 312 | * Exchange an access_token for a user_claim from the userinfo endpoint 313 | * 314 | * @param string $access_token The access token supplied from authentication user claim. 315 | * 316 | * @return array|WP_Error 317 | */ 318 | public function request_userinfo( $access_token ) { 319 | // Allow modifications to the request. 320 | $request = apply_filters( 'openid-connect-generic-alter-request', array(), 'get-userinfo' ); 321 | 322 | /* 323 | * Section 5.3.1 of the spec recommends sending the access token using the authorization header 324 | * a filter may or may not have already added headers - make sure they exist then add the token. 325 | */ 326 | if ( ! array_key_exists( 'headers', $request ) || ! is_array( $request['headers'] ) ) { 327 | $request['headers'] = array(); 328 | } 329 | 330 | $request['headers']['Authorization'] = 'Bearer ' . $access_token; 331 | 332 | // Add Host header - required for when the openid-connect endpoint is behind a reverse-proxy. 333 | $parsed_url = parse_url( $this->endpoint_userinfo ); 334 | $host = $parsed_url['host']; 335 | 336 | if ( ! empty( $parsed_url['port'] ) ) { 337 | $host .= ":{$parsed_url['port']}"; 338 | } 339 | 340 | $request['headers']['Host'] = $host; 341 | 342 | // Attempt the request including the access token in the query string for backwards compatibility. 343 | $start_time = microtime( true ); 344 | $response = wp_remote_post( $this->endpoint_userinfo, $request ); 345 | $end_time = microtime( true ); 346 | $this->logger->log( $this->endpoint_userinfo, 'request_userinfo', $end_time - $start_time ); 347 | 348 | if ( is_wp_error( $response ) ) { 349 | $response->add( 'request_userinfo', __( 'Request for userinfo failed.', 'daggerhart-openid-connect-generic' ) ); 350 | } 351 | 352 | return $response; 353 | } 354 | 355 | /** 356 | * Generate a new state, save it as a transient, and return the state hash. 357 | * 358 | * @param string $redirect_to The redirect URL to be used after IDP authentication. 359 | * 360 | * @return string 361 | */ 362 | public function new_state( $redirect_to ) { 363 | // New state w/ timestamp. 364 | $state = md5( mt_rand() . microtime( true ) ); 365 | $state_value = array( 366 | $state => array( 367 | 'redirect_to' => $redirect_to, 368 | ), 369 | ); 370 | set_transient( 'openid-connect-generic-state--' . $state, $state_value, $this->state_time_limit ); 371 | 372 | return $state; 373 | } 374 | 375 | /** 376 | * Check the existence of a given state transient. 377 | * 378 | * @param string $state The state hash to validate. 379 | * 380 | * @return bool 381 | */ 382 | public function check_state( $state ) { 383 | 384 | $state_found = true; 385 | 386 | if ( ! get_option( '_transient_openid-connect-generic-state--' . $state ) ) { 387 | do_action( 'openid-connect-generic-state-not-found', $state ); 388 | $state_found = false; 389 | } 390 | 391 | $valid = get_transient( 'openid-connect-generic-state--' . $state ); 392 | 393 | if ( ! $valid && $state_found ) { 394 | do_action( 'openid-connect-generic-state-expired', $state ); 395 | } 396 | 397 | return boolval( $valid ); 398 | } 399 | 400 | /** 401 | * Get the authorization state from the request 402 | * 403 | * @param array|WP_Error $request The authentication request results. 404 | * 405 | * @return string|WP_Error 406 | */ 407 | public function get_authentication_state( $request ) { 408 | if ( ! isset( $request['state'] ) ) { 409 | return new WP_Error( 'missing-authentication-state', __( 'Missing authentication state.', 'daggerhart-openid-connect-generic' ), $request ); 410 | } 411 | 412 | return $request['state']; 413 | } 414 | 415 | /** 416 | * Ensure that the token meets basic requirements. 417 | * 418 | * @param array $token_response The token response. 419 | * 420 | * @return bool|WP_Error 421 | */ 422 | public function validate_token_response( $token_response ) { 423 | /* 424 | * Ensure 2 specific items exist with the token response in order 425 | * to proceed with confidence: id_token and token_type == 'Bearer' 426 | */ 427 | if ( ! isset( $token_response['id_token'] ) || 428 | ! isset( $token_response['token_type'] ) || strcasecmp( $token_response['token_type'], 'Bearer' ) 429 | ) { 430 | return new WP_Error( 'invalid-token-response', 'Invalid token response', $token_response ); 431 | } 432 | 433 | return true; 434 | } 435 | 436 | /** 437 | * Extract the id_token_claim from the token_response. 438 | * 439 | * @param array $token_response The token response. 440 | * 441 | * @return array|WP_Error 442 | */ 443 | public function get_id_token_claim( $token_response ) { 444 | // Validate there is an id_token. 445 | if ( ! isset( $token_response['id_token'] ) ) { 446 | return new WP_Error( 'no-identity-token', __( 'No identity token.', 'daggerhart-openid-connect-generic' ), $token_response ); 447 | } 448 | 449 | // Break apart the id_token in the response for decoding. 450 | $tmp = explode( '.', $token_response['id_token'] ); 451 | 452 | if ( ! isset( $tmp[1] ) ) { 453 | return new WP_Error( 'missing-identity-token', __( 'Missing identity token.', 'daggerhart-openid-connect-generic' ), $token_response ); 454 | } 455 | 456 | // Extract the id_token's claims from the token. 457 | $id_token_claim = json_decode( 458 | base64_decode( 459 | str_replace( // Because token is encoded in base64 URL (and not just base64). 460 | array( '-', '_' ), 461 | array( '+', '/' ), 462 | $tmp[1] 463 | ) 464 | ), 465 | true 466 | ); 467 | 468 | return $id_token_claim; 469 | } 470 | 471 | /** 472 | * Ensure the id_token_claim contains the required values. 473 | * 474 | * @param array $id_token_claim The ID token claim. 475 | * 476 | * @return bool|WP_Error 477 | */ 478 | public function validate_id_token_claim( $id_token_claim ) { 479 | if ( ! is_array( $id_token_claim ) ) { 480 | return new WP_Error( 'bad-id-token-claim', __( 'Bad ID token claim.', 'daggerhart-openid-connect-generic' ), $id_token_claim ); 481 | } 482 | 483 | // Validate the identification data and it's value. 484 | if ( ! isset( $id_token_claim['sub'] ) || empty( $id_token_claim['sub'] ) ) { 485 | return new WP_Error( 'no-subject-identity', __( 'No subject identity.', 'daggerhart-openid-connect-generic' ), $id_token_claim ); 486 | } 487 | 488 | // Validate acr values when the option is set in the configuration. 489 | if ( ! empty( $this->acr_values ) && isset( $id_token_claim['acr'] ) ) { 490 | if ( $this->acr_values != $id_token_claim['acr'] ) { 491 | return new WP_Error( 'no-match-acr', __( 'No matching acr values.', 'daggerhart-openid-connect-generic' ), $id_token_claim ); 492 | } 493 | } 494 | 495 | return true; 496 | } 497 | 498 | /** 499 | * Attempt to exchange the access_token for a user_claim. 500 | * 501 | * @param array $token_response The token response. 502 | * 503 | * @return array|WP_Error|null 504 | */ 505 | public function get_user_claim( $token_response ) { 506 | // Send a userinfo request to get user claim. 507 | $user_claim_result = $this->request_userinfo( $token_response['access_token'] ); 508 | 509 | // Make sure we didn't get an error, and that the response body exists. 510 | if ( is_wp_error( $user_claim_result ) || ! isset( $user_claim_result['body'] ) ) { 511 | return new WP_Error( 'bad-claim', __( 'Bad user claim.', 'daggerhart-openid-connect-generic' ), $user_claim_result ); 512 | } 513 | 514 | $user_claim = json_decode( $user_claim_result['body'], true ); 515 | 516 | return $user_claim; 517 | } 518 | 519 | /** 520 | * Make sure the user_claim has all required values, and that the subject 521 | * identity matches of the id_token matches that of the user_claim. 522 | * 523 | * @param array $user_claim The authenticated user claim. 524 | * @param array $id_token_claim The ID token claim. 525 | * 526 | * @return bool|WP_Error 527 | */ 528 | public function validate_user_claim( $user_claim, $id_token_claim ) { 529 | // Validate the user claim. 530 | if ( ! is_array( $user_claim ) ) { 531 | return new WP_Error( 'invalid-user-claim', __( 'Invalid user claim.', 'daggerhart-openid-connect-generic' ), $user_claim ); 532 | } 533 | 534 | // Allow for errors from the IDP. 535 | if ( isset( $user_claim['error'] ) ) { 536 | $message = __( 'Error from the IDP.', 'daggerhart-openid-connect-generic' ); 537 | if ( ! empty( $user_claim['error_description'] ) ) { 538 | $message = $user_claim['error_description']; 539 | } 540 | return new WP_Error( 'invalid-user-claim-' . $user_claim['error'], $message, $user_claim ); 541 | } 542 | 543 | // Make sure the id_token sub equals the user_claim sub, according to spec. 544 | if ( $id_token_claim['sub'] !== $user_claim['sub'] ) { 545 | return new WP_Error( 'incorrect-user-claim', __( 'Incorrect user claim.', 'daggerhart-openid-connect-generic' ), func_get_args() ); 546 | } 547 | 548 | // Allow for other plugins to alter the login success. 549 | $login_user = apply_filters( 'openid-connect-generic-user-login-test', true, $user_claim ); 550 | 551 | if ( ! $login_user ) { 552 | return new WP_Error( 'unauthorized', __( 'Unauthorized access.', 'daggerhart-openid-connect-generic' ), $login_user ); 553 | } 554 | 555 | return true; 556 | } 557 | 558 | /** 559 | * Retrieve the subject identity from the id_token. 560 | * 561 | * @param array $id_token_claim The ID token claim. 562 | * 563 | * @return mixed 564 | */ 565 | public function get_subject_identity( $id_token_claim ) { 566 | return $id_token_claim['sub']; 567 | } 568 | } 569 | -------------------------------------------------------------------------------- /includes/openid-connect-generic-login-form.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2015-2020 daggerhart 9 | * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+ 10 | */ 11 | 12 | /** 13 | * OpenID_Connect_Generic_Login_Form class. 14 | * 15 | * Login form and login button handling. 16 | * 17 | * @package OpenID_Connect_Generic 18 | * @category Login 19 | */ 20 | class OpenID_Connect_Generic_Login_Form { 21 | 22 | /** 23 | * Plugin settings object. 24 | * 25 | * @var OpenID_Connect_Generic_Option_Settings 26 | */ 27 | private $settings; 28 | 29 | /** 30 | * Plugin client wrapper instance. 31 | * 32 | * @var OpenID_Connect_Generic_Client_Wrapper 33 | */ 34 | private $client_wrapper; 35 | 36 | /** 37 | * The class constructor. 38 | * 39 | * @param OpenID_Connect_Generic_Option_Settings $settings A plugin settings object instance. 40 | * @param OpenID_Connect_Generic_Client_Wrapper $client_wrapper A plugin client wrapper object instance. 41 | */ 42 | public function __construct( $settings, $client_wrapper ) { 43 | $this->settings = $settings; 44 | $this->client_wrapper = $client_wrapper; 45 | } 46 | 47 | /** 48 | * Create an instance of the OpenID_Connect_Generic_Login_Form class. 49 | * 50 | * @param OpenID_Connect_Generic_Option_Settings $settings A plugin settings object instance. 51 | * @param OpenID_Connect_Generic_Client_Wrapper $client_wrapper A plugin client wrapper object instance. 52 | * 53 | * @return void 54 | */ 55 | public static function register( $settings, $client_wrapper ) { 56 | $login_form = new self( $settings, $client_wrapper ); 57 | 58 | // Alter the login form as dictated by settings. 59 | add_filter( 'login_message', array( $login_form, 'handle_login_page' ), 99 ); 60 | 61 | // Add a shortcode for the login button. 62 | add_shortcode( 'openid_connect_generic_login_button', array( $login_form, 'make_login_button' ) ); 63 | 64 | $login_form->handle_redirect_login_type_auto(); 65 | } 66 | 67 | /** 68 | * Auto Login redirect. 69 | * 70 | * @return void 71 | */ 72 | public function handle_redirect_login_type_auto() { 73 | 74 | if ( 'wp-login.php' == $GLOBALS['pagenow'] 75 | && ( 'auto' == $this->settings->login_type || ! empty( $_GET['force_redirect'] ) ) 76 | // Don't send users to the IDP on logout or post password protected authentication. 77 | && ( ! isset( $_GET['action'] ) || ! in_array( $_GET['action'], array( 'logout', 'postpass' ) ) ) 78 | // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WP Login Form doesn't have a nonce. 79 | && ! isset( $_POST['wp-submit'] ) ) { 80 | if ( ! isset( $_GET['login-error'] ) ) { 81 | wp_redirect( $this->client_wrapper->get_authentication_url() ); 82 | exit; 83 | } else { 84 | add_action( 'login_footer', array( $this, 'remove_login_form' ), 99 ); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Implements filter login_message. 91 | * 92 | * @param string $message The text message to display on the login page. 93 | * 94 | * @return string 95 | */ 96 | public function handle_login_page( $message ) { 97 | 98 | if ( isset( $_GET['login-error'] ) ) { 99 | $error_message = ! empty( $_GET['message'] ) ? sanitize_text_field( wp_unslash( $_GET['message'] ) ) : 'Unknown error.'; 100 | $message .= $this->make_error_output( sanitize_text_field( wp_unslash( $_GET['login-error'] ) ), $error_message ); 101 | } 102 | 103 | // Login button is appended to existing messages in case of error. 104 | $message .= $this->make_login_button(); 105 | 106 | return $message; 107 | } 108 | 109 | /** 110 | * Display an error message to the user. 111 | * 112 | * @param string $error_code The error code. 113 | * @param string $error_message The error message test. 114 | * 115 | * @return string 116 | */ 117 | public function make_error_output( $error_code, $error_message ) { 118 | 119 | ob_start(); 120 | ?> 121 |
122 | : 123 | 124 |
125 | __( 'Login with OpenID Connect', 'daggerhart-openid-connect-generic' ), 142 | ), 143 | $atts, 144 | 'openid_connect_generic_login_button' 145 | ); 146 | 147 | $text = apply_filters( 'openid-connect-generic-login-button-text', $atts['button_text'] ); 148 | $text = esc_html( $text ); 149 | 150 | $href = $this->client_wrapper->get_authentication_url( $atts ); 151 | $href = esc_url_raw( $href ); 152 | 153 | $login_button = << 155 | {$text} 156 | 157 | HTML; 158 | 159 | return $login_button; 160 | } 161 | 162 | /** 163 | * Removes the login form from the HTML DOM 164 | * 165 | * @return void 166 | */ 167 | public function remove_login_form() { 168 | ?> 169 | 176 | 8 | * @copyright 2015-2023 daggerhart 9 | * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+ 10 | */ 11 | 12 | /** 13 | * OpenID_Connect_Generic_Option_Logger class. 14 | * 15 | * Simple class for logging messages to the options table. 16 | * 17 | * @package OpenID_Connect_Generic 18 | * @category Logging 19 | */ 20 | class OpenID_Connect_Generic_Option_Logger { 21 | 22 | /** 23 | * Thw WordPress option name/key. 24 | * 25 | * @var string 26 | */ 27 | const OPTION_NAME = 'openid-connect-generic-logs'; 28 | 29 | /** 30 | * The default message type. 31 | * 32 | * @var string 33 | */ 34 | private $default_message_type = 'none'; 35 | 36 | /** 37 | * The number of items to keep in the log. 38 | * 39 | * @var int 40 | */ 41 | private $log_limit = 1000; 42 | 43 | /** 44 | * Whether or not logging is enabled. 45 | * 46 | * @var bool 47 | */ 48 | private $logging_enabled = true; 49 | 50 | /** 51 | * Internal cache of logs. 52 | * 53 | * @var array 54 | */ 55 | private $logs; 56 | 57 | /** 58 | * Setup the logger according to the needs of the instance. 59 | * 60 | * @param string|null $default_message_type The log message type. 61 | * @param bool|TRUE|null $logging_enabled Whether logging is enabled. 62 | * @param int|null $log_limit The log entry limit. 63 | */ 64 | public function __construct( $default_message_type = null, $logging_enabled = null, $log_limit = null ) { 65 | if ( ! is_null( $default_message_type ) ) { 66 | $this->default_message_type = $default_message_type; 67 | } 68 | if ( ! is_null( $logging_enabled ) ) { 69 | $this->logging_enabled = boolval( $logging_enabled ); 70 | } 71 | if ( ! is_null( $log_limit ) ) { 72 | $this->log_limit = intval( $log_limit ); 73 | } 74 | } 75 | 76 | /** 77 | * Save an array of data to the logs. 78 | * 79 | * @param string|array|WP_Error $data The log message data. 80 | * @param string|null $type The log message type. 81 | * @param float|null $processing_time Optional event processing time. 82 | * @param int|null $time The log message timestamp (default: time()). 83 | * @param int|null $user_ID The current WordPress user ID (default: get_current_user_id()). 84 | * @param string|null $request_uri The related HTTP request URI (default: $_SERVER['REQUEST_URI']|'Unknown'). 85 | * 86 | * @return bool 87 | */ 88 | public function log( $data, $type = null, $processing_time = null, $time = null, $user_ID = null, $request_uri = null ) { 89 | if ( boolval( $this->logging_enabled ) ) { 90 | $logs = $this->get_logs(); 91 | $logs[] = $this->make_message( $data, $type, $processing_time, $time, $user_ID, $request_uri ); 92 | $logs = $this->upkeep_logs( $logs ); 93 | return $this->save_logs( $logs ); 94 | } 95 | 96 | return false; 97 | } 98 | 99 | /** 100 | * Retrieve all log messages. 101 | * 102 | * @return array 103 | */ 104 | public function get_logs() { 105 | if ( empty( $this->logs ) ) { 106 | $this->logs = get_option( self::OPTION_NAME, array() ); 107 | } 108 | 109 | // Call the upkeep_logs function to give the appearance that logs have been reduced to the $this->log_limit. 110 | // The logs are actually limited during a logging action but the logger isn't available during a simple settings update. 111 | return $this->upkeep_logs( $this->logs ); 112 | } 113 | 114 | /** 115 | * Get the name of the option where this log is stored. 116 | * 117 | * @return string 118 | */ 119 | public function get_option_name() { 120 | return self::OPTION_NAME; 121 | } 122 | 123 | /** 124 | * Create a message array containing the data and other information. 125 | * 126 | * @param string|array|WP_Error $data The log message data. 127 | * @param string|null $type The log message type. 128 | * @param float|null $processing_time Optional event processing time. 129 | * @param int|null $time The log message timestamp (default: time()). 130 | * @param int|null $user_ID The current WordPress user ID (default: get_current_user_id()). 131 | * @param string|null $request_uri The related HTTP request URI (default: $_SERVER['REQUEST_URI']|'Unknown'). 132 | * 133 | * @return array 134 | */ 135 | private function make_message( $data, $type, $processing_time, $time, $user_ID, $request_uri ) { 136 | // Determine the type of message. 137 | if ( empty( $type ) ) { 138 | $type = $this->default_message_type; 139 | 140 | if ( is_array( $data ) && isset( $data['type'] ) ) { 141 | $type = $data['type']; 142 | unset( $data['type'] ); 143 | } 144 | 145 | if ( is_wp_error( $data ) ) { 146 | $type = $data->get_error_code(); 147 | $data = $data->get_error_message( $type ); 148 | } 149 | } 150 | 151 | if ( empty( $request_uri ) ) { 152 | $request_uri = ( ! empty( $_SERVER['REQUEST_URI'] ) ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : 'Unknown'; 153 | $request_uri = preg_replace( '/code=([^&]+)/i', 'code=', $request_uri ); 154 | } 155 | 156 | // Construct the message. 157 | $message = array( 158 | 'type' => $type, 159 | 'time' => ! empty( $time ) ? $time : time(), 160 | 'user_ID' => ! is_null( $user_ID ) ? $user_ID : get_current_user_id(), 161 | 'uri' => $request_uri, 162 | 'data' => $data, 163 | 'processing_time' => $processing_time, 164 | ); 165 | 166 | return $message; 167 | } 168 | 169 | /** 170 | * Keep the log count under the limit. 171 | * 172 | * @param array $logs The plugin logs. 173 | * 174 | * @return array 175 | */ 176 | private function upkeep_logs( $logs ) { 177 | $items_to_remove = count( $logs ) - $this->log_limit; 178 | 179 | if ( $items_to_remove > 0 ) { 180 | // Only keep the last $log_limit messages from the end. 181 | $logs = array_slice( $logs, $items_to_remove ); 182 | } 183 | 184 | return $logs; 185 | } 186 | 187 | /** 188 | * Save the log messages. 189 | * 190 | * @param array $logs The array of log messages. 191 | * 192 | * @return bool 193 | */ 194 | private function save_logs( $logs ) { 195 | // Save the logs. 196 | $this->logs = $logs; 197 | return update_option( self::OPTION_NAME, $logs, false ); 198 | } 199 | 200 | /** 201 | * Clear all log messages. 202 | * 203 | * @return void 204 | */ 205 | public function clear_logs() { 206 | $this->save_logs( array() ); 207 | } 208 | 209 | /** 210 | * Get a simple html table of all the logs. 211 | * 212 | * @param array $logs The array of log messages. 213 | * 214 | * @return string 215 | */ 216 | public function get_logs_table( $logs = array() ) { 217 | if ( empty( $logs ) ) { 218 | $logs = $this->get_logs(); 219 | } 220 | $logs = array_reverse( $logs ); 221 | 222 | ini_set( 'xdebug.var_display_max_depth', '-1' ); 223 | 224 | ob_start(); 225 | ?> 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 256 | 257 | 258 | 259 | 260 |
235 |
236 | 237 | 238 |
239 |
240 | 241 | 242 |
243 |
244 | 245 | user_login : '0' ); ?> 246 |
247 |
248 | 249 | 250 |
251 |
252 | 253 | 254 |
255 |
261 | 8 | * @copyright 2015-2023 daggerhart 9 | * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+ 10 | */ 11 | 12 | /** 13 | * OpenId_Connect_Generic_Option_Settings class. 14 | * 15 | * WordPress options handling. 16 | * 17 | * @package OpenID_Connect_Generic 18 | * @category Settings 19 | * 20 | * Legacy Settings: 21 | * 22 | * @property string $ep_login The login endpoint. 23 | * @property string $ep_token The token endpoint. 24 | * @property string $ep_userinfo The userinfo endpoint. 25 | * 26 | * OAuth Client Settings: 27 | * 28 | * @property string $login_type How the client (login form) should provide login options. 29 | * @property string $client_id The ID the client will be recognized as when connecting the to Identity provider server. 30 | * @property string $client_secret The secret key the IDP server expects from the client. 31 | * @property string $scope The list of scopes this client should access. 32 | * @property string $endpoint_login The IDP authorization endpoint URL. 33 | * @property string $endpoint_userinfo The IDP User information endpoint URL. 34 | * @property string $endpoint_token The IDP token validation endpoint URL. 35 | * @property string $endpoint_end_session The IDP logout endpoint URL. 36 | * @property string $acr_values The Authentication contract as defined on the IDP. 37 | * 38 | * Non-standard Settings: 39 | * 40 | * @property bool $no_sslverify The flag to enable/disable SSL verification during authorization. 41 | * @property int $http_request_timeout The timeout for requests made to the IDP. Default value is 5. 42 | * @property string $identity_key The key in the user claim array to find the user's identification data. 43 | * @property string $nickname_key The key in the user claim array to find the user's nickname. 44 | * @property string $email_format The key(s) in the user claim array to formulate the user's email address. 45 | * @property string $displayname_format The key(s) in the user claim array to formulate the user's display name. 46 | * @property bool $identify_with_username The flag which indicates how the user's identity will be determined. 47 | * @property int $state_time_limit The valid time limit of the state, in seconds. Defaults to 180 seconds. 48 | * 49 | * Plugin Settings: 50 | * 51 | * @property bool $enforce_privacy The flag to indicates whether a user us required to be authenticated to access the site. 52 | * @property bool $alternate_redirect_uri The flag to indicate whether to use the alternative redirect URI. 53 | * @property bool $token_refresh_enable The flag whether to support refresh tokens by IDPs. 54 | * @property bool $link_existing_users The flag to indicate whether to link to existing WordPress-only accounts or greturn an error. 55 | * @property bool $create_if_does_not_exist The flag to indicate whether to create new users or not. 56 | * @property bool $redirect_user_back The flag to indicate whether to redirect the user back to the page on which they started. 57 | * @property bool $redirect_on_logout The flag to indicate whether to redirect to the login screen on session expiration. 58 | * @property bool $enable_logging The flag to enable/disable logging. 59 | * @property int $log_limit The maximum number of log entries to keep. 60 | */ 61 | class OpenID_Connect_Generic_Option_Settings { 62 | 63 | /** 64 | * WordPress option name/key. 65 | * 66 | * @var string 67 | */ 68 | const OPTION_NAME = 'openid_connect_generic_settings'; 69 | 70 | /** 71 | * Stored option values array. 72 | * 73 | * @var array 74 | */ 75 | private $values; 76 | 77 | /** 78 | * Default plugin settings values. 79 | * 80 | * @var array 81 | */ 82 | private $default_settings; 83 | 84 | /** 85 | * List of settings that can be defined by environment variables. 86 | * 87 | * @var array 88 | */ 89 | private $environment_settings = array( 90 | 'client_id' => 'OIDC_CLIENT_ID', 91 | 'client_secret' => 'OIDC_CLIENT_SECRET', 92 | 'endpoint_end_session' => 'OIDC_ENDPOINT_LOGOUT_URL', 93 | 'endpoint_login' => 'OIDC_ENDPOINT_LOGIN_URL', 94 | 'endpoint_token' => 'OIDC_ENDPOINT_TOKEN_URL', 95 | 'endpoint_userinfo' => 'OIDC_ENDPOINT_USERINFO_URL', 96 | 'login_type' => 'OIDC_LOGIN_TYPE', 97 | 'scope' => 'OIDC_CLIENT_SCOPE', 98 | 'create_if_does_not_exist' => 'OIDC_CREATE_IF_DOES_NOT_EXIST', 99 | 'enforce_privacy' => 'OIDC_ENFORCE_PRIVACY', 100 | 'link_existing_users' => 'OIDC_LINK_EXISTING_USERS', 101 | 'redirect_on_logout' => 'OIDC_REDIRECT_ON_LOGOUT', 102 | 'redirect_user_back' => 'OIDC_REDIRECT_USER_BACK', 103 | 'acr_values' => 'OIDC_ACR_VALUES', 104 | 'enable_logging' => 'OIDC_ENABLE_LOGGING', 105 | 'log_limit' => 'OIDC_LOG_LIMIT', 106 | ); 107 | 108 | /** 109 | * The class constructor. 110 | * 111 | * @param array $default_settings The default plugin settings values. 112 | * @param bool $granular_defaults The granular defaults. 113 | */ 114 | public function __construct( $default_settings = array(), $granular_defaults = true ) { 115 | $this->default_settings = $default_settings; 116 | $this->values = array(); 117 | 118 | $this->values = (array) get_option( self::OPTION_NAME, $this->default_settings ); 119 | 120 | // For each defined environment variable/constant be sure the settings key is set. 121 | foreach ( $this->environment_settings as $key => $constant ) { 122 | if ( defined( $constant ) ) { 123 | $this->__set( $key, constant( $constant ) ); 124 | } 125 | } 126 | 127 | if ( $granular_defaults ) { 128 | $this->values = array_replace_recursive( $this->default_settings, $this->values ); 129 | } 130 | } 131 | 132 | /** 133 | * Magic getter for settings. 134 | * 135 | * @param string $key The array key/option name. 136 | * 137 | * @return mixed 138 | */ 139 | public function __get( $key ) { 140 | if ( isset( $this->values[ $key ] ) ) { 141 | return $this->values[ $key ]; 142 | } 143 | } 144 | 145 | /** 146 | * Magic setter for settings. 147 | * 148 | * @param string $key The array key/option name. 149 | * @param mixed $value The option value. 150 | * 151 | * @return void 152 | */ 153 | public function __set( $key, $value ) { 154 | $this->values[ $key ] = $value; 155 | } 156 | 157 | /** 158 | * Magic method to check is an attribute isset. 159 | * 160 | * @param string $key The array key/option name. 161 | * 162 | * @return bool 163 | */ 164 | public function __isset( $key ) { 165 | return isset( $this->values[ $key ] ); 166 | } 167 | 168 | /** 169 | * Magic method to clear an attribute. 170 | * 171 | * @param string $key The array key/option name. 172 | * 173 | * @return void 174 | */ 175 | public function __unset( $key ) { 176 | unset( $this->values[ $key ] ); 177 | } 178 | 179 | /** 180 | * Get the plugin settings array. 181 | * 182 | * @return array 183 | */ 184 | public function get_values() { 185 | return $this->values; 186 | } 187 | 188 | /** 189 | * Get the plugin WordPress options name. 190 | * 191 | * @return string 192 | */ 193 | public function get_option_name() { 194 | return self::OPTION_NAME; 195 | } 196 | 197 | /** 198 | * Save the plugin options to the WordPress options table. 199 | * 200 | * @return void 201 | */ 202 | public function save() { 203 | 204 | // For each defined environment variable/constant be sure it isn't saved to the database. 205 | foreach ( $this->environment_settings as $key => $constant ) { 206 | if ( defined( $constant ) ) { 207 | $this->__unset( $key ); 208 | } 209 | } 210 | 211 | update_option( self::OPTION_NAME, $this->values ); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /includes/openid-connect-generic-settings-page.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2015-2023 daggerhart 9 | * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+ 10 | */ 11 | 12 | /** 13 | * OpenID_Connect_Generic_Settings_Page class. 14 | * 15 | * Admin settings page. 16 | * 17 | * @package OpenID_Connect_Generic 18 | * @category Settings 19 | */ 20 | class OpenID_Connect_Generic_Settings_Page { 21 | 22 | /** 23 | * Local copy of the settings provided by the base plugin. 24 | * 25 | * @var OpenID_Connect_Generic_Option_Settings 26 | */ 27 | private $settings; 28 | 29 | /** 30 | * Instance of the plugin logger. 31 | * 32 | * @var OpenID_Connect_Generic_Option_Logger 33 | */ 34 | private $logger; 35 | 36 | /** 37 | * The controlled list of settings & associated defined during 38 | * construction for i18n reasons. 39 | * 40 | * @var array 41 | */ 42 | private $settings_fields = array(); 43 | 44 | /** 45 | * Options page slug. 46 | * 47 | * @var string 48 | */ 49 | private $options_page_name = 'openid-connect-generic-settings'; 50 | 51 | /** 52 | * Options page settings group name. 53 | * 54 | * @var string 55 | */ 56 | private $settings_field_group; 57 | 58 | /** 59 | * Settings page class constructor. 60 | * 61 | * @param OpenID_Connect_Generic_Option_Settings $settings The plugin settings object. 62 | * @param OpenID_Connect_Generic_Option_Logger $logger The plugin logging class object. 63 | */ 64 | public function __construct( OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ) { 65 | 66 | $this->settings = $settings; 67 | $this->logger = $logger; 68 | $this->settings_field_group = $this->settings->get_option_name() . '-group'; 69 | 70 | $fields = $this->get_settings_fields(); 71 | 72 | // Some simple pre-processing. 73 | foreach ( $fields as $key => &$field ) { 74 | $field['key'] = $key; 75 | $field['name'] = $this->settings->get_option_name() . '[' . $key . ']'; 76 | } 77 | 78 | // Allow alterations of the fields. 79 | $this->settings_fields = $fields; 80 | } 81 | 82 | /** 83 | * Hook the settings page into WordPress. 84 | * 85 | * @param OpenID_Connect_Generic_Option_Settings $settings A plugin settings object instance. 86 | * @param OpenID_Connect_Generic_Option_Logger $logger A plugin logger object instance. 87 | * 88 | * @return void 89 | */ 90 | public static function register( OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ) { 91 | $settings_page = new self( $settings, $logger ); 92 | 93 | // Add our options page the the admin menu. 94 | add_action( 'admin_menu', array( $settings_page, 'admin_menu' ) ); 95 | 96 | // Register our settings. 97 | add_action( 'admin_init', array( $settings_page, 'admin_init' ) ); 98 | } 99 | 100 | /** 101 | * Implements hook admin_menu to add our options/settings page to the 102 | * dashboard menu. 103 | * 104 | * @return void 105 | */ 106 | public function admin_menu() { 107 | add_options_page( 108 | __( 'OpenID Connect - Generic Client', 'daggerhart-openid-connect-generic' ), 109 | __( 'OpenID Connect Client', 'daggerhart-openid-connect-generic' ), 110 | 'manage_options', 111 | $this->options_page_name, 112 | array( $this, 'settings_page' ) 113 | ); 114 | } 115 | 116 | /** 117 | * Implements hook admin_init to register our settings. 118 | * 119 | * @return void 120 | */ 121 | public function admin_init() { 122 | register_setting( 123 | $this->settings_field_group, 124 | $this->settings->get_option_name(), 125 | array( 126 | $this, 127 | 'sanitize_settings', 128 | ) 129 | ); 130 | 131 | add_settings_section( 132 | 'client_settings', 133 | __( 'Client Settings', 'daggerhart-openid-connect-generic' ), 134 | array( $this, 'client_settings_description' ), 135 | $this->options_page_name 136 | ); 137 | 138 | add_settings_section( 139 | 'user_settings', 140 | __( 'WordPress User Settings', 'daggerhart-openid-connect-generic' ), 141 | array( $this, 'user_settings_description' ), 142 | $this->options_page_name 143 | ); 144 | 145 | add_settings_section( 146 | 'authorization_settings', 147 | __( 'Authorization Settings', 'daggerhart-openid-connect-generic' ), 148 | array( $this, 'authorization_settings_description' ), 149 | $this->options_page_name 150 | ); 151 | 152 | add_settings_section( 153 | 'log_settings', 154 | __( 'Log Settings', 'daggerhart-openid-connect-generic' ), 155 | array( $this, 'log_settings_description' ), 156 | $this->options_page_name 157 | ); 158 | 159 | // Preprocess fields and add them to the page. 160 | foreach ( $this->settings_fields as $key => $field ) { 161 | // Make sure each key exists in the settings array. 162 | if ( ! isset( $this->settings->{ $key } ) ) { 163 | $this->settings->{ $key } = null; 164 | } 165 | 166 | // Determine appropriate output callback. 167 | switch ( $field['type'] ) { 168 | case 'checkbox': 169 | $callback = 'do_checkbox'; 170 | break; 171 | 172 | case 'select': 173 | $callback = 'do_select'; 174 | break; 175 | 176 | case 'text': 177 | default: 178 | $callback = 'do_text_field'; 179 | break; 180 | } 181 | 182 | // Add the field. 183 | add_settings_field( 184 | $key, 185 | $field['title'], 186 | array( $this, $callback ), 187 | $this->options_page_name, 188 | $field['section'], 189 | $field 190 | ); 191 | } 192 | } 193 | 194 | /** 195 | * Get the plugin settings fields definition. 196 | * 197 | * @return array 198 | */ 199 | private function get_settings_fields() { 200 | 201 | /** 202 | * Simple settings fields have: 203 | * 204 | * - title 205 | * - description 206 | * - type ( checkbox | text | select ) 207 | * - section - settings/option page section ( client_settings | authorization_settings ) 208 | * - example (optional example will appear beneath description and be wrapped in ) 209 | */ 210 | $fields = array( 211 | 'login_type' => array( 212 | 'title' => __( 'Login Type', 'daggerhart-openid-connect-generic' ), 213 | 'description' => __( 'Select how the client (login form) should provide login options.', 'daggerhart-openid-connect-generic' ), 214 | 'type' => 'select', 215 | 'options' => array( 216 | 'button' => __( 'OpenID Connect button on login form', 'daggerhart-openid-connect-generic' ), 217 | 'auto' => __( 'Auto Login - SSO', 'daggerhart-openid-connect-generic' ), 218 | ), 219 | 'disabled' => defined( 'OIDC_LOGIN_TYPE' ), 220 | 'section' => 'client_settings', 221 | ), 222 | 'client_id' => array( 223 | 'title' => __( 'Client ID', 'daggerhart-openid-connect-generic' ), 224 | 'description' => __( 'The ID this client will be recognized as when connecting the to Identity provider server.', 'daggerhart-openid-connect-generic' ), 225 | 'example' => 'my-wordpress-client-id', 226 | 'type' => 'text', 227 | 'disabled' => defined( 'OIDC_CLIENT_ID' ), 228 | 'section' => 'client_settings', 229 | ), 230 | 'client_secret' => array( 231 | 'title' => __( 'Client Secret Key', 'daggerhart-openid-connect-generic' ), 232 | 'description' => __( 'Arbitrary secret key the server expects from this client. Can be anything, but should be very unique.', 'daggerhart-openid-connect-generic' ), 233 | 'type' => 'text', 234 | 'disabled' => defined( 'OIDC_CLIENT_SECRET' ), 235 | 'section' => 'client_settings', 236 | ), 237 | 'scope' => array( 238 | 'title' => __( 'OpenID Scope', 'daggerhart-openid-connect-generic' ), 239 | 'description' => __( 'Space separated list of scopes this client should access.', 'daggerhart-openid-connect-generic' ), 240 | 'example' => 'email profile openid offline_access', 241 | 'type' => 'text', 242 | 'disabled' => defined( 'OIDC_CLIENT_SCOPE' ), 243 | 'section' => 'client_settings', 244 | ), 245 | 'endpoint_login' => array( 246 | 'title' => __( 'Login Endpoint URL', 'daggerhart-openid-connect-generic' ), 247 | 'description' => __( 'Identify provider authorization endpoint.', 'daggerhart-openid-connect-generic' ), 248 | 'example' => 'https://example.com/oauth2/authorize', 249 | 'type' => 'text', 250 | 'disabled' => defined( 'OIDC_ENDPOINT_LOGIN_URL' ), 251 | 'section' => 'client_settings', 252 | ), 253 | 'endpoint_userinfo' => array( 254 | 'title' => __( 'Userinfo Endpoint URL', 'daggerhart-openid-connect-generic' ), 255 | 'description' => __( 'Identify provider User information endpoint.', 'daggerhart-openid-connect-generic' ), 256 | 'example' => 'https://example.com/oauth2/UserInfo', 257 | 'type' => 'text', 258 | 'disabled' => defined( 'OIDC_ENDPOINT_USERINFO_URL' ), 259 | 'section' => 'client_settings', 260 | ), 261 | 'endpoint_token' => array( 262 | 'title' => __( 'Token Validation Endpoint URL', 'daggerhart-openid-connect-generic' ), 263 | 'description' => __( 'Identify provider token endpoint.', 'daggerhart-openid-connect-generic' ), 264 | 'example' => 'https://example.com/oauth2/token', 265 | 'type' => 'text', 266 | 'disabled' => defined( 'OIDC_ENDPOINT_TOKEN_URL' ), 267 | 'section' => 'client_settings', 268 | ), 269 | 'endpoint_end_session' => array( 270 | 'title' => __( 'End Session Endpoint URL', 'daggerhart-openid-connect-generic' ), 271 | 'description' => __( 'Identify provider logout endpoint.', 'daggerhart-openid-connect-generic' ), 272 | 'example' => 'https://example.com/oauth2/logout', 273 | 'type' => 'text', 274 | 'disabled' => defined( 'OIDC_ENDPOINT_LOGOUT_URL' ), 275 | 'section' => 'client_settings', 276 | ), 277 | 'acr_values' => array( 278 | 'title' => __( 'ACR values', 'daggerhart-openid-connect-generic' ), 279 | 'description' => __( 'Use a specific defined authentication contract from the IDP - optional.', 'daggerhart-openid-connect-generic' ), 280 | 'type' => 'text', 281 | 'disabled' => defined( 'OIDC_ACR_VALUES' ), 282 | 'section' => 'client_settings', 283 | ), 284 | 'identity_key' => array( 285 | 'title' => __( 'Identity Key', 'daggerhart-openid-connect-generic' ), 286 | 'description' => __( 'Where in the user claim array to find the user\'s identification data. Possible standard values: preferred_username, name, or sub. If you\'re having trouble, use "sub".', 'daggerhart-openid-connect-generic' ), 287 | 'example' => 'preferred_username', 288 | 'type' => 'text', 289 | 'section' => 'client_settings', 290 | ), 291 | 'no_sslverify' => array( 292 | 'title' => __( 'Disable SSL Verify', 'daggerhart-openid-connect-generic' ), 293 | // translators: %1$s HTML tags for layout/styles, %2$s closing HTML tag for styles. 294 | 'description' => sprintf( __( 'Do not require SSL verification during authorization. The OAuth extension uses curl to make the request. By default CURL will generally verify the SSL certificate to see if its valid an issued by an accepted CA. This setting disabled that verification.%1$sNot recommended for production sites.%2$s', 'daggerhart-openid-connect-generic' ), '
', '' ), 295 | 'type' => 'checkbox', 296 | 'section' => 'client_settings', 297 | ), 298 | 'http_request_timeout' => array( 299 | 'title' => __( 'HTTP Request Timeout', 'daggerhart-openid-connect-generic' ), 300 | 'description' => __( 'Set the timeout for requests made to the IDP. Default value is 5.', 'daggerhart-openid-connect-generic' ), 301 | 'example' => 30, 302 | 'type' => 'text', 303 | 'section' => 'client_settings', 304 | ), 305 | 'enforce_privacy' => array( 306 | 'title' => __( 'Enforce Privacy', 'daggerhart-openid-connect-generic' ), 307 | 'description' => __( 'Require users be logged in to see the site.', 'daggerhart-openid-connect-generic' ), 308 | 'type' => 'checkbox', 309 | 'disabled' => defined( 'OIDC_ENFORCE_PRIVACY' ), 310 | 'section' => 'authorization_settings', 311 | ), 312 | 'alternate_redirect_uri' => array( 313 | 'title' => __( 'Alternate Redirect URI', 'daggerhart-openid-connect-generic' ), 314 | 'description' => __( 'Provide an alternative redirect route. Useful if your server is causing issues with the default admin-ajax method. You must flush rewrite rules after changing this setting. This can be done by saving the Permalinks settings page.', 'daggerhart-openid-connect-generic' ), 315 | 'type' => 'checkbox', 316 | 'section' => 'authorization_settings', 317 | ), 318 | 'nickname_key' => array( 319 | 'title' => __( 'Nickname Key', 'daggerhart-openid-connect-generic' ), 320 | 'description' => __( 'Where in the user claim array to find the user\'s nickname. Possible standard values: preferred_username, name, or sub.', 'daggerhart-openid-connect-generic' ), 321 | 'example' => 'preferred_username', 322 | 'type' => 'text', 323 | 'section' => 'client_settings', 324 | ), 325 | 'email_format' => array( 326 | 'title' => __( 'Email Formatting', 'daggerhart-openid-connect-generic' ), 327 | 'description' => __( 'String from which the user\'s email address is built. Specify "{email}" as long as the user claim contains an email claim.', 'daggerhart-openid-connect-generic' ), 328 | 'example' => '{email}', 329 | 'type' => 'text', 330 | 'section' => 'client_settings', 331 | ), 332 | 'displayname_format' => array( 333 | 'title' => __( 'Display Name Formatting', 'daggerhart-openid-connect-generic' ), 334 | 'description' => __( 'String from which the user\'s display name is built.', 'daggerhart-openid-connect-generic' ), 335 | 'example' => '{given_name} {family_name}', 336 | 'type' => 'text', 337 | 'section' => 'client_settings', 338 | ), 339 | 'identify_with_username' => array( 340 | 'title' => __( 'Identify with User Name', 'daggerhart-openid-connect-generic' ), 341 | 'description' => __( 'If checked, the user\'s identity will be determined by the user name instead of the email address.', 'daggerhart-openid-connect-generic' ), 342 | 'type' => 'checkbox', 343 | 'section' => 'client_settings', 344 | ), 345 | 'state_time_limit' => array( 346 | 'title' => __( 'State time limit', 'daggerhart-openid-connect-generic' ), 347 | 'description' => __( 'State valid time in seconds. Defaults to 180', 'daggerhart-openid-connect-generic' ), 348 | 'type' => 'number', 349 | 'section' => 'client_settings', 350 | ), 351 | 'token_refresh_enable' => array( 352 | 'title' => __( 'Enable Refresh Token', 'daggerhart-openid-connect-generic' ), 353 | 'description' => __( 'If checked, support refresh tokens used to obtain access tokens from supported IDPs.', 'daggerhart-openid-connect-generic' ), 354 | 'type' => 'checkbox', 355 | 'section' => 'client_settings', 356 | ), 357 | 'link_existing_users' => array( 358 | 'title' => __( 'Link Existing Users', 'daggerhart-openid-connect-generic' ), 359 | 'description' => __( 'If a WordPress account already exists with the same identity as a newly-authenticated user over OpenID Connect, login as that user instead of generating an error.', 'daggerhart-openid-connect-generic' ), 360 | 'type' => 'checkbox', 361 | 'disabled' => defined( 'OIDC_LINK_EXISTING_USERS' ), 362 | 'section' => 'user_settings', 363 | ), 364 | 'create_if_does_not_exist' => array( 365 | 'title' => __( 'Create user if does not exist', 'daggerhart-openid-connect-generic' ), 366 | 'description' => __( 'If the user identity is not linked to an existing WordPress user, it is created. If this setting is not enabled, and if the user authenticates with an account which is not linked to an existing WordPress user, then the authentication will fail.', 'daggerhart-openid-connect-generic' ), 367 | 'type' => 'checkbox', 368 | 'disabled' => defined( 'OIDC_CREATE_IF_DOES_NOT_EXIST' ), 369 | 'section' => 'user_settings', 370 | ), 371 | 'redirect_user_back' => array( 372 | 'title' => __( 'Redirect Back to Origin Page', 'daggerhart-openid-connect-generic' ), 373 | 'description' => __( 'After a successful OpenID Connect authentication, this will redirect the user back to the page on which they clicked the OpenID Connect login button. This will cause the login process to proceed in a traditional WordPress fashion. For example, users logging in through the default wp-login.php page would end up on the WordPress Dashboard and users logging in through the WooCommerce "My Account" page would end up on their account page.', 'daggerhart-openid-connect-generic' ), 374 | 'type' => 'checkbox', 375 | 'disabled' => defined( 'OIDC_REDIRECT_USER_BACK' ), 376 | 'section' => 'user_settings', 377 | ), 378 | 'redirect_on_logout' => array( 379 | 'title' => __( 'Redirect to the login screen when session is expired', 'daggerhart-openid-connect-generic' ), 380 | 'description' => __( 'When enabled, this will automatically redirect the user back to the WordPress login page if their access token has expired.', 'daggerhart-openid-connect-generic' ), 381 | 'type' => 'checkbox', 382 | 'disabled' => defined( 'OIDC_REDIRECT_ON_LOGOUT' ), 383 | 'section' => 'user_settings', 384 | ), 385 | 'enable_logging' => array( 386 | 'title' => __( 'Enable Logging', 'daggerhart-openid-connect-generic' ), 387 | 'description' => __( 'Very simple log messages for debugging purposes.', 'daggerhart-openid-connect-generic' ), 388 | 'type' => 'checkbox', 389 | 'disabled' => defined( 'OIDC_ENABLE_LOGGING' ), 390 | 'section' => 'log_settings', 391 | ), 392 | 'log_limit' => array( 393 | 'title' => __( 'Log Limit', 'daggerhart-openid-connect-generic' ), 394 | 'description' => __( 'Number of items to keep in the log. These logs are stored as an option in the database, so space is limited.', 'daggerhart-openid-connect-generic' ), 395 | 'type' => 'number', 396 | 'disabled' => defined( 'OIDC_LOG_LIMIT' ), 397 | 'section' => 'log_settings', 398 | ), 399 | ); 400 | 401 | return apply_filters( 'openid-connect-generic-settings-fields', $fields ); 402 | } 403 | 404 | /** 405 | * Sanitization callback for settings/option page. 406 | * 407 | * @param array $input The submitted settings values. 408 | * 409 | * @return array 410 | */ 411 | public function sanitize_settings( $input ) { 412 | $options = array(); 413 | 414 | // Loop through settings fields to control what we're saving. 415 | foreach ( $this->settings_fields as $key => $field ) { 416 | if ( isset( $input[ $key ] ) ) { 417 | $options[ $key ] = sanitize_text_field( trim( $input[ $key ] ) ); 418 | } else { 419 | $options[ $key ] = ''; 420 | } 421 | } 422 | 423 | return $options; 424 | } 425 | 426 | /** 427 | * Output the options/settings page. 428 | * 429 | * @return void 430 | */ 431 | public function settings_page() { 432 | wp_enqueue_style( 'daggerhart-openid-connect-generic-admin', plugin_dir_url( __DIR__ ) . 'css/styles-admin.css', array(), OpenID_Connect_Generic::VERSION, 'all' ); 433 | 434 | $redirect_uri = admin_url( 'admin-ajax.php?action=openid-connect-authorize' ); 435 | 436 | if ( $this->settings->alternate_redirect_uri ) { 437 | $redirect_uri = site_url( '/openid-connect-authorize' ); 438 | } 439 | ?> 440 |
441 |

442 | 443 |
444 | settings_field_group ); 446 | do_settings_sections( $this->options_page_name ); 447 | submit_button(); 448 | 449 | // Simple debug to view settings array. 450 | if ( isset( $_GET['debug'] ) ) { 451 | var_dump( $this->settings->get_values() ); 452 | } 453 | ?> 454 |
455 | 456 |

457 | 458 |

459 | 460 | 461 |

462 |

463 | 464 | [openid_connect_generic_login_button] 465 |

466 |

467 | 468 | [openid_connect_generic_auth_url] 469 |

470 | 471 | settings->enable_logging ) { ?> 472 |

473 |
474 | logger->get_logs_table() ); ?> 475 |
476 | 477 | 478 |
479 | 491 | 496 | value="settings->{ $field['key'] } ); ?>"> 497 | do_field_description( $field ); 499 | } 500 | 501 | /** 502 | * Output a checkbox for a boolean setting. 503 | * - hidden field is default value so we don't have to check isset() on save. 504 | * 505 | * @param array $field The settings field definition array. 506 | * 507 | * @return void 508 | */ 509 | public function do_checkbox( $field ) { 510 | $hidden_value = 0; 511 | if ( ! empty( $field['disabled'] ) && boolval( $field['disabled'] ) === true ) { 512 | $hidden_value = intval( $this->settings->{ $field['key'] } ); 513 | } 514 | ?> 515 | 516 | 520 | value="1" 521 | settings->{ $field['key'] }, 1 ); ?>> 522 | do_field_description( $field ); 524 | } 525 | 526 | /** 527 | * Output a select control. 528 | * 529 | * @param array $field The settings field definition array. 530 | * 531 | * @return void 532 | */ 533 | public function do_select( $field ) { 534 | $current_value = isset( $this->settings->{ $field['key'] } ) ? $this->settings->{ $field['key'] } : ''; 535 | ?> 536 | 545 | do_field_description( $field ); 547 | } 548 | 549 | /** 550 | * Output the field description, and example if present. 551 | * 552 | * @param array $field The settings field definition array. 553 | * 554 | * @return void 555 | */ 556 | public function do_field_description( $field ) { 557 | ?> 558 |

559 | 560 | 561 |
: 562 | 563 | 564 |

565 | \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: en\n" 16 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 17 | "X-Poedit-Country: United States\n" 18 | "X-Poedit-SourceCharset: UTF-8\n" 19 | "X-Poedit-KeywordsList: " 20 | "__;_e;_x:1,2c;_ex:1,2c;_n:1,2;_nx:1,2,4c;_n_noop:1,2;_nx_noop:1,2,3c;esc_" 21 | "attr__;esc_html__;esc_attr_e;esc_html_e;esc_attr_x:1,2c;esc_html_x:1,2c;\n" 22 | "X-Poedit-Basepath: ../\n" 23 | "X-Poedit-SearchPath-0: .\n" 24 | "X-Poedit-Bookmarks: \n" 25 | "X-Textdomain-Support: yes\n" 26 | "X-Generator: grunt-wp-i18n 1.0.3\n" 27 | 28 | #: includes/openid-connect-generic-client-wrapper.php:293 29 | msgid "Session expired. Please login again." 30 | msgstr "" 31 | 32 | #: includes/openid-connect-generic-client-wrapper.php:540 33 | msgid "User identity is not linked to an existing WordPress user." 34 | msgstr "" 35 | 36 | #: includes/openid-connect-generic-client-wrapper.php:598 37 | msgid "Invalid user." 38 | msgstr "" 39 | 40 | #: includes/openid-connect-generic-client-wrapper.php:816 41 | msgid "No appropriate username found." 42 | msgstr "" 43 | 44 | #: includes/openid-connect-generic-client-wrapper.php:826 45 | #. translators: %1$s is the santitized version of the username from the IDP. 46 | msgid "Username %1$s could not be sanitized." 47 | msgstr "" 48 | 49 | #: includes/openid-connect-generic-client-wrapper.php:848 50 | #. translators: %1$s is the configured User Claim nickname key. 51 | msgid "No nickname found in user claim using key: %1$s." 52 | msgstr "" 53 | 54 | #: includes/openid-connect-generic-client-wrapper.php:945 55 | msgid "User claim incomplete." 56 | msgstr "" 57 | 58 | #: includes/openid-connect-generic-client-wrapper.php:1048 59 | msgid "Bad user claim result." 60 | msgstr "" 61 | 62 | #: includes/openid-connect-generic-client-wrapper.php:1114 63 | msgid "Can not authorize." 64 | msgstr "" 65 | 66 | #: includes/openid-connect-generic-client-wrapper.php:1143 67 | msgid "Failed user creation." 68 | msgstr "" 69 | 70 | #: includes/openid-connect-generic-client.php:176 71 | msgid "Missing state." 72 | msgstr "" 73 | 74 | #: includes/openid-connect-generic-client.php:180 75 | msgid "Invalid state." 76 | msgstr "" 77 | 78 | #: includes/openid-connect-generic-client.php:195 79 | msgid "Missing authentication code." 80 | msgstr "" 81 | 82 | #: includes/openid-connect-generic-client.php:240 83 | msgid "Request for authentication token failed." 84 | msgstr "" 85 | 86 | #: includes/openid-connect-generic-client.php:273 87 | msgid "Refresh token failed." 88 | msgstr "" 89 | 90 | #: includes/openid-connect-generic-client.php:288 91 | msgid "Missing token body." 92 | msgstr "" 93 | 94 | #: includes/openid-connect-generic-client.php:296 95 | msgid "Invalid token." 96 | msgstr "" 97 | 98 | #: includes/openid-connect-generic-client.php:349 99 | msgid "Request for userinfo failed." 100 | msgstr "" 101 | 102 | #: includes/openid-connect-generic-client.php:409 103 | msgid "Missing authentication state." 104 | msgstr "" 105 | 106 | #: includes/openid-connect-generic-client.php:446 107 | msgid "No identity token." 108 | msgstr "" 109 | 110 | #: includes/openid-connect-generic-client.php:453 111 | msgid "Missing identity token." 112 | msgstr "" 113 | 114 | #: includes/openid-connect-generic-client.php:480 115 | msgid "Bad ID token claim." 116 | msgstr "" 117 | 118 | #: includes/openid-connect-generic-client.php:485 119 | msgid "No subject identity." 120 | msgstr "" 121 | 122 | #: includes/openid-connect-generic-client.php:491 123 | msgid "No matching acr values." 124 | msgstr "" 125 | 126 | #: includes/openid-connect-generic-client.php:511 127 | msgid "Bad user claim." 128 | msgstr "" 129 | 130 | #: includes/openid-connect-generic-client.php:531 131 | msgid "Invalid user claim." 132 | msgstr "" 133 | 134 | #: includes/openid-connect-generic-client.php:536 135 | msgid "Error from the IDP." 136 | msgstr "" 137 | 138 | #: includes/openid-connect-generic-client.php:545 139 | msgid "Incorrect user claim." 140 | msgstr "" 141 | 142 | #: includes/openid-connect-generic-client.php:552 143 | msgid "Unauthorized access." 144 | msgstr "" 145 | 146 | #: includes/openid-connect-generic-login-form.php:122 147 | #. translators: %1$s is the error code from the IDP. 148 | msgid "ERROR (%1$s)" 149 | msgstr "" 150 | 151 | #: includes/openid-connect-generic-login-form.php:141 152 | msgid "Login with OpenID Connect" 153 | msgstr "" 154 | 155 | #: includes/openid-connect-generic-option-logger.php:228 156 | msgid "Details" 157 | msgstr "" 158 | 159 | #: includes/openid-connect-generic-option-logger.php:229 160 | msgid "Data" 161 | msgstr "" 162 | 163 | #: includes/openid-connect-generic-option-logger.php:236 164 | msgid "Date" 165 | msgstr "" 166 | 167 | #: includes/openid-connect-generic-option-logger.php:240 168 | msgid "Type" 169 | msgstr "" 170 | 171 | #: includes/openid-connect-generic-option-logger.php:244 172 | msgid "User" 173 | msgstr "" 174 | 175 | #: includes/openid-connect-generic-option-logger.php:248 176 | msgid "URI " 177 | msgstr "" 178 | 179 | #: includes/openid-connect-generic-option-logger.php:252 180 | msgid "Response Time (sec)" 181 | msgstr "" 182 | 183 | #: includes/openid-connect-generic-settings-page.php:108 184 | msgid "OpenID Connect - Generic Client" 185 | msgstr "" 186 | 187 | #: includes/openid-connect-generic-settings-page.php:109 188 | msgid "OpenID Connect Client" 189 | msgstr "" 190 | 191 | #: includes/openid-connect-generic-settings-page.php:133 192 | msgid "Client Settings" 193 | msgstr "" 194 | 195 | #: includes/openid-connect-generic-settings-page.php:140 196 | msgid "WordPress User Settings" 197 | msgstr "" 198 | 199 | #: includes/openid-connect-generic-settings-page.php:147 200 | msgid "Authorization Settings" 201 | msgstr "" 202 | 203 | #: includes/openid-connect-generic-settings-page.php:154 204 | msgid "Log Settings" 205 | msgstr "" 206 | 207 | #: includes/openid-connect-generic-settings-page.php:212 208 | msgid "Login Type" 209 | msgstr "" 210 | 211 | #: includes/openid-connect-generic-settings-page.php:213 212 | msgid "Select how the client (login form) should provide login options." 213 | msgstr "" 214 | 215 | #: includes/openid-connect-generic-settings-page.php:216 216 | msgid "OpenID Connect button on login form" 217 | msgstr "" 218 | 219 | #: includes/openid-connect-generic-settings-page.php:217 220 | msgid "Auto Login - SSO" 221 | msgstr "" 222 | 223 | #: includes/openid-connect-generic-settings-page.php:223 224 | msgid "Client ID" 225 | msgstr "" 226 | 227 | #: includes/openid-connect-generic-settings-page.php:224 228 | msgid "" 229 | "The ID this client will be recognized as when connecting the to Identity " 230 | "provider server." 231 | msgstr "" 232 | 233 | #: includes/openid-connect-generic-settings-page.php:231 234 | msgid "Client Secret Key" 235 | msgstr "" 236 | 237 | #: includes/openid-connect-generic-settings-page.php:232 238 | msgid "" 239 | "Arbitrary secret key the server expects from this client. Can be anything, " 240 | "but should be very unique." 241 | msgstr "" 242 | 243 | #: includes/openid-connect-generic-settings-page.php:238 244 | msgid "OpenID Scope" 245 | msgstr "" 246 | 247 | #: includes/openid-connect-generic-settings-page.php:239 248 | msgid "Space separated list of scopes this client should access." 249 | msgstr "" 250 | 251 | #: includes/openid-connect-generic-settings-page.php:246 252 | msgid "Login Endpoint URL" 253 | msgstr "" 254 | 255 | #: includes/openid-connect-generic-settings-page.php:247 256 | msgid "Identify provider authorization endpoint." 257 | msgstr "" 258 | 259 | #: includes/openid-connect-generic-settings-page.php:254 260 | msgid "Userinfo Endpoint URL" 261 | msgstr "" 262 | 263 | #: includes/openid-connect-generic-settings-page.php:255 264 | msgid "Identify provider User information endpoint." 265 | msgstr "" 266 | 267 | #: includes/openid-connect-generic-settings-page.php:262 268 | msgid "Token Validation Endpoint URL" 269 | msgstr "" 270 | 271 | #: includes/openid-connect-generic-settings-page.php:263 272 | msgid "Identify provider token endpoint." 273 | msgstr "" 274 | 275 | #: includes/openid-connect-generic-settings-page.php:270 276 | msgid "End Session Endpoint URL" 277 | msgstr "" 278 | 279 | #: includes/openid-connect-generic-settings-page.php:271 280 | msgid "Identify provider logout endpoint." 281 | msgstr "" 282 | 283 | #: includes/openid-connect-generic-settings-page.php:278 284 | msgid "ACR values" 285 | msgstr "" 286 | 287 | #: includes/openid-connect-generic-settings-page.php:279 288 | msgid "Use a specific defined authentication contract from the IDP - optional." 289 | msgstr "" 290 | 291 | #: includes/openid-connect-generic-settings-page.php:285 292 | msgid "Identity Key" 293 | msgstr "" 294 | 295 | #: includes/openid-connect-generic-settings-page.php:286 296 | msgid "" 297 | "Where in the user claim array to find the user's identification data. " 298 | "Possible standard values: preferred_username, name, or sub. If you're " 299 | "having trouble, use \"sub\"." 300 | msgstr "" 301 | 302 | #: includes/openid-connect-generic-settings-page.php:292 303 | msgid "Disable SSL Verify" 304 | msgstr "" 305 | 306 | #: includes/openid-connect-generic-settings-page.php:294 307 | #. translators: %1$s HTML tags for layout/styles, %2$s closing HTML tag for 308 | #. styles. 309 | msgid "" 310 | "Do not require SSL verification during authorization. The OAuth extension " 311 | "uses curl to make the request. By default CURL will generally verify the " 312 | "SSL certificate to see if its valid an issued by an accepted CA. This " 313 | "setting disabled that verification.%1$sNot recommended for production " 314 | "sites.%2$s" 315 | msgstr "" 316 | 317 | #: includes/openid-connect-generic-settings-page.php:299 318 | msgid "HTTP Request Timeout" 319 | msgstr "" 320 | 321 | #: includes/openid-connect-generic-settings-page.php:300 322 | msgid "Set the timeout for requests made to the IDP. Default value is 5." 323 | msgstr "" 324 | 325 | #: includes/openid-connect-generic-settings-page.php:306 326 | msgid "Enforce Privacy" 327 | msgstr "" 328 | 329 | #: includes/openid-connect-generic-settings-page.php:307 330 | msgid "Require users be logged in to see the site." 331 | msgstr "" 332 | 333 | #: includes/openid-connect-generic-settings-page.php:313 334 | msgid "Alternate Redirect URI" 335 | msgstr "" 336 | 337 | #: includes/openid-connect-generic-settings-page.php:314 338 | msgid "" 339 | "Provide an alternative redirect route. Useful if your server is causing " 340 | "issues with the default admin-ajax method. You must flush rewrite rules " 341 | "after changing this setting. This can be done by saving the Permalinks " 342 | "settings page." 343 | msgstr "" 344 | 345 | #: includes/openid-connect-generic-settings-page.php:319 346 | msgid "Nickname Key" 347 | msgstr "" 348 | 349 | #: includes/openid-connect-generic-settings-page.php:320 350 | msgid "" 351 | "Where in the user claim array to find the user's nickname. Possible " 352 | "standard values: preferred_username, name, or sub." 353 | msgstr "" 354 | 355 | #: includes/openid-connect-generic-settings-page.php:326 356 | msgid "Email Formatting" 357 | msgstr "" 358 | 359 | #: includes/openid-connect-generic-settings-page.php:327 360 | msgid "" 361 | "String from which the user's email address is built. Specify \"{email}\" as " 362 | "long as the user claim contains an email claim." 363 | msgstr "" 364 | 365 | #: includes/openid-connect-generic-settings-page.php:333 366 | msgid "Display Name Formatting" 367 | msgstr "" 368 | 369 | #: includes/openid-connect-generic-settings-page.php:334 370 | msgid "String from which the user's display name is built." 371 | msgstr "" 372 | 373 | #: includes/openid-connect-generic-settings-page.php:340 374 | msgid "Identify with User Name" 375 | msgstr "" 376 | 377 | #: includes/openid-connect-generic-settings-page.php:341 378 | msgid "" 379 | "If checked, the user's identity will be determined by the user name instead " 380 | "of the email address." 381 | msgstr "" 382 | 383 | #: includes/openid-connect-generic-settings-page.php:346 384 | msgid "State time limit" 385 | msgstr "" 386 | 387 | #: includes/openid-connect-generic-settings-page.php:347 388 | msgid "State valid time in seconds. Defaults to 180" 389 | msgstr "" 390 | 391 | #: includes/openid-connect-generic-settings-page.php:352 392 | msgid "Enable Refresh Token" 393 | msgstr "" 394 | 395 | #: includes/openid-connect-generic-settings-page.php:353 396 | msgid "" 397 | "If checked, support refresh tokens used to obtain access tokens from " 398 | "supported IDPs." 399 | msgstr "" 400 | 401 | #: includes/openid-connect-generic-settings-page.php:358 402 | msgid "Link Existing Users" 403 | msgstr "" 404 | 405 | #: includes/openid-connect-generic-settings-page.php:359 406 | msgid "" 407 | "If a WordPress account already exists with the same identity as a " 408 | "newly-authenticated user over OpenID Connect, login as that user instead of " 409 | "generating an error." 410 | msgstr "" 411 | 412 | #: includes/openid-connect-generic-settings-page.php:365 413 | msgid "Create user if does not exist" 414 | msgstr "" 415 | 416 | #: includes/openid-connect-generic-settings-page.php:366 417 | msgid "" 418 | "If the user identity is not linked to an existing WordPress user, it is " 419 | "created. If this setting is not enabled, and if the user authenticates with " 420 | "an account which is not linked to an existing WordPress user, then the " 421 | "authentication will fail." 422 | msgstr "" 423 | 424 | #: includes/openid-connect-generic-settings-page.php:372 425 | msgid "Redirect Back to Origin Page" 426 | msgstr "" 427 | 428 | #: includes/openid-connect-generic-settings-page.php:373 429 | msgid "" 430 | "After a successful OpenID Connect authentication, this will redirect the " 431 | "user back to the page on which they clicked the OpenID Connect login " 432 | "button. This will cause the login process to proceed in a traditional " 433 | "WordPress fashion. For example, users logging in through the default " 434 | "wp-login.php page would end up on the WordPress Dashboard and users logging " 435 | "in through the WooCommerce \"My Account\" page would end up on their " 436 | "account page." 437 | msgstr "" 438 | 439 | #: includes/openid-connect-generic-settings-page.php:379 440 | msgid "Redirect to the login screen when session is expired" 441 | msgstr "" 442 | 443 | #: includes/openid-connect-generic-settings-page.php:380 444 | msgid "" 445 | "When enabled, this will automatically redirect the user back to the " 446 | "WordPress login page if their access token has expired." 447 | msgstr "" 448 | 449 | #: includes/openid-connect-generic-settings-page.php:386 450 | msgid "Enable Logging" 451 | msgstr "" 452 | 453 | #: includes/openid-connect-generic-settings-page.php:387 454 | msgid "Very simple log messages for debugging purposes." 455 | msgstr "" 456 | 457 | #: includes/openid-connect-generic-settings-page.php:393 458 | msgid "Log Limit" 459 | msgstr "" 460 | 461 | #: includes/openid-connect-generic-settings-page.php:394 462 | msgid "" 463 | "Number of items to keep in the log. These logs are stored as an option in " 464 | "the database, so space is limited." 465 | msgstr "" 466 | 467 | #: includes/openid-connect-generic-settings-page.php:456 468 | msgid "Notes" 469 | msgstr "" 470 | 471 | #: includes/openid-connect-generic-settings-page.php:459 472 | msgid "Redirect URI" 473 | msgstr "" 474 | 475 | #: includes/openid-connect-generic-settings-page.php:463 476 | msgid "Login Button Shortcode" 477 | msgstr "" 478 | 479 | #: includes/openid-connect-generic-settings-page.php:467 480 | msgid "Authentication URL Shortcode" 481 | msgstr "" 482 | 483 | #: includes/openid-connect-generic-settings-page.php:472 484 | msgid "Logs" 485 | msgstr "" 486 | 487 | #: includes/openid-connect-generic-settings-page.php:561 488 | msgid "Example" 489 | msgstr "" 490 | 491 | #: includes/openid-connect-generic-settings-page.php:574 492 | msgid "Enter your OpenID Connect identity provider settings." 493 | msgstr "" 494 | 495 | #: includes/openid-connect-generic-settings-page.php:583 496 | msgid "Modify the interaction between OpenID Connect and WordPress users." 497 | msgstr "" 498 | 499 | #: includes/openid-connect-generic-settings-page.php:592 500 | msgid "Control the authorization mechanics of the site." 501 | msgstr "" 502 | 503 | #: includes/openid-connect-generic-settings-page.php:601 504 | msgid "Log information about login attempts through OpenID Connect Generic." 505 | msgstr "" 506 | 507 | #: openid-connect-generic.php:242 508 | msgid "Private site" 509 | msgstr "" 510 | 511 | #. Plugin Name of the plugin/theme 512 | msgid "OpenID Connect Generic" 513 | msgstr "" 514 | 515 | #. Plugin URI of the plugin/theme 516 | msgid "https://github.com/daggerhart/openid-connect-generic" 517 | msgstr "" 518 | 519 | #. Description of the plugin/theme 520 | msgid "" 521 | "Connect to an OpenID Connect identity provider using Authorization Code " 522 | "Flow." 523 | msgstr "" 524 | 525 | #. Author of the plugin/theme 526 | msgid "daggerhart" 527 | msgstr "" 528 | 529 | #. Author URI of the plugin/theme 530 | msgid "http://www.daggerhart.com" 531 | msgstr "" -------------------------------------------------------------------------------- /openid-connect-generic.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright 2015-2023 daggerhart 12 | * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+ 13 | * @link https://github.com/daggerhart 14 | * 15 | * @wordpress-plugin 16 | * Plugin Name: OpenID Connect Generic 17 | * Plugin URI: https://github.com/daggerhart/openid-connect-generic 18 | * Description: Connect to an OpenID Connect identity provider using Authorization Code Flow. 19 | * Version: 3.10.0 20 | * Requires at least: 5.0 21 | * Requires PHP: 7.4 22 | * Author: daggerhart 23 | * Author URI: http://www.daggerhart.com 24 | * Text Domain: daggerhart-openid-connect-generic 25 | * Domain Path: /languages 26 | * License: GPL-2.0+ 27 | * License URI: http://www.gnu.org/licenses/gpl-2.0.txt 28 | * GitHub Plugin URI: https://github.com/daggerhart/openid-connect-generic 29 | */ 30 | 31 | /* 32 | Notes 33 | Spec Doc - http://openid.net/specs/openid-connect-basic-1_0-32.html 34 | 35 | Filters 36 | - openid-connect-generic-alter-request - 3 args: request array, plugin settings, specific request op 37 | - openid-connect-generic-settings-fields - modify the fields provided on the settings page 38 | - openid-connect-generic-login-button-text - modify the login button text 39 | - openid-connect-generic-cookie-redirect-url - modify the redirect url stored as a cookie 40 | - openid-connect-generic-user-login-test - (bool) should the user be logged in based on their claim 41 | - openid-connect-generic-user-creation-test - (bool) should the user be created based on their claim 42 | - openid-connect-generic-auth-url - modify the authentication url 43 | - openid-connect-generic-alter-user-claim - modify the user_claim before a new user is created 44 | - openid-connect-generic-alter-user-data - modify user data before a new user is created 45 | - openid-connect-modify-token-response-before-validation - modify the token response before validation 46 | - openid-connect-modify-id-token-claim-before-validation - modify the token claim before validation 47 | 48 | Actions 49 | - openid-connect-generic-user-create - 2 args: fires when a new user is created by this plugin 50 | - openid-connect-generic-user-update - 1 arg: user ID, fires when user is updated by this plugin 51 | - openid-connect-generic-update-user-using-current-claim - 2 args: fires every time an existing user logs in and the claims are updated. 52 | - openid-connect-generic-redirect-user-back - 2 args: $redirect_url, $user. Allows interruption of redirect during login. 53 | - openid-connect-generic-user-logged-in - 1 arg: $user, fires when user is logged in. 54 | - openid-connect-generic-cron-daily - daily cron action 55 | - openid-connect-generic-state-not-found - the given state does not exist in the database, regardless of its expiration. 56 | - openid-connect-generic-state-expired - the given state exists, but expired before this login attempt. 57 | 58 | Callable actions 59 | 60 | User Meta 61 | - openid-connect-generic-subject-identity - the identity of the user provided by the idp 62 | - openid-connect-generic-last-id-token-claim - the user's most recent id_token claim, decoded 63 | - openid-connect-generic-last-user-claim - the user's most recent user_claim 64 | - openid-connect-generic-last-token-response - the user's most recent token response 65 | 66 | Options 67 | - openid_connect_generic_settings - plugin settings 68 | - openid-connect-generic-valid-states - locally stored generated states 69 | */ 70 | 71 | 72 | /** 73 | * OpenID_Connect_Generic class. 74 | * 75 | * Defines plugin initialization functionality. 76 | * 77 | * @package OpenID_Connect_Generic 78 | * @category General 79 | */ 80 | class OpenID_Connect_Generic { 81 | 82 | /** 83 | * Singleton instance of self 84 | * 85 | * @var OpenID_Connect_Generic 86 | */ 87 | protected static $_instance = null; 88 | 89 | /** 90 | * Plugin version. 91 | * 92 | * @var string 93 | */ 94 | const VERSION = '3.10.0'; 95 | 96 | /** 97 | * Plugin settings. 98 | * 99 | * @var OpenID_Connect_Generic_Option_Settings 100 | */ 101 | private $settings; 102 | 103 | /** 104 | * Plugin logs. 105 | * 106 | * @var OpenID_Connect_Generic_Option_Logger 107 | */ 108 | private $logger; 109 | 110 | /** 111 | * Openid Connect Generic client 112 | * 113 | * @var OpenID_Connect_Generic_Client 114 | */ 115 | private $client; 116 | 117 | /** 118 | * Client wrapper. 119 | * 120 | * @var OpenID_Connect_Generic_Client_Wrapper 121 | */ 122 | public $client_wrapper; 123 | 124 | /** 125 | * Setup the plugin 126 | * 127 | * @param OpenID_Connect_Generic_Option_Settings $settings The settings object. 128 | * @param OpenID_Connect_Generic_Option_Logger $logger The loggin object. 129 | * 130 | * @return void 131 | */ 132 | public function __construct( OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ) { 133 | $this->settings = $settings; 134 | $this->logger = $logger; 135 | self::$_instance = $this; 136 | } 137 | 138 | // @codeCoverageIgnoreStart 139 | 140 | /** 141 | * WordPress Hook 'init'. 142 | * 143 | * @return void 144 | */ 145 | public function init() { 146 | 147 | $this->client = new OpenID_Connect_Generic_Client( 148 | $this->settings->client_id, 149 | $this->settings->client_secret, 150 | $this->settings->scope, 151 | $this->settings->endpoint_login, 152 | $this->settings->endpoint_userinfo, 153 | $this->settings->endpoint_token, 154 | $this->get_redirect_uri( $this->settings ), 155 | $this->settings->acr_values, 156 | $this->get_state_time_limit( $this->settings ), 157 | $this->logger 158 | ); 159 | 160 | $this->client_wrapper = OpenID_Connect_Generic_Client_Wrapper::register( $this->client, $this->settings, $this->logger ); 161 | if ( defined( 'WP_CLI' ) && WP_CLI ) { 162 | return; 163 | } 164 | 165 | OpenID_Connect_Generic_Login_Form::register( $this->settings, $this->client_wrapper ); 166 | 167 | // Add a shortcode to get the auth URL. 168 | add_shortcode( 'openid_connect_generic_auth_url', array( $this->client_wrapper, 'get_authentication_url' ) ); 169 | 170 | // Add actions to our scheduled cron jobs. 171 | add_action( 'openid-connect-generic-cron-daily', array( $this, 'cron_states_garbage_collection' ) ); 172 | 173 | $this->upgrade(); 174 | 175 | if ( is_admin() ) { 176 | OpenID_Connect_Generic_Settings_Page::register( $this->settings, $this->logger ); 177 | } 178 | } 179 | 180 | /** 181 | * Get the default redirect URI. 182 | * 183 | * @param OpenID_Connect_Generic_Option_Settings $settings The settings object. 184 | * 185 | * @return string 186 | */ 187 | public function get_redirect_uri( OpenID_Connect_Generic_Option_Settings $settings ) { 188 | $redirect_uri = admin_url( 'admin-ajax.php?action=openid-connect-authorize' ); 189 | 190 | if ( $settings->alternate_redirect_uri ) { 191 | $redirect_uri = site_url( '/openid-connect-authorize' ); 192 | } 193 | 194 | return $redirect_uri; 195 | } 196 | 197 | /** 198 | * Get the default state time limit. 199 | * 200 | * @param OpenID_Connect_Generic_Option_Settings $settings The settings object. 201 | * 202 | * @return int 203 | */ 204 | public function get_state_time_limit( OpenID_Connect_Generic_Option_Settings $settings ) { 205 | $state_time_limit = 180; 206 | // State time limit cannot be zero. 207 | if ( $settings->state_time_limit ) { 208 | $state_time_limit = intval( $settings->state_time_limit ); 209 | } 210 | 211 | return $state_time_limit; 212 | } 213 | 214 | /** 215 | * Check if privacy enforcement is enabled, and redirect users that aren't 216 | * logged in. 217 | * 218 | * @return void 219 | */ 220 | public function enforce_privacy_redirect() { 221 | if ( $this->settings->enforce_privacy && ! is_user_logged_in() ) { 222 | // The client endpoint relies on the wp-admin ajax endpoint. 223 | if ( 224 | ! defined( 'DOING_AJAX' ) || 225 | ! boolval( constant( 'DOING_AJAX' ) ) || 226 | ! isset( $_GET['action'] ) || 227 | 'openid-connect-authorize' != $_GET['action'] ) { 228 | auth_redirect(); 229 | } 230 | } 231 | } 232 | 233 | /** 234 | * Enforce privacy settings for rss feeds. 235 | * 236 | * @param string $content The content. 237 | * 238 | * @return mixed 239 | */ 240 | public function enforce_privacy_feeds( $content ) { 241 | if ( $this->settings->enforce_privacy && ! is_user_logged_in() ) { 242 | $content = __( 'Private site', 'daggerhart-openid-connect-generic' ); 243 | } 244 | return $content; 245 | } 246 | 247 | /** 248 | * Handle plugin upgrades 249 | * 250 | * @return void 251 | */ 252 | public function upgrade() { 253 | $last_version = get_option( 'openid-connect-generic-plugin-version', 0 ); 254 | $settings = $this->settings; 255 | 256 | if ( version_compare( self::VERSION, $last_version, '>' ) ) { 257 | // An upgrade is required. 258 | self::setup_cron_jobs(); 259 | 260 | // @todo move this to another file for upgrade scripts 261 | if ( isset( $settings->ep_login ) ) { 262 | $settings->endpoint_login = $settings->ep_login; 263 | $settings->endpoint_token = $settings->ep_token; 264 | $settings->endpoint_userinfo = $settings->ep_userinfo; 265 | 266 | unset( $settings->ep_login, $settings->ep_token, $settings->ep_userinfo ); 267 | $settings->save(); 268 | } 269 | 270 | // Update the stored version number. 271 | update_option( 'openid-connect-generic-plugin-version', self::VERSION ); 272 | } 273 | } 274 | 275 | /** 276 | * Expire state transients by attempting to access them and allowing the 277 | * transient's own mechanisms to delete any that have expired. 278 | * 279 | * @return void 280 | */ 281 | public function cron_states_garbage_collection() { 282 | global $wpdb; 283 | $states = $wpdb->get_col( "SELECT `option_name` FROM {$wpdb->options} WHERE `option_name` LIKE '_transient_openid-connect-generic-state--%'" ); 284 | 285 | if ( ! empty( $states ) ) { 286 | foreach ( $states as $state ) { 287 | $transient = str_replace( '_transient_', '', $state ); 288 | get_transient( $transient ); 289 | } 290 | } 291 | } 292 | 293 | /** 294 | * Ensure cron jobs are added to the schedule. 295 | * 296 | * @return void 297 | */ 298 | public static function setup_cron_jobs() { 299 | if ( ! wp_next_scheduled( 'openid-connect-generic-cron-daily' ) ) { 300 | wp_schedule_event( time(), 'daily', 'openid-connect-generic-cron-daily' ); 301 | } 302 | } 303 | 304 | /** 305 | * Activation hook. 306 | * 307 | * @return void 308 | */ 309 | public static function activation() { 310 | self::setup_cron_jobs(); 311 | } 312 | 313 | /** 314 | * Deactivation hook. 315 | * 316 | * @return void 317 | */ 318 | public static function deactivation() { 319 | wp_clear_scheduled_hook( 'openid-connect-generic-cron-daily' ); 320 | } 321 | 322 | /** 323 | * Simple autoloader. 324 | * 325 | * @param string $class The class name. 326 | * 327 | * @return void 328 | */ 329 | public static function autoload( $class ) { 330 | $prefix = 'OpenID_Connect_Generic_'; 331 | 332 | if ( stripos( $class, $prefix ) !== 0 ) { 333 | return; 334 | } 335 | 336 | $filename = $class . '.php'; 337 | 338 | // Internal files are all lowercase and use dashes in filenames. 339 | if ( false === strpos( $filename, '\\' ) ) { 340 | $filename = strtolower( str_replace( '_', '-', $filename ) ); 341 | } else { 342 | $filename = str_replace( '\\', DIRECTORY_SEPARATOR, $filename ); 343 | } 344 | 345 | $filepath = __DIR__ . '/includes/' . $filename; 346 | 347 | if ( file_exists( $filepath ) ) { 348 | require_once $filepath; 349 | } 350 | } 351 | 352 | /** 353 | * Instantiate the plugin and hook into WordPress. 354 | * 355 | * @return void 356 | */ 357 | public static function bootstrap() { 358 | /** 359 | * This is a documented valid call for spl_autoload_register. 360 | * 361 | * @link https://www.php.net/manual/en/function.spl-autoload-register.php#71155 362 | */ 363 | spl_autoload_register( array( 'OpenID_Connect_Generic', 'autoload' ) ); 364 | 365 | $settings = new OpenID_Connect_Generic_Option_Settings( 366 | // Default settings values. 367 | array( 368 | // OAuth client settings. 369 | 'login_type' => defined( 'OIDC_LOGIN_TYPE' ) ? OIDC_LOGIN_TYPE : 'button', 370 | 'client_id' => defined( 'OIDC_CLIENT_ID' ) ? OIDC_CLIENT_ID : '', 371 | 'client_secret' => defined( 'OIDC_CLIENT_SECRET' ) ? OIDC_CLIENT_SECRET : '', 372 | 'scope' => defined( 'OIDC_CLIENT_SCOPE' ) ? OIDC_CLIENT_SCOPE : '', 373 | 'endpoint_login' => defined( 'OIDC_ENDPOINT_LOGIN_URL' ) ? OIDC_ENDPOINT_LOGIN_URL : '', 374 | 'endpoint_userinfo' => defined( 'OIDC_ENDPOINT_USERINFO_URL' ) ? OIDC_ENDPOINT_USERINFO_URL : '', 375 | 'endpoint_token' => defined( 'OIDC_ENDPOINT_TOKEN_URL' ) ? OIDC_ENDPOINT_TOKEN_URL : '', 376 | 'endpoint_end_session' => defined( 'OIDC_ENDPOINT_LOGOUT_URL' ) ? OIDC_ENDPOINT_LOGOUT_URL : '', 377 | 'acr_values' => defined( 'OIDC_ACR_VALUES' ) ? OIDC_ACR_VALUES : '', 378 | 379 | // Non-standard settings. 380 | 'no_sslverify' => 0, 381 | 'http_request_timeout' => 5, 382 | 'identity_key' => 'preferred_username', 383 | 'nickname_key' => 'preferred_username', 384 | 'email_format' => '{email}', 385 | 'displayname_format' => '', 386 | 'identify_with_username' => false, 387 | 'state_time_limit' => 180, 388 | 389 | // Plugin settings. 390 | 'enforce_privacy' => defined( 'OIDC_ENFORCE_PRIVACY' ) ? intval( OIDC_ENFORCE_PRIVACY ) : 0, 391 | 'alternate_redirect_uri' => 0, 392 | 'token_refresh_enable' => 1, 393 | 'link_existing_users' => defined( 'OIDC_LINK_EXISTING_USERS' ) ? intval( OIDC_LINK_EXISTING_USERS ) : 0, 394 | 'create_if_does_not_exist' => defined( 'OIDC_CREATE_IF_DOES_NOT_EXIST' ) ? intval( OIDC_CREATE_IF_DOES_NOT_EXIST ) : 1, 395 | 'redirect_user_back' => defined( 'OIDC_REDIRECT_USER_BACK' ) ? intval( OIDC_REDIRECT_USER_BACK ) : 0, 396 | 'redirect_on_logout' => defined( 'OIDC_REDIRECT_ON_LOGOUT' ) ? intval( OIDC_REDIRECT_ON_LOGOUT ) : 1, 397 | 'enable_logging' => defined( 'OIDC_ENABLE_LOGGING' ) ? intval( OIDC_ENABLE_LOGGING ) : 0, 398 | 'log_limit' => defined( 'OIDC_LOG_LIMIT' ) ? intval( OIDC_LOG_LIMIT ) : 1000, 399 | ) 400 | ); 401 | 402 | $logger = new OpenID_Connect_Generic_Option_Logger( 'error', $settings->enable_logging, $settings->log_limit ); 403 | 404 | $plugin = new self( $settings, $logger ); 405 | 406 | add_action( 'init', array( $plugin, 'init' ) ); 407 | 408 | // Privacy hooks. 409 | add_action( 'template_redirect', array( $plugin, 'enforce_privacy_redirect' ), 0 ); 410 | add_filter( 'the_content_feed', array( $plugin, 'enforce_privacy_feeds' ), 999 ); 411 | add_filter( 'the_excerpt_rss', array( $plugin, 'enforce_privacy_feeds' ), 999 ); 412 | add_filter( 'comment_text_rss', array( $plugin, 'enforce_privacy_feeds' ), 999 ); 413 | } 414 | 415 | /** 416 | * Create (if needed) and return a singleton of self. 417 | * 418 | * @return OpenID_Connect_Generic 419 | */ 420 | public static function instance() { 421 | if ( null === self::$_instance ) { 422 | self::bootstrap(); 423 | } 424 | return self::$_instance; 425 | } 426 | } 427 | 428 | OpenID_Connect_Generic::instance(); 429 | 430 | register_activation_hook( __FILE__, array( 'OpenID_Connect_Generic', 'activation' ) ); 431 | register_deactivation_hook( __FILE__, array( 'OpenID_Connect_Generic', 'deactivation' ) ); 432 | 433 | // Provide publicly accessible plugin helper functions. 434 | require_once 'includes/functions.php'; 435 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === OpenID Connect Generic Client === 2 | Contributors: daggerhart, tnolte 3 | Donate link: http://www.daggerhart.com/ 4 | Tags: security, login, oauth2, openidconnect, apps, authentication, autologin, sso 5 | Requires at least: 5.0 6 | Tested up to: 6.4.3 7 | Stable tag: 3.10.0 8 | Requires PHP: 7.4 9 | License: GPLv2 or later 10 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 11 | 12 | A simple client that provides SSO or opt-in authentication against a generic OAuth2 Server implementation. 13 | 14 | == Description == 15 | 16 | This plugin allows to authenticate users against OpenID Connect OAuth2 API with Authorization Code Flow. 17 | Once installed, it can be configured to automatically authenticate users (SSO), or provide a "Login with OpenID Connect" 18 | button on the login form. After consent has been obtained, an existing user is automatically logged into WordPress, while 19 | new users are created in WordPress database. 20 | 21 | Much of the documentation can be found on the Settings > OpenID Connect Generic dashboard page. 22 | 23 | Please submit issues to the Github repo: https://github.com/daggerhart/openid-connect-generic 24 | 25 | == Installation == 26 | 27 | 1. Upload to the `/wp-content/plugins/` directory 28 | 1. Activate the plugin 29 | 1. Visit Settings > OpenID Connect and configure to meet your needs 30 | 31 | == Frequently Asked Questions == 32 | 33 | = What is the client's Redirect URI? = 34 | 35 | Most OAuth2 servers will require whitelisting a set of redirect URIs for security purposes. The Redirect URI provided 36 | by this client is like so: https://example.com/wp-admin/admin-ajax.php?action=openid-connect-authorize 37 | 38 | Replace `example.com` with your domain name and path to WordPress. 39 | 40 | = Can I change the client's Redirect URI? = 41 | 42 | Some OAuth2 servers do not allow for a client redirect URI to contain a query string. The default URI provided by 43 | this module leverages WordPress's `admin-ajax.php` endpoint as an easy way to provide a route that does not include 44 | HTML, but this will naturally involve a query string. Fortunately, this plugin provides a setting that will make use of 45 | an alternate redirect URI that does not include a query string. 46 | 47 | On the settings page for this plugin (Dashboard > Settings > OpenID Connect Generic) there is a checkbox for 48 | **Alternate Redirect URI**. When checked, the plugin will use the Redirect URI 49 | `https://example.com/openid-connect-authorize`. 50 | 51 | 52 | == Changelog == 53 | 54 | = 3.10.0 = 55 | 56 | * Chore: @timnolte - Dependency updates. 57 | * Fix: @drzraf - Prevents running the auth url filter twice. 58 | * Fix: @timnolte - Updates the log cleanup handling to properly retain the configured number of log entries. 59 | * Fix: @timnolte - Updates the log display output to reflect the log retention policy. 60 | * Chore: @timnolte - Adds Unit Testing & New Local Development Environment. 61 | * Feature: @timnolte - Updates logging to allow for tracking processing time. 62 | * Feature: @menno-ll - Adds a remember me feature via a new filter. 63 | * Improvement: @menno-ll - Updates WP Cookie Expiration to Same as Session Length. 64 | 65 | = 3.9.1 = 66 | 67 | * Improvement: @timnolte - Refactors Composer setup and GitHub Actions. 68 | * Improvement: @timnolte - Bumps WordPress tested version compatibility. 69 | 70 | = 3.9.0 = 71 | 72 | * Feature: @matchaxnb - Added support for additional configuration constants. 73 | * Feature: @schanzen - Added support for agregated claims. 74 | * Fix: @rkcreation - Fixed access token not updating user metadata after login. 75 | * Fix: @danc1248 - Fixed user creation issue on Multisite Networks. 76 | * Feature: @RobjS - Added plugin singleton to support for more developer customization. 77 | * Feature: @jkouris - Added action hook to allow custom handling of session expiration. 78 | * Fix: @tommcc - Fixed admin CSS loading only on the plugin settings screen. 79 | * Feature: @rkcreation - Added method to refresh the user claim. 80 | * Feature: @Glowsome - Added acr_values support & verification checks that it when defined in options is honored. 81 | * Fix: @timnolte - Fixed regression which caused improper fallback on missing claims. 82 | * Fix: @slykar - Fixed missing query string handling in redirect URL. 83 | * Fix: @timnolte - Fixed issue with some user linking and user creation handling. 84 | * Improvement: @timnolte - Fixed plugin settings typos and screen formatting. 85 | * Security: @timnolte - Updated build tooling security vulnerabilities. 86 | * Improvement: @timnolte - Changed build tooling scripts. 87 | 88 | = 3.8.5 = 89 | 90 | * Fix: @timnolte - Fixed missing URL request validation before use & ensure proper current page URL is setup for Redirect Back. 91 | * Fix: @timnolte - Fixed Redirect URL Logic to Handle Sub-directory Installs. 92 | * Fix: @timnolte - Fixed issue with redirecting user back when the openid_connect_generic_auth_url shortcode is used. 93 | 94 | = 3.8.4 = 95 | 96 | * Fix: @timnolte - Fixed invalid State object access for redirection handling. 97 | * Improvement: @timnolte - Fixed local wp-env Docker development environment. 98 | * Improvement: @timnolte - Fixed Composer scripts for linting and static analysis. 99 | 100 | = 3.8.3 = 101 | 102 | * Fix: @timnolte - Fixed problems with proper redirect handling. 103 | * Improvement: @timnolte - Changes redirect handling to use State instead of cookies. 104 | * Improvement: @timnolte - Refactored additional code to meet coding standards. 105 | 106 | = 3.8.2 = 107 | 108 | * Fix: @timnolte - Fixed reported XSS vulnerability on WordPress login screen. 109 | 110 | = 3.8.1 = 111 | 112 | * Fix: @timnolte - Prevent SSO redirect on password protected posts. 113 | * Fix: @timnolte - CI/CD build issues. 114 | * Fix: @timnolte - Invalid redirect handling on logout for Auto Login setting. 115 | 116 | = 3.8.0 = 117 | 118 | * Feature: @timnolte - Ability to use 6 new constants for setting client configuration instead of storing in the DB. 119 | * Improvement: @timnolte - Plugin development & contribution updates. 120 | * Improvement: @timnolte - Refactored to meet WordPress coding standards. 121 | * Improvement: @timnolte - Refactored to provide localization. 122 | 123 | -------- 124 | 125 | [See the previous changelogs here](https://github.com/oidc-wp/openid-connect-generic/blob/main/CHANGELOG.md#changelog) 126 | -------------------------------------------------------------------------------- /wp-cli.yml: -------------------------------------------------------------------------------- 1 | path: /app/wp 2 | --------------------------------------------------------------------------------