initial commit of working plugin

isekai
daggerhart 10 years ago
commit 7ce9613650

@ -0,0 +1,303 @@
<?php
class OpenID_Connect_Generic_Settings {
// local copy of the settings provided by the base plugin
private $settings = array();
// The controlled list of settings & associated
// defined during construction for i18n reasons
private $settings_fields = array();
// options page slug
private $options_page_name = 'openid-connect-generic-settings';
// options page settings group name
private $settings_field_group = '';
/**
* @param $settings
*/
function __construct( $settings ){
$this->settings = $settings;
$this->settings_field_group = OPENID_CONNECT_GENERIC_SETTINGS_NAME . '-group';
// add our options page the the admin menu
add_action( 'admin_menu', array( $this, 'admin_menu' ) );
// register our settings
add_action( 'admin_init', array( $this, 'admin_init' ) );
/*
* Simple settings fields simply have:
*
* - title
* - description
* - type ( checkbox | text )
* - section ( settings/option page section )
* - example (optional)
*/
$this->settings_fields = array(
'login_type' => array(
'title' => __('Login Type'),
'description' => __('Select how the client (login form) should provide login options.'),
'type' => 'select',
'options' => array(
'button' => __('OpenID Connect button on login form'),
'auto' => __('Auto Login - SSO'),
),
'section' => 'client_settings',
),
'ep_login' => array(
'title' => __('Login Endpoint URL'),
'description' => __('Identify provider authorization endpoint.'),
'example' => 'https://example.com/oauth2/authorize',
'type' => 'text',
'section' => 'client_settings',
),
'ep_token' => array(
'title' => __('Token Validation Endpoint URL'),
'description' => __('Identify provider token endpoint.'),
'example' => 'https://example.com/oauth2/token',
'type' => 'text',
'section' => 'client_settings',
),
'ep_userinfo' => array(
'title' => __('Userinfo Endpoint URL'),
'description' => __('Identify provider User information endpoint.'),
'example' => 'https://example.com/oauth2/UserInfo',
'type' => 'text',
'section' => 'client_settings',
),
'no_sslverify' => array(
'title' => __('Disable SSL Verify'),
'description' => __('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.'),
'type' => 'checkbox',
'section' => 'client_settings',
),
'client_id' => array(
'title' => __('Client ID'),
'description' => __('The ID this client will be recognized as when connecting the to Identity provider server.'),
'example' => 'my-wordpress-client-id',
'type' => 'text',
'section' => 'client_settings',
),
'client_secret' => array(
'title' => __('Client Secret Key'),
'description' => __('Arbitrary secret key the server expects from this client. Can be anything, but should be very unique.'),
'type' => 'text',
'section' => 'client_settings',
),
'scope' => array(
'title' => __('OpenID Scope'),
'description' => __('Space separated list of scopes this client should access.'),
'example' => 'email profile openid',
'type' => 'text',
'section' => 'client_settings',
),
'identity_key' => array(
'title' => __('Identity Key'),
'description' => __('Where in the response array to find the identification data. When in doubt, use "sub".'),
'example' => 'sub',
'type' => 'text',
'section' => 'client_settings',
),
'allowed_regex' => array(
'title' => __('Authorization Regex'),
'description' => __('Provide a regular expression that enforces your expectations concerning the identity value returned from the IDP.'),
'type' => 'text',
'section' => 'authorization_settings',
),
'enforce_privacy' => array(
'title' => __('Enforce Privacy'),
'description' => __('Require users be logged in to see the site.'),
'type' => 'checkbox',
'section' => 'authorization_settings',
),
);
// allow alterations of the fields
$this->settings_fields = apply_filters( 'openid-connect-generic-settings-fields', $this->settings_fields );
}
/**
* Implements hook admin_menu to add our options/settings page to the
* dashboard menu
*/
public function admin_menu() {
add_options_page(
__('OpenID Connect - Generic Client'),
__('OpenID Connect Client'),
'manage_options',
$this->options_page_name,
array( $this, 'settings_page') );
}
/**
* Implements hook admin_init to register our settings
*/
public function admin_init() {
register_setting( $this->settings_field_group, OPENID_CONNECT_GENERIC_SETTINGS_NAME, array( $this, 'sanitize_settings' ) );
add_settings_section( 'client_settings',
__('Client Settings'),
array( $this, 'client_settings_description' ),
$this->options_page_name
);
add_settings_section( 'authorization_settings',
__('Authorization Settings'),
array( $this, 'authorization_settings_description' ),
$this->options_page_name
);
// preprocess fields and add them to the page
foreach ( $this->settings_fields as $key => $field ) {
$field['key'] = $key;
$field['name'] = OPENID_CONNECT_GENERIC_SETTINGS_NAME . '[' . $key . ']';
// make sure each key exists in the settings array
if ( ! isset( $this->settings[ $key ] ) ){
$this->settings[ $key ] = null;
}
// determine appropriate output callback
switch ( $field['type'] ) {
case 'checkbox':
$callback = 'do_checkbox';
break;
case 'select':
$callback = 'do_select';
break;
case 'text':
default:
$callback = 'do_text_field';
break;
}
// add the field
add_settings_field( $key, $field['title'],
array( $this, $callback ),
$this->options_page_name,
$field['section'],
$field
);
}
}
/**
* Sanitization callback for settings/option page
*
* @param $input - submitted settings values
* @return array
*/
public function sanitize_settings( $input ) {
$options = array();
// loop through settings fields to control what we're saving
foreach ( $this->settings_fields as $key => $field ) {
if ( isset( $input[ $key ] ) ){
$options[ $key ] = sanitize_text_field( trim( $input[ $key ] ) );
}
else {
$options[ $key ] = '';
}
}
return $options;
}
/**
* Output the options/settings page
*/
public function settings_page() {
?>
<div class="wrap">
<h2><?php print esc_html( get_admin_page_title() ); ?></h2>
<form method="post" action="options.php">
<?php
settings_fields( $this->settings_field_group );
do_settings_sections( $this->options_page_name );
submit_button();
?>
</form>
</div>
<?php
}
/**
* Output a standard text field
*
* @param $field
*/
public function do_text_field( $field ) {
?>
<input type="text"
id="<?php print esc_attr( $field['key'] ); ?>"
class="large-text"
name="<?php print esc_attr( $field['name'] ); ?>"
value="<?php print esc_attr( $this->settings[ $field['key'] ] ); ?>">
<?php
$this->do_field_description( $field );
}
/**
* Output a checkbox for a boolean setting
* - hidden field is default value so we don't have to check isset() on save
*
* @param $field
*/
public function do_checkbox( $field ) {
?>
<input type="hidden" name="settings[<?php print esc_attr( $field['key'] ); ?>]" value="0">
<input type="checkbox"
id="<?php print esc_attr( $field['key'] ); ?>"
name="<?php print esc_attr( $field['name'] ); ?>"
value="1"
<?php checked( $this->settings[ $field['key'] ] , 1 ); ?>>
<?php
$this->do_field_description( $field );
}
/**
* @param $field
*/
function do_select( $field ) {
$current_value = ( $this->settings[ $field['key'] ] ? $this->settings[ $field['key'] ] : '');
?>
<select name="<?php print esc_attr( $field['name'] ); ?>">
<?php foreach ( $field['options'] as $value => $text ): ?>
<option value="<?php print esc_attr( $value ); ?>" <?php selected( $value, $current_value ); ?>><?php print esc_html( $text ); ?></option>
<?php endforeach; ?>
</select>
<?php
$this->do_field_description( $field );
}
/**
* Simply output the field description, and example if present
*
* @param $field
*/
public function do_field_description( $field ){
?>
<p class="description">
<?php print $field['description']; ?>
<?php if ( isset( $field['example'] ) ) : ?>
<br /><strong><?php _e( 'Example' ); ?>: </strong><code><?php print $field['example']; ?></code>
<?php endif; ?>
</p>
<?php
}
public function client_settings_description() {
_e('Enter your OpenID Connect identity provider settings');
}
public function authorization_settings_description() {
_e('Control the authorization mechanics of the site');
}
}

