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';
|