435 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			435 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * OpenID Connect Generic Client
 | |
|  *
 | |
|  * This plugin provides the ability to authenticate users with Identity
 | |
|  * Providers using the OpenID Connect OAuth2 API with Authorization Code Flow.
 | |
|  *
 | |
|  * @package   OpenID_Connect_Generic
 | |
|  * @category  General
 | |
|  * @author    Jonathan Daggerhart <jonathan@daggerhart.com>
 | |
|  * @copyright 2015-2023 daggerhart
 | |
|  * @license   http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
 | |
|  * @link      https://github.com/daggerhart
 | |
|  *
 | |
|  * @wordpress-plugin
 | |
|  * Plugin Name:       OpenID Connect Generic
 | |
|  * Plugin URI:        https://github.com/daggerhart/openid-connect-generic
 | |
|  * Description:       Connect to an OpenID Connect identity provider using Authorization Code Flow.
 | |
|  * Version:           3.10.0
 | |
|  * Requires at least: 5.0
 | |
|  * Requires PHP:      7.4
 | |
|  * Author:            daggerhart
 | |
|  * Author URI:        http://www.daggerhart.com
 | |
|  * Text Domain:       daggerhart-openid-connect-generic
 | |
|  * Domain Path:       /languages
 | |
|  * License:           GPL-2.0+
 | |
|  * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
 | |
|  * GitHub Plugin URI: https://github.com/daggerhart/openid-connect-generic
 | |
|  */
 | |
| 
 | |
| /*
 | |
| Notes
 | |
|   Spec Doc - http://openid.net/specs/openid-connect-basic-1_0-32.html
 | |
| 
 | |
|   Filters
 | |
|   - openid-connect-generic-alter-request       - 3 args: request array, plugin settings, specific request op
 | |
|   - openid-connect-generic-settings-fields     - modify the fields provided on the settings page
 | |
|   - openid-connect-generic-login-button-text   - modify the login button text
 | |
|   - openid-connect-generic-cookie-redirect-url - modify the redirect url stored as a cookie
 | |
|   - openid-connect-generic-user-login-test     - (bool) should the user be logged in based on their claim
 | |
|   - openid-connect-generic-user-creation-test  - (bool) should the user be created based on their claim
 | |
|   - openid-connect-generic-auth-url            - modify the authentication url
 | |
|   - openid-connect-generic-alter-user-claim    - modify the user_claim before a new user is created
 | |
|   - openid-connect-generic-alter-user-data     - modify user data before a new user is created
 | |
|   - openid-connect-modify-token-response-before-validation - modify the token response before validation
 | |
|   - openid-connect-modify-id-token-claim-before-validation - modify the token claim before validation
 | |
| 
 | |
|   Actions
 | |
|   - openid-connect-generic-user-create                     - 2 args: fires when a new user is created by this plugin
 | |
|   - openid-connect-generic-user-update                     - 1 arg: user ID, fires when user is updated by this plugin
 | |
|   - openid-connect-generic-update-user-using-current-claim - 2 args: fires every time an existing user logs in and the claims are updated.
 | |
|   - openid-connect-generic-redirect-user-back              - 2 args: $redirect_url, $user. Allows interruption of redirect during login.
 | |
|   - openid-connect-generic-user-logged-in                  - 1 arg: $user, fires when user is logged in.
 | |
|   - openid-connect-generic-cron-daily                      - daily cron action
 | |
|   - openid-connect-generic-state-not-found                 - the given state does not exist in the database, regardless of its expiration.
 | |
|   - openid-connect-generic-state-expired                   - the given state exists, but expired before this login attempt.
 | |
| 
 | |
|   Callable actions
 | |
| 
 | |
|   User Meta
 | |
|   - openid-connect-generic-subject-identity    - the identity of the user provided by the idp
 | |
|   - openid-connect-generic-last-id-token-claim - the user's most recent id_token claim, decoded
 | |
|   - openid-connect-generic-last-user-claim     - the user's most recent user_claim
 | |
|   - openid-connect-generic-last-token-response - the user's most recent token response
 | |
| 
 | |
|   Options
 | |
|   - openid_connect_generic_settings     - plugin settings
 | |
|   - openid-connect-generic-valid-states - locally stored generated states
 | |
| */
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * OpenID_Connect_Generic class.
 | |