@ -0,0 +1,554 @@
<?php
/*
Plugin Name: OpenID Connect - Generic Client
Plugin URI:
Description: Connect to an OpenID Connect identity provider with Authorization Code Flow
Version: 2.0
Author: daggerhart
Author URI:
License: GPLv2 Copyright (c) 2015 daggerhart
*/
/*
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
User Meta
- openid-connect-generic-user - (bool) if the user was created by this plugin
- openid-connect-generic-user-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
Options
- openid_connect_generic_settings - plugin settings
- openid-connect-generic-valid-states - locally stored generated states
*/
// - authentication is identifying the user
// - authorization is providing access & permission
// The id_token is used to identify the authenticated user, e.g. for SSO.
// The access_token must be used to prove access rights to protected resources, e.g. for the userinfo endpoint in OpenId Connect.
define( 'OPENID_CONNECT_GENERIC_DIR', dirname( __FILE__ ) );
define( 'OPENID_CONNECT_GENERIC_SETTINGS_NAME', 'openid_connect_generic_settings' );
class OpenID_Connect_Generic {
private $cookie_id_key = 'openid-connect-generic-identity';
// states are only valid for 3 minutes
private $state_time_limit = 180;
// default plugin settings values
private $default_settings = array(
'login_type' => 'button',
'no_sslverify' => 0,
'enforce_privacy' => 0,
'identity_key' => 'sub',
);
// storage for plugin settings
private $settings = array();
// storage for error messages
private $errors = array();
private $redirect_uri;
/**
* Initialize the plugin
*/
function __construct(){
add_action( 'init', array( $this, 'init' ) );
$this->redirect_uri = admin_url( 'admin-ajax.php?action=openid-connect-authorize' );
// translatable errors
$this->errors = array(
1 => __('Cannot get authentication response'),
2 => __('Cannot get token response'),
3 => __('Cannot get user claims'),
4 => __('Cannot get valid token'),
5 => __('Cannot get user key'),
6 => __('Cannot create authorized user'),
7 => __('User not found'),
99 => __('Unknown error')
);
}
/**
* Get plugin settings
* - settings field logic in admin/settings class
*
* @return array
*/
public function get_settings() {
if ( ! empty( $this->settings ) ){
return $this->settings;
}
return wp_parse_args( get_option( OPENID_CONNECT_GENERIC_SETTINGS_NAME, array() ), $this->default_settings );
}
/**
* Implements hook init
* - hook plugin into WP as needed
*/
public function init(){
// check the user's status based on plugin settings
$this->check_user_status();
// remove cookies on logout
add_action( 'wp_logout', array( $this, 'wp_logout' ) );
// verify legitimacy of user token on admin pages
add_action( 'admin_init', array( $this, 'check_user_token' ) );
// alter the login form as dictated by settings
add_filter( 'login_message', array( $this, 'login_message' ), 99 );
// alter the requests according to settings
add_filter( 'openid-connect-generic-alter-request', array( $this, 'alter_request' ), 10, 3 );
// administration yo!
if ( is_admin() ) {
// use the ajax url to handle processing authorization without any html output
// this callback will occur when then IDP returns with an authenticated value
add_action( 'wp_ajax_openid-connect-authorize', array( $this, 'auth_callback' ) );
add_action( 'wp_ajax_nopriv_openid-connect-authorize', array( $this, 'auth_callback' ) );
// initialize the settings page
require_once OPENID_CONNECT_GENERIC_DIR . '/admin/openid-connect-generic-settings.php';
new OpenID_Connect_Generic_Settings( $this->get_settings() );
}
}
/**
* Validate the user's status based on plugin settings
*/
function check_user_status(){
$settings = $this->get_settings();
// check if privacy enforcement is enabled
if ( $settings['enforce_privacy'] &&
! is_user_logged_in() &&
// avoid redirects on cron or ajax
( ! defined( 'DOING_AJAX') || ! DOING_AJAX ) &&
( ! defined( 'DOING_CRON' ) || ! DOING_CRON )
)
{
global $pagenow;
// avoid redirect loop
if ( $pagenow != 'wp-login.php' && ! isset( $_GET['loggedout'] ) ) {
wp_redirect( wp_login_url() );
exit;
}
}
// verify token for any logged in user
if ( is_user_logged_in() ) {
$this->check_user_token();
}
}
/**
* Check the user's cookie
*/
function check_user_token(){
$is_openid_connect_user = get_user_meta( wp_get_current_user()->ID, 'openid-connect-generic-user', true );
if ( is_user_logged_in() && ! empty( $is_openid_connect_user ) && ! isset( $_COOKIE[ $this->cookie_id_key ] ) ) {
wp_logout();
wp_redirect( wp_login_url() );
exit;
}
}
/**
* Control the authentication and subsequent authorization of the user when
* returning from the IDP.
*/
function auth_callback(){
$settings = $this->get_settings();
// look for an existing error of some kind
if ( isset( $_GET['error'] ) ) {
$this->error_redirect( 99 );
}
// make sure we have a legitimate authentication code and valid state
if ( !isset( $_GET['code'] ) || !isset( $_GET['state'] ) || !$this->check_state( $_GET['state'] ) ) {
$this->error_redirect( 1 );
}
// we have an authorization code, make sure it is good by
// attempting to exchange it for an authentication token
$token_result = $this->request_authentication_token( $_GET['code'] );
// ensure the token is not an error generated by wp
if ( is_wp_error( $token_result ) ){
$this->error_redirect( 2 );
}
// extract token response from token
$token_response = json_decode( $token_result['body'], true );
// we need to ensure 3 specific items exist with the token response in order
// to proceed with confidence: id_token, access_token, and token_type == 'Bearer'
if ( ! isset( $token_response['id_token'] ) || ! isset( $token_response['access_token'] ) ||
! isset( $token_response['token_type'] ) || $token_response['token_type'] !== 'Bearer' )
{
$this->error_redirect( 4 );
}
// - end authentication
// - start authorization
// The id_token is used to identify the authenticated user, e.g. for SSO.
// The access_token must be used to prove access rights to protected resources
// e.g. for the userinfo endpoint
// break apart the id_token int eh response for decoding
$tmp = explode('.', $token_response['id_token'] );
// Extract the id_token's claims from the token
$id_token_claim = json_decode( base64_decode( $tmp[1] ), true );
// make sure we can find our identification data and that it has a value
if ( ! isset( $id_token_claim[ $settings['identity_key'] ] ) || empty( $id_token_claim[ $settings['identity_key'] ] ) ) {
$this->error_redirect( 5 );
}
// if desired, admins can use regex to determine if the identity value is valid
// according to their own standards expectations
if ( isset( $settings['allowed_regex'] ) && !empty( $settings['allowed_regex'] ) &&
preg_match( $settings['allowed_regex'], $id_token_claim[ $settings['identity_key'] ] ) !== 1)
{
$this->error_redirect( 5 );
}
// send a userinfo request to get user claim
$user_claim_result = $this->request_userinfo( $token_response['access_token'] );
// make sure we didn't get an error, and that the response body exists
if ( is_wp_error( $user_claim_result ) || ! isset( $user_claim_result['body'] ) ) {
$this->error_redirect( 3 );
}
$user_claim = json_decode( $user_claim_result['body'], true );
// make sure the id_token sub === user_claim sub, according to spec
if ( $id_token_claim['sub'] !== $user_claim['sub'] ) {
$this->error_redirect( 4 );
}
$user_identity = $id_token_claim[ $settings['identity_key'] ];
$oauth_expiry = $token_response['expires_in'] + current_time( 'timestamp', true );
setcookie( $this->cookie_id_key, $user_identity, $oauth_expiry, COOKIEPATH, COOKIE_DOMAIN, true );
// - end authorization
// - start user handling
// look for user by their openid-connect-generic-user-identity value
$user_query = new WP_User_Query( array(
'meta_query' => array(
array(
'key' => 'openid-connect-generic-user-identity',
'value' => 746,
)
)
));
// if we found an existing users, grab the first one returned
if ( $user_query->get_total() > 0 ) {
$user = $user_query->get_results()[0];
}
// otherwise, user does not exist and we'll need to create it
else {
// default username & email to the user identity, since that is the only
// thing we can be sure to have
$username = $user_identity;
$email = $user_identity;
// allow claim details to determine username
if ( isset( $user_claim['name'] ) && isset( $user_claim['email'] ) ) {
$username = $user_claim['name'];
$email = $user_claim['email'];
}
// if no name exists, attempt another request for userinfo
else if ( isset( $token_response['access_token'] ) ) {
$user_claim_result = $this->request_userinfo( $token_response['access_token'] );
// make sure we didn't get an error
if ( is_wp_error( $user_claim_result ) ) {
$this->error_redirect( 3 );
}
$user_claim = json_decode( $user_claim_result['body'], true );
if ( isset( $user_claim['name'] ) ) {
$username = $user_claim['name'];
}
if ( isset( $user_claim['email'] ) ) {
$email = $user_claim['email'];
}
}
// create the new user
$uid = wp_create_user( $username, wp_generate_password( 32, true, true ), $email );
// make sure we didn't fail in creating the user
if ( is_wp_error( $uid ) ) {
$this->error_redirect( 6 );
}
$user = get_user_by( 'id', $uid );
// save some meta data about this new user for the future
add_user_meta( $user->ID, 'openid-connect-generic-user', true, true );
add_user_meta( $user->ID, 'openid-connect-generic-user-identity', (string) $user_identity, true );
}
// ensure our found user is a real WP_User
if ( ! is_a( $user, 'WP_User' ) || ! $user->exists() ) {
$this->error_redirect( 7 );
}
// hey, we made it!
// let's remember the tokens for future reference
update_user_meta( $user->ID, 'openid-connect-generic-last-id-token-claim', $id_token_claim );
update_user_meta( $user->ID, 'openid-connect-generic-last-user-claim', $user_claim );
// get a cookie and go home!
wp_set_auth_cookie( $user->ID, false );
wp_redirect( home_url() );
// - end user handling
}
/**
* Using the authorization_code, request an authentication token from the idp
*
* @param $code - authorization_code
* @return array|\WP_Error
*/
function request_authentication_token( $code ){
$settings = $this->get_settings();
$request = array(
'body' => array(
'code' => $code,
'client_id' => $settings['client_id'],
'client_secret' => $settings['client_secret'],
'redirect_uri' => $this->redirect_uri,
'grant_type' => 'authorization_code',
'scope' => $settings['scope'],
)
);
// allow modifications to the request
$request = apply_filters( 'openid-connect-generic-alter-request', $request, $settings, 'get-authentication-token' );
// call the server and ask for a token
$response = wp_remote_post( $settings['ep_token'], $request );
return $response;
}
/**
* Using an access_token, request the userinfo from the idp
*
* @param $access_token
* @return array|\WP_Error
*/
function request_userinfo( $access_token ){
$settings = $this->get_settings();
// allow modifications to the request
$request = apply_filters( 'openid-connect-generic-alter-request', array(), $settings, 'get-userinfo' );
// attempt the request
$response = wp_remote_get( $settings['ep_userinfo'].'?access_token='.$access_token, $request );
return $response;
}
/**
* Modify outgoing requests according to settings
*
* @param $request
* @param $settings
* @param $op
* @return mixed
*/
function alter_request( $request, $settings, $op ){
if ( isset( $settings['no_sslverify'] ) && $settings['no_sslverify'] ) {
$request['sslverify'] = false;
}
return $request;
}
/**
* Create a single use authentication url
*
* @return string
*/
function make_authentication_url() {
$settings = $this->get_settings();
$url = sprintf( '%1$s?response_type=code&scope=%2$s&client_id=%3$s&state=%4$s&redirect_uri=%5$s',
$settings['ep_login'],
urlencode( $settings['scope'] ),
urlencode( $settings['client_id'] ),
$this->new_state(),
urlencode( $this->redirect_uri )
);
return $url;
}
/**
* Generate a new state,
* save it to the states option with a timestamp,
* and return it.
*
* @return string
*/
function new_state(){
$states = get_option( 'openid-connect-generic-valid-states', array() );
// new state w/ timestamp
$new_state = md5( mt_rand() );
$states[ $new_state ] = time();
// save state
update_option( 'openid-connect-generic-valid-states', $states );
return $new_state;
}
/**
* Check the validity of a given state
*
* @param $state
* @return bool
*/
function check_state( $state ){
$states = get_option( 'openid-connect-generic-valid-states', array() );
$valid = false;
// remove any expired states
foreach ( $states as $code => $timestamp ){
if ( ( $timestamp + $this->state_time_limit ) < time() ) {
unset( $states[ $code ] );
}
}
// see if the current state is still within the list of valid states
if ( isset( $states[ $state ] ) ){
// state is valid, remove it
unset( $states[ $state ] );
$valid = true;
}
// save our altered states
update_option( 'openid-connect-generic-valid-states', $states );
return $valid;
}
/**
* Implements filter login_message
*
* @param $message
* @return string
*/
function login_message( $message ){
$settings = $this->get_settings();
// errors and auto login can't happen at the same time
if ( isset( $_GET['login-error'] ) ) {
$message = $this->error_message( $_GET['login-error'] );
}
else if ( $settings['login_type'] == 'auto' ) {
wp_redirect( $this->make_authentication_url() );
exit;
}
// login button is appended to existing messages in case of error
if ( $settings['login_type'] == 'button' ) {
$message.= $this->login_button();
}
return $message;
}
/**
* Handle errors by redirecting the user to the login form
* along with an error code
*
* @param $error_number
*/
function error_redirect( $error_number ){
$url = wp_login_url() . '?login-error=' . $error_number;
wp_redirect( $url );
exit;
}
/**
* Display an error message to the user
*
* @param $error_number
* @return string
*/
function error_message( $error_number ){
// fallback to unknown error
if ( ! isset( $this->errors[ $error_number ] ) ) {
$error_number = 99;
}
ob_start();
?>
<div id="login_error"><?php print $this->errors[ $error_number ]; ?></div>
<?php
return ob_get_clean();
}
/**
* Create a login button (link)
*
* @return string
*/
function login_button() {
$text = apply_filters( 'openid-connect-generic-login-button-text', __('Login with OpenID Connect') );
$href =$this->make_authentication_url();
ob_start();
?>
<div class="openid-connect-login-button" style="margin: 1em 0; text-align: center;">
<a class="button button-large" href="<?php print esc_url( $href ); ?>"><?php print $text; ?></a>
</div>
<?php
return ob_get_clean();
}
/**
* Implements hook wp_logout
*
* Remove cookies
*/
function wp_logout(){
setcookie( $this->cookie_id_key , '1', 0, COOKIEPATH, COOKIE_DOMAIN, true );
}
}
new OpenID_Connect_Generic();
Loading…
Cancel
Save