|  *
 | |
|  * Defines plugin initialization functionality.
 | |
|  *
 | |
|  * @package OpenID_Connect_Generic
 | |
|  * @category  General
 | |
|  */
 | |
| class OpenID_Connect_Generic {
 | |
| 
 | |
| 	/**
 | |
| 	 * Singleton instance of self
 | |
| 	 *
 | |
| 	 * @var OpenID_Connect_Generic
 | |
| 	 */
 | |
| 	protected static $_instance = null;
 | |
| 
 | |
| 	/**
 | |
| 	 * Plugin version.
 | |
| 	 *
 | |
| 	 * @var string
 | |
| 	 */
 | |
| 	const VERSION = '3.10.0';
 | |
| 
 | |
| 	/**
 | |
| 	 * Plugin settings.
 | |
| 	 *
 | |
| 	 * @var OpenID_Connect_Generic_Option_Settings
 | |
| 	 */
 | |
| 	private $settings;
 | |
| 
 | |
| 	/**
 | |
| 	 * Plugin logs.
 | |
| 	 *
 | |
| 	 * @var OpenID_Connect_Generic_Option_Logger
 | |
| 	 */
 | |
| 	private $logger;
 | |
| 
 | |
| 	/**
 | |
| 	 * Openid Connect Generic client
 | |
| 	 *
 | |
| 	 * @var OpenID_Connect_Generic_Client
 | |
| 	 */
 | |
| 	private $client;
 | |
| 
 | |
| 	/**
 | |
| 	 * Client wrapper.
 | |
| 	 *
 | |
| 	 * @var OpenID_Connect_Generic_Client_Wrapper
 | |
| 	 */
 | |
| 	public $client_wrapper;
 | |
| 
 | |
| 	/**
 | |
| 	 * Setup the plugin
 | |
| 	 *
 | |
| 	 * @param OpenID_Connect_Generic_Option_Settings $settings The settings object.
 | |
| 	 * @param OpenID_Connect_Generic_Option_Logger   $logger   The loggin object.
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public function __construct( OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ) {
 | |
| 		$this->settings = $settings;
 | |
| 		$this->logger = $logger;
 | |
| 		self::$_instance = $this;
 | |
| 	}
 | |
| 
 | |
| 	// @codeCoverageIgnoreStart
 | |
| 
 | |
| 	/**
 | |
| 	 * WordPress Hook 'init'.
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public function init() {
 | |
| 
 | |
| 		$this->client = new OpenID_Connect_Generic_Client(
 | |
| 			$this->settings->client_id,
 | |
| 			$this->settings->client_secret,
 | |
| 			$this->settings->scope,
 | |
| 			$this->settings->endpoint_login,
 | |
| 			$this->settings->endpoint_userinfo,
 | |
| 			$this->settings->endpoint_token,
 | |
| 			$this->get_redirect_uri( $this->settings ),
 | |
| 			$this->settings->acr_values,
 | |
| 			$this->get_state_time_limit( $this->settings ),
 | |
| 			$this->logger
 | |
| 		);
 | |
| 
 | |
| 		$this->client_wrapper = OpenID_Connect_Generic_Client_Wrapper::register( $this->client, $this->settings, $this->logger );
 | |
| 		if ( defined( 'WP_CLI' ) && WP_CLI ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		OpenID_Connect_Generic_Login_Form::register( $this->settings, $this->client_wrapper );
 | |
| 
 | |
| 		// Add a shortcode to get the auth URL.
 | |
| 		add_shortcode( 'openid_connect_generic_auth_url', array( $this->client_wrapper, 'get_authentication_url' ) );
 | |
| 
 | |
| 		// Add actions to our scheduled cron jobs.
 | |
| 		add_action( 'openid-connect-generic-cron-daily', array( $this, 'cron_states_garbage_collection' ) );
 | |
| 
 | |
| 		$this->upgrade();
 | |
| 
 | |
| 		if ( is_admin() ) {
 | |
| 			OpenID_Connect_Generic_Settings_Page::register( $this->settings, $this->logger );
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the default redirect URI.
 | |
| 	 *
 | |
| 	 * @param OpenID_Connect_Generic_Option_Settings $settings The settings object.
 | |
| 	 *
 | |
| 	 * @return string
 | |
| 	 */
 | |
| 	public function get_redirect_uri( OpenID_Connect_Generic_Option_Settings $settings ) {
 | |
| 		$redirect_uri = admin_url( 'admin-ajax.php?action=openid-connect-authorize' );
 | |
| 
 | |
| 		if ( $settings->alternate_redirect_uri ) {
 | |
| 			$redirect_uri = site_url( '/openid-connect-authorize' );
 | |
| 		}
 | |
| 
 | |
| 		return $redirect_uri;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the default state time limit.
 | |
| 	 *
 | |
| 	 * @param OpenID_Connect_Generic_Option_Settings $settings The settings object.
 | |
| 	 *
 | |
| 	 * @return int
 | |
| 	 */
 | |
| 	public function get_state_time_limit( OpenID_Connect_Generic_Option_Settings $settings ) {
 | |
| 		$state_time_limit = 180;
 | |
| 		// State time limit cannot be zero.
 | |
| 		if ( $settings->state_time_limit ) {
 | |
| 			$state_time_limit = intval( $settings->state_time_limit );
 | |
| 		}
 | |
| 
 | |
| 		return $state_time_limit;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Check if privacy enforcement is enabled, and redirect users that aren't
 | |
| 	 * logged in.
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public function enforce_privacy_redirect() {
 | |
| 		if ( $this->settings->enforce_privacy && ! is_user_logged_in() ) {
 | |
| 			// The client endpoint relies on the wp-admin ajax endpoint.
 | |
| 			if (
 | |
| 				! defined( 'DOING_AJAX' ) ||
 | |
| 				! boolval( constant( 'DOING_AJAX' ) ) ||
 | |
| 				! isset( $_GET['action'] ) ||
 | |
| 				'openid-connect-authorize' != $_GET['action'] ) {
 | |
| 				auth_redirect();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Enforce privacy settings for rss feeds.
 | |
| 	 *
 | |
| 	 * @param string $content The content.
 | |
| 	 *
 | |
| 	 * @return mixed
 | |
| 	 */
 | |
| 	public function enforce_privacy_feeds( $content ) {
 | |
| 		if ( $this->settings->enforce_privacy && ! is_user_logged_in() ) {
 | |
| 			$content = __( 'Private site', 'daggerhart-openid-connect-generic' );
 | |
| 		}
 | |
| 		return $content;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Handle plugin upgrades
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public function upgrade() {
 | |
| 		$last_version = get_option( 'openid-connect-generic-plugin-version', 0 );
 | |
| 		$settings = $this->settings;
 | |
| 
 | |
| 		if ( version_compare( self::VERSION, $last_version, '>' ) ) {
 | |
| 			// An upgrade is required.
 | |
| 			self::setup_cron_jobs();
 | |
| 
 | |
| 			// @todo move this to another file for upgrade scripts
 | |
| 			if ( isset( $settings->ep_login ) ) {
 | |
| 				$settings->endpoint_login = $settings->ep_login;
 | |
| 				$settings->endpoint_token = $settings->ep_token;
 | |
| 				$settings->endpoint_userinfo = $settings->ep_userinfo;
 | |
| 
 | |
| 				unset( $settings->ep_login, $settings->ep_token, $settings->ep_userinfo );
 | |
| 				$settings->save();
 | |
| 			}
 | |
| 
 | |
| 			// Update the stored version number.
 | |
| 			update_option( 'openid-connect-generic-plugin-version', self::VERSION );
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Expire state transients by attempting to access them and allowing the
 | |
| 	 * transient's own mechanisms to delete any that have expired.
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public function cron_states_garbage_collection() {
 | |
| 		global $wpdb;
 | |
| 		$states = $wpdb->get_col( "SELECT `option_name` FROM {$wpdb->options} WHERE `option_name` LIKE '_transient_openid-connect-generic-state--%'" );
 | |
| 
 | |
| 		if ( ! empty( $states ) ) {
 | |
| 			foreach ( $states as $state ) {
 | |
| 				$transient = str_replace( '_transient_', '', $state );
 | |
| 				get_transient( $transient );
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Ensure cron jobs are added to the schedule.
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public static function setup_cron_jobs() {
 | |
| 		if ( ! wp_next_scheduled( 'openid-connect-generic-cron-daily' ) ) {
 | |
| 			wp_schedule_event( time(), 'daily', 'openid-connect-generic-cron-daily' );
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Activation hook.
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public static function activation() {
 | |
| 		self::setup_cron_jobs();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Deactivation hook.
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public static function deactivation() {
 | |
| 		wp_clear_scheduled_hook( 'openid-connect-generic-cron-daily' );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Simple autoloader.
 | |
| 	 *
 | |
| 	 * @param string $class The class name.
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public static function autoload( $class ) {
 | |
| 		$prefix = 'OpenID_Connect_Generic_';
 | |
| 
 | |
| 		if ( stripos( $class, $prefix ) !== 0 ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		$filename = $class . '.php';
 | |
| 
 | |
| 		// Internal files are all lowercase and use dashes in filenames.
 | |
| 		if ( false === strpos( $filename, '\\' ) ) {
 | |
| 			$filename = strtolower( str_replace( '_', '-', $filename ) );
 | |
| 		} else {
 | |
| 			$filename  = str_replace( '\\', DIRECTORY_SEPARATOR, $filename );
 | |
| 		}
 | |
| 
 | |
| 		$filepath = __DIR__ . '/includes/' . $filename;
 | |
| 
 | |
| 		if ( file_exists( $filepath ) ) {
 | |
| 			require_once $filepath;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Instantiate the plugin and hook into WordPress.
 | |
| 	 *
 | |
| 	 * @return void
 | |
| 	 */
 | |
| 	public static function bootstrap() {
 | |
| 		/**
 | |
| 		 * This is a documented valid call for spl_autoload_register.
 | |
| 		 *
 | |
| 		 * @link https://www.php.net/manual/en/function.spl-autoload-register.php#71155
 | |
| 		 */
 | |
| 		spl_autoload_register( array( 'OpenID_Connect_Generic', 'autoload' ) );
 | |
| 
 | |
| 		$settings = new OpenID_Connect_Generic_Option_Settings(
 | |
| 			// Default settings values.
 | |
| 			array(
 | |
| 				// OAuth client settings.
 | |
| 				'login_type'           => defined( 'OIDC_LOGIN_TYPE' ) ? OIDC_LOGIN_TYPE : 'button',
 | |
| 				'client_id'            => defined( 'OIDC_CLIENT_ID' ) ? OIDC_CLIENT_ID : '',
 | |
| 				'client_secret'        => defined( 'OIDC_CLIENT_SECRET' ) ? OIDC_CLIENT_SECRET : '',
 | |
| 				'scope'                => defined( 'OIDC_CLIENT_SCOPE' ) ? OIDC_CLIENT_SCOPE : '',
 | |
| 				'endpoint_login'       => defined( 'OIDC_ENDPOINT_LOGIN_URL' ) ? OIDC_ENDPOINT_LOGIN_URL : '',
 | |
| 				'endpoint_userinfo'    => defined( 'OIDC_ENDPOINT_USERINFO_URL' ) ? OIDC_ENDPOINT_USERINFO_URL : '',
 | |
| 				'endpoint_token'       => defined( 'OIDC_ENDPOINT_TOKEN_URL' ) ? OIDC_ENDPOINT_TOKEN_URL : '',
 | |
| 				'endpoint_end_session' => defined( 'OIDC_ENDPOINT_LOGOUT_URL' ) ? OIDC_ENDPOINT_LOGOUT_URL : '',
 | |
| 				'acr_values'           => defined( 'OIDC_ACR_VALUES' ) ? OIDC_ACR_VALUES : '',
 | |
| 
 | |
| 				// Non-standard settings.
 | |
| 				'no_sslverify'           => 0,
 | |
| 				'http_request_timeout'   => 5,
 | |
| 				'identity_key'           => 'preferred_username',
 | |
| 				'nickname_key'           => 'preferred_username',
 | |
| 				'email_format'           => '{email}',
 | |
| 				'displayname_format'     => '',
 | |
| 				'identify_with_username' => false,
 | |
| 				'state_time_limit'       => 180,
 | |
| 
 | |
| 				// Plugin settings.
 | |
| 				'enforce_privacy'          => defined( 'OIDC_ENFORCE_PRIVACY' ) ? intval( OIDC_ENFORCE_PRIVACY ) : 0,
 | |
| 				'alternate_redirect_uri'   => 0,
 | |
| 				'token_refresh_enable'     => 1,
 | |
| 				'link_existing_users'      => defined( 'OIDC_LINK_EXISTING_USERS' ) ? intval( OIDC_LINK_EXISTING_USERS ) : 0,
 | |
| 				'create_if_does_not_exist' => defined( 'OIDC_CREATE_IF_DOES_NOT_EXIST' ) ? intval( OIDC_CREATE_IF_DOES_NOT_EXIST ) : 1,
 | |
| 				'redirect_user_back'       => defined( 'OIDC_REDIRECT_USER_BACK' ) ? intval( OIDC_REDIRECT_USER_BACK ) : 0,
 | |
| 				'redirect_on_logout'       => defined( 'OIDC_REDIRECT_ON_LOGOUT' ) ? intval( OIDC_REDIRECT_ON_LOGOUT ) : 1,
 | |
| 				'enable_logging'           => defined( 'OIDC_ENABLE_LOGGING' ) ? intval( OIDC_ENABLE_LOGGING ) : 0,
 | |
| 				'log_limit'                => defined( 'OIDC_LOG_LIMIT' ) ? intval( OIDC_LOG_LIMIT ) : 1000,
 | |
| 			)
 | |
| 		);
 | |
| 
 | |
| 		$logger = new OpenID_Connect_Generic_Option_Logger( 'error', $settings->enable_logging, $settings->log_limit );
 | |
| 
 | |
| 		$plugin = new self( $settings, $logger );
 | |
| 
 | |
| 		add_action( 'init', array( $plugin, 'init' ) );
 | |
| 
 | |
| 		// Privacy hooks.
 | |
| 		add_action( 'template_redirect', array( $plugin, 'enforce_privacy_redirect' ), 0 );
 | |
| 		add_filter( 'the_content_feed', array( $plugin, 'enforce_privacy_feeds' ), 999 );
 | |
| 		add_filter( 'the_excerpt_rss', array( $plugin, 'enforce_privacy_feeds' ), 999 );
 | |
| 		add_filter( 'comment_text_rss', array( $plugin, 'enforce_privacy_feeds' ), 999 );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Create (if needed) and return a singleton of self.
 | |
| 	 *
 | |
| 	 * @return OpenID_Connect_Generic
 | |
| 	 */
 | |
| 	public static function instance() {
 | |
| 		if ( null === self::$_instance ) {
 | |
| 			self::bootstrap();
 | |
| 		}
 | |
| 		return self::$_instance;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| OpenID_Connect_Generic::instance();
 | |
| 
 | |
| register_activation_hook( __FILE__, array( 'OpenID_Connect_Generic', 'activation' ) );
 | |
| register_deactivation_hook( __FILE__, array( 'OpenID_Connect_Generic', 'deactivation' ) );
 | |
| 
 | |
| // Provide publicly accessible plugin helper functions.
 | |
| require_once 'includes/functions.php';
 |