major refactor to separate responsibility
parent
0ddb501a55
commit
2e3167fa20
@ -0,0 +1,455 @@
|
||||
<?php
|
||||
|
||||
class OpenID_Connect_Generic_Client_Wrapper {
|
||||
|
||||
private $client;
|
||||
|
||||
// settings object
|
||||
private $settings;
|
||||
|
||||
// logger object
|
||||
private $logger;
|
||||
|
||||
// internal tracking cookie key
|
||||
private $cookie_id_key = 'openid-connect-generic-identity';
|
||||
|
||||
// WP_Error if there was a problem, or false if no error
|
||||
private $error = false;
|
||||
|
||||
|
||||
/**
|
||||
* Inject necessary objects and services into the client
|
||||
*
|
||||
* @param \WP_Option_Settings $settings
|
||||
* @param \WP_Option_Logger $logger
|
||||
*/
|
||||
function __construct( OpenID_Connect_Generic_Client $client, WP_Option_Settings $settings, WP_Option_Logger $logger ){
|
||||
$this->client = $client;
|
||||
$this->settings = $settings;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook the client into WP
|
||||
*
|
||||
* @param \OpenID_Connect_Generic_Client $client
|
||||
* @param \WP_Option_Settings $settings
|
||||
* @param \WP_Option_Logger $logger
|
||||
*/
|
||||
static public function register( OpenID_Connect_Generic_Client $client, WP_Option_Settings $settings, WP_Option_Logger $logger ){
|
||||
$client_wrapper = new self( $client, $settings, $logger );
|
||||
|
||||
// remove cookies on logout
|
||||
add_action( 'wp_logout', array( $client_wrapper, 'wp_logout' ) );
|
||||
|
||||
// verify legitimacy of user token on admin pages
|
||||
add_action( 'admin_init', array( $client_wrapper, 'check_user_token' ) );
|
||||
|
||||
// alter the requests according to settings
|
||||
add_filter( 'openid-connect-generic-alter-request', array( $client_wrapper, 'alter_request' ), 10, 3 );
|
||||
|
||||
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( $client_wrapper, 'authentication_request_callback' ) );
|
||||
add_action( 'wp_ajax_nopriv_openid-connect-authorize', array( $client_wrapper, 'authentication_request_callback' ) );
|
||||
}
|
||||
|
||||
$client_wrapper->startup();
|
||||
|
||||
return $client_wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the initial validation that should occur on each page load
|
||||
*/
|
||||
function startup(){
|
||||
$this->handle_privacy();
|
||||
|
||||
// verify token for any logged in user
|
||||
if ( is_user_logged_in() ) {
|
||||
$this->check_user_token();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication url from the client
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function get_authentication_url(){
|
||||
return $this->client->make_authentication_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the privacy settings
|
||||
*/
|
||||
function handle_privacy() {
|
||||
// check if privacy enforcement is enabled
|
||||
if ( $this->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'] ) && ! isset( $_GET['login-error'] ) ) {
|
||||
$this->error_redirect( new WP_Error( 'privacy', __( 'This site requires login.' ), $_GET ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
$this->error_redirect( new WP_Error( 'mismatch-identity', __( 'Mismatch identity' ), $_COOKIE ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle errors by redirecting the user to the login form
|
||||
* along with an error code
|
||||
*
|
||||
* @param $error WP_Error
|
||||
*/
|
||||
function error_redirect( $error ) {
|
||||
$this->logger->log( $error );
|
||||
|
||||
// redirect user back to login page
|
||||
wp_redirect(
|
||||
wp_login_url() .
|
||||
'?login-error=' . $error->get_error_code() .
|
||||
'&message=' . urlencode( $error->get_error_message() )
|
||||
);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current error state
|
||||
*
|
||||
* @return bool | WP_Error
|
||||
*/
|
||||
function get_error(){
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook wp_logout
|
||||
*
|
||||
* Remove cookies
|
||||
*/
|
||||
function wp_logout() {
|
||||
setcookie( $this->cookie_id_key, '1', 0, COOKIEPATH, COOKIE_DOMAIN, TRUE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify outgoing requests according to settings
|
||||
*
|
||||
* @param $request
|
||||
* @param $op
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
function alter_request( $request, $op ) {
|
||||
if ( $this->settings->no_sslverify ) {
|
||||
$request['sslverify'] = FALSE;
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Control the authentication and subsequent authorization of the user when
|
||||
* returning from the IDP.
|
||||
*/
|
||||
function authentication_request_callback() {
|
||||
$settings = $this->settings;
|
||||
$client = $this->client;
|
||||
|
||||
//
|
||||
$authentication_request = $client->validate_authentication_request( $_GET );
|
||||
|
||||
if ( is_wp_error( $authentication_request ) ){
|
||||
$this->error_redirect( $authentication_request );
|
||||
}
|
||||
|
||||
// retrieve the authentication code from the authentication request
|
||||
$code = $client->get_authentication_code( $authentication_request );
|
||||
|
||||
if ( is_wp_error( $code ) ){
|
||||
$this->error_redirect( $code );
|
||||
}
|
||||
|
||||
// attempting to exchange an authorization code for an authentication token
|
||||
$token_result = $client->request_authentication_token( $code );
|
||||
|
||||
if ( is_wp_error( $token_result ) ) {
|
||||
$this->error_redirect( $token_result );
|
||||
}
|
||||
|
||||
// get the decoded response from the authentication request result
|
||||
$token_response = $client->get_token_response( $token_result );
|
||||
|
||||
if ( is_wp_error( $token_response ) ){
|
||||
$this->error_redirect( $token_response );
|
||||
}
|
||||
|
||||
// ensure the that response contains required information
|
||||
$valid = $client->validate_token_response( $token_response );
|
||||
|
||||
if ( is_wp_error( $valid ) ) {
|
||||
$this->error_redirect( $valid );
|
||||
}
|
||||
|
||||
// - 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
|
||||
|
||||
//
|
||||
$id_token_claim = $client->get_id_token_claim( $token_response );
|
||||
|
||||
if ( is_wp_error( $id_token_claim ) ){
|
||||
$this->error_redirect( $id_token_claim );
|
||||
}
|
||||
|
||||
//
|
||||
$valid = $client->validate_id_token_claim( $id_token_claim );
|
||||
|
||||
if ( is_wp_error( $valid ) ){
|
||||
$this->error_redirect( $valid );
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// // if desired, admins can use regex to determine if the identity value is valid
|
||||
// // according to their own standards expectations
|
||||
// if ( ! empty( $settings->allowed_regex ) &&
|
||||
// preg_match( $settings->allowed_regex, $id_token_claim[ $settings->identity_key ] ) !== 1
|
||||
// ) {
|
||||
// return new WP_Error( 'no-subject-identity', __( 'No subject identity' ), $id_token_claim );
|
||||
// }
|
||||
|
||||
|
||||
//
|
||||
$user_claim = $client->get_user_claim( $token_response );
|
||||
|
||||
if ( is_wp_error( $user_claim ) ){
|
||||
$this->error_redirect( $user_claim );
|
||||
}
|
||||
|
||||
//
|
||||
$valid = $client->validate_user_claim( $user_claim, $id_token_claim );
|
||||
|
||||
if ( is_wp_error( $valid ) ){
|
||||
$this->error_redirect( $valid );
|
||||
}
|
||||
|
||||
// - end authorization
|
||||
|
||||
|
||||
|
||||
// request is authenticated and authorized
|
||||
// - start user handling
|
||||
$user_identity = $client->get_user_identity( $id_token_claim );
|
||||
$user = $this->get_user_by_identity( $user_identity );
|
||||
|
||||
// if we didn't find an existing user, we'll need to create it
|
||||
if ( ! $user ) {
|
||||
$user = $this->create_new_user( $user_identity, $user_claim );
|
||||
}
|
||||
|
||||
//
|
||||
$valid = $this->validate_user( $user );
|
||||
|
||||
if ( is_wp_error( $valid ) ){
|
||||
$this->error_redirect( $valid );
|
||||
}
|
||||
|
||||
$this->login_user( $user, $token_response, $id_token_claim, $user_claim, $user_identity );
|
||||
|
||||
$this->logger->log( "Successful login for: {$user->user_login} ({$user->ID})", 'login-success' );
|
||||
|
||||
wp_redirect( home_url() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the potential WP_User
|
||||
*
|
||||
* @param $user
|
||||
*
|
||||
* @return \WP_Error
|
||||
*/
|
||||
function validate_user( $user ){
|
||||
// ensure our found user is a real WP_User
|
||||
if ( ! is_a( $user, 'WP_User' ) || ! $user->exists() ) {
|
||||
return new WP_Error( 'invalid-user', __( 'Invalid user' ), $user );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param $user
|
||||
*/
|
||||
function login_user( $user, $token_response, $id_token_claim, $user_claim, $user_identity ){
|
||||
// hey, we made it!
|
||||
// let's remember the tokens for future reference
|
||||
update_user_meta( $user->ID, 'openid-connect-generic-last-token-response', $token_response );
|
||||
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 );
|
||||
|
||||
// save our authorization cookie for the response expiration
|
||||
$oauth_expiry = $token_response['expires_in'] + current_time( 'timestamp', TRUE );
|
||||
setcookie( $this->cookie_id_key, $user_identity, $oauth_expiry, COOKIEPATH, COOKIE_DOMAIN, TRUE );
|
||||
|
||||
// get a cookie and go home!
|
||||
wp_set_auth_cookie( $user->ID, FALSE );
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param $user_identity
|
||||
*
|
||||
* @return false|\WP_User
|
||||
*/
|
||||
function get_user_by_identity( $user_identity ){
|
||||
// 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' => $user_identity,
|
||||
)
|
||||
)
|
||||
) );
|
||||
|
||||
// if we found an existing users, grab the first one returned
|
||||
if ( $user_query->get_total() > 0 ) {
|
||||
$users = $user_query->get_results();
|
||||
return $users[0];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid user_login collisions by incrementing
|
||||
*
|
||||
* @param $user_claim array
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_username_from_claim( $user_claim ) {
|
||||
if ( isset( $user_claim['preferred_username'] ) && ! empty( $user_claim['preferred_username'] ) ) {
|
||||
$desired_username = $user_claim['preferred_username'];
|
||||
}
|
||||
else if ( isset( $user_claim['name'] ) && ! empty( $user_claim['name'] ) ) {
|
||||
$desired_username = $user_claim['name'];
|
||||
}
|
||||
else if ( isset( $user_claim['email'] ) && ! empty( $user_claim['email'] ) ) {
|
||||
$tmp = explode( '@', $user_claim['email'] );
|
||||
$desired_username = $tmp[0];
|
||||
}
|
||||
else {
|
||||
// nothing to build a name from
|
||||
return new WP_Error( 'no-username', __( 'No appropriate username found' ), $user_claim );
|
||||
}
|
||||
|
||||
// normalize the data a bit
|
||||
$desired_username = strtolower( preg_replace( '/[^a-zA-Z\_0-9]/', '', $desired_username ) );
|
||||
|
||||
// copy the username for incrementing
|
||||
$username = $desired_username;
|
||||
|
||||
// original user gets "name"
|
||||
// second user gets "name2"
|
||||
// etc
|
||||
$count = 1;
|
||||
while ( username_exists( $username ) ) {
|
||||
$count ++;
|
||||
$username = $desired_name . $count;
|
||||
}
|
||||
|
||||
return $username;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param $user_identity
|
||||
* @param $user_claim
|
||||
*
|
||||
* @return \WP_Error | \WP_User
|
||||
*/
|
||||
function create_new_user( $user_identity, $user_claim){
|
||||
// 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['email'] ) ) {
|
||||
$email = $user_claim['email'];
|
||||
$username = $this->get_username_from_claim( $user_claim );
|
||||
}
|
||||
// if no name exists, attempt another request for userinfo
|
||||
else if ( isset( $token_response['access_token'] ) ) {
|
||||
$user_claim_result = $this->client->request_userinfo( $token_response['access_token'] );
|
||||
|
||||
// make sure we didn't get an error
|
||||
if ( is_wp_error( $user_claim_result ) ) {
|
||||
return new WP_Error( 'bad-user-claim-result', __( 'Bad user claim result' ), $user_claim_result );
|
||||
}
|
||||
|
||||
$user_claim = json_decode( $user_claim_result['body'], TRUE );
|
||||
|
||||
if ( isset( $user_claim['email'] ) ) {
|
||||
$email = $user_claim['email'];
|
||||
$username = $this->get_username_from_claim( $user_claim );
|
||||
}
|
||||
}
|
||||
|
||||
// allow other plugins / themes to determine authorization
|
||||
// of new accounts based on the returned user claim
|
||||
$create_user = apply_filters( 'openid-connect-generic-user-creation-test', TRUE, $user_claim );
|
||||
|
||||
if ( ! $create_user ) {
|
||||
return new WP_Error( 'cannot-authorize', __( 'Can not authorize.' ), $create_user );
|
||||
}
|
||||
|
||||
// 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 ) ) {
|
||||
return new WP_Error( 'failed-user-creation', __( 'Failed user creation.' ), $uid );
|
||||
}
|
||||
|
||||
$user = get_user_by( 'id', $uid );
|
||||
|
||||
$this->log( "New user created: {$user->user_login} ($uid)", 'success' );
|
||||
|
||||
// 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 );
|
||||
|
||||
// allow plugins / themes to take action on new user creation
|
||||
do_action( 'openid-connect-generic-user-create', $user, $user_claim );
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
@ -0,0 +1,323 @@
|
||||
<?php
|
||||
|
||||
class OpenID_Connect_Generic_Client {
|
||||
|
||||
private $client_id;
|
||||
private $client_secret;
|
||||
private $scope;
|
||||
private $endpoint_login;
|
||||
private $endpoint_userinfo;
|
||||
private $endpoint_token;
|
||||
|
||||
// login flow "ajax" endpoint
|
||||
private $redirect_uri;
|
||||
|
||||
// states are only valid for 3 minutes
|
||||
private $state_time_limit = 180;
|
||||
|
||||
/**
|
||||
* @param $client_id
|
||||
* @param $client_secret
|
||||
* @param $scope
|
||||
* @param $endpoint_login
|
||||
* @param $endpoint_userinfo
|
||||
* @param $endpoint_token
|
||||
* @param $redirect_uri
|
||||
*/
|
||||
function __construct( $client_id, $client_secret, $scope, $endpoint_login, $endpoint_userinfo, $endpoint_token, $redirect_uri ){
|
||||
$this->client_id = $client_id;
|
||||
$this->client_secret = $client_secret;
|
||||
$this->scope = $scope;
|
||||
$this->endpoint_login = $endpoint_login;
|
||||
$this->endpoint_userinfo = $endpoint_userinfo;
|
||||
$this->endpoint_token = $endpoint_token;
|
||||
$this->redirect_uri = $redirect_uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single use authentication url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function make_authentication_url() {
|
||||
$url = sprintf( '%1$s?response_type=code&scope=%2$s&client_id=%3$s&state=%4$s&redirect_uri=%5$s',
|
||||
$this->endpoint_login,
|
||||
urlencode( $this->scope ),
|
||||
urlencode( $this->client_id ),
|
||||
$this->new_state(),
|
||||
urlencode( $this->redirect_uri )
|
||||
);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param $request
|
||||
*
|
||||
* @return \WP_Error
|
||||
*/
|
||||
function validate_authentication_request( $request ){
|
||||
// look for an existing error of some kind
|
||||
if ( isset( $request['error'] ) ) {
|
||||
return new WP_Error( 'unknown-error', 'An unknown error occurred.', $request );
|
||||
}
|
||||
|
||||
// make sure we have a legitimate authentication code and valid state
|
||||
if ( ! isset( $request['code'] ) ) {
|
||||
return new WP_Error( 'no-code', 'No authentication code present in the request.', $request );
|
||||
}
|
||||
|
||||
// check the client request state
|
||||
if ( ! isset( $request['state'] ) || ! $this->check_state( $request['state'] ) ){
|
||||
return new WP_Error( 'missing-state', __( 'Missing state.' ), $request );
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authorization code from the request
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function get_authentication_code( $request ){
|
||||
return $request['code'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the authorization_code, request an authentication token from the idp
|
||||
*
|
||||
* @param $code - authorization_code
|
||||
*
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
function request_authentication_token( $code ) {
|
||||
$request = array(
|
||||
'body' => array(
|
||||
'code' => $code,
|
||||
'client_id' => $this->client_id,
|
||||
'client_secret' => $this->client_secret,
|
||||
'redirect_uri' => $this->redirect_uri,
|
||||
'grant_type' => 'authorization_code',
|
||||
'scope' => $this->scope,
|
||||
)
|
||||
);
|
||||
|
||||
// allow modifications to the request
|
||||
$request = apply_filters( 'openid-connect-generic-alter-request', $request, 'get-authentication-token' );
|
||||
|
||||
// call the server and ask for a token
|
||||
$response = wp_remote_post( $this->endpoint_token, $request );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param $token_result
|
||||
* @return array|mixed|object
|
||||
*/
|
||||
function get_token_response( $token_result ){
|
||||
if ( ! isset( $token_result['body'] ) ){
|
||||
return new WP_Error( 'missing-token-body', __( 'Missing token body.' ), $token_response );
|
||||
}
|
||||
|
||||
// extract token response from token
|
||||
$token_response = json_decode( $token_result['body'], TRUE );
|
||||
|
||||
return $token_response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Using an access_token, request the userinfo from the idp
|
||||
*
|
||||
* @param $access_token
|
||||
*
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
function request_userinfo( $access_token ) {
|
||||
// allow modifications to the request
|
||||
$request = apply_filters( 'openid-connect-generic-alter-request', array(), 'get-userinfo' );
|
||||
|
||||
// attempt the request
|
||||
$response = wp_remote_get( $this->endpoint_userinfo . '?access_token=' . $access_token, $request );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() . microtime( true ) );
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param $token_response
|
||||
*
|
||||
* @return bool|\WP_Error
|
||||
*/
|
||||
function validate_token_response( $token_response ){
|
||||
// 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'
|
||||
) {
|
||||
return new WP_Error( 'invalid-token-response', 'Invalid token response', $token_response );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param $token_response
|
||||
*
|
||||
* @return array|mixed|object|\WP_Error
|
||||
*/
|
||||
function get_id_token_claim( $token_response ){
|
||||
// name sure we have an id_token
|
||||
if ( ! isset( $token_response['id_token'] ) ) {
|
||||
return new WP_Error( 'no-identity-token', __( 'No identity token' ), $token_response );
|
||||
}
|
||||
|
||||
// break apart the id_token in the response for decoding
|
||||
$tmp = explode( '.', $token_response['id_token'] );
|
||||
|
||||
if ( ! isset( $tmp[1] ) ) {
|
||||
return new WP_Error( 'no-identity-token', __( 'No identity token' ), $token_response );
|
||||
}
|
||||
|
||||
// Extract the id_token's claims from the token
|
||||
$id_token_claim = json_decode( base64_decode( $tmp[1] ), TRUE );
|
||||
|
||||
return $id_token_claim;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param $id_token_claim
|
||||
*
|
||||
* @return bool|\WP_Error
|
||||
*/
|
||||
function validate_id_token_claim( $id_token_claim ){
|
||||
// make sure we can find our identification data and that it has a value
|
||||
if ( ! isset( $id_token_claim['sub'] ) || empty( $id_token_claim['sub'] ) ) {
|
||||
return new WP_Error( 'no-subject-identity', __( 'No subject identity' ), $id_token_claim );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param $token_response
|
||||
*
|
||||
* @return array|mixed|object|\WP_Error
|
||||
*/
|
||||
function get_user_claim( $token_response ){
|
||||
// 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'] ) ) {
|
||||
return new WP_Error( 'bad-claim', __( 'Bad user claim' ), $user_claim_result );
|
||||
}
|
||||
|
||||
$user_claim = json_decode( $user_claim_result['body'], TRUE );
|
||||
|
||||
return $user_claim;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param $user_claim
|
||||
* @param $id_token_claim
|
||||
*
|
||||
* @return \WP_Error
|
||||
*/
|
||||
function validate_user_claim( $user_claim, $id_token_claim ) {
|
||||
// must be an array
|
||||
if ( ! is_array( $user_claim ) ){
|
||||
return new WP_Error( 'invalid-user-claim', __( 'Invalid user claim' ), $user_claim );
|
||||
}
|
||||
|
||||
// make sure the id_token sub === user_claim sub, according to spec
|
||||
if ( $id_token_claim['sub' ] !== $user_claim['sub'] ) {
|
||||
return new WP_Error( 'invalid-user-claim', __( 'Invalid user claim' ), func_get_args() );
|
||||
}
|
||||
|
||||
// allow for other plugins to alter the login success
|
||||
$login_user = apply_filters( 'openid-connect-generic-user-login-test', TRUE, $user_claim );
|
||||
|
||||
if ( ! $login_user ) {
|
||||
return new WP_Error( 'unauthorized', __( 'Unauthorized' ), $login_user );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
function get_user_identity( $id_token_claim ){
|
||||
return $id_token_claim['sub'];
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
class OpenID_Connect_Generic_Login_Form {
|
||||
|
||||
private $settings;
|
||||
private $client_wrapper;
|
||||
|
||||
/**
|
||||
* @param $settings
|
||||
* @param $client_wrapper
|
||||
*/
|
||||
function __construct( $settings, $client_wrapper ){
|
||||
$this->settings = $settings;
|
||||
$this->client_wrapper = $client_wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $settings
|
||||
* @param $client_wrapper
|
||||
*
|
||||
* @return \OpenID_Connect_Generic_Login_Form
|
||||
*/
|
||||
static public function register( $settings, $client_wrapper ){
|
||||
$login_form = new self( $settings, $client_wrapper );
|
||||
|
||||
// alter the login form as dictated by settings
|
||||
add_filter( 'login_message', array( $login_form, 'handle_login_page' ), 99 );
|
||||
|
||||
return $login_form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements filter login_message
|
||||
*
|
||||
* @param $message
|
||||
* @return string
|
||||
*/
|
||||
function handle_login_page( $message ) {
|
||||
$settings = $this->settings;
|
||||
|
||||
// errors and auto login can't happen at the same time
|
||||
if ( isset( $_GET['login-error'] ) ) {
|
||||
$message = $this->make_error_output( $_GET['login-error'], $_GET['message'] );
|
||||
}
|
||||
else if ( $settings->login_type == 'auto' ) {
|
||||
wp_redirect( $this->client_wrapper->get_authentication_url() );
|
||||
exit;
|
||||
}
|
||||
|
||||
// login button is appended to existing messages in case of error
|
||||
if ( $settings->login_type == 'button' ) {
|
||||
$message .= $this->make_login_button();
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an error message to the user
|
||||
*
|
||||
* @param $error_code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function make_error_output( $error_code, $error_message ) {
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div id="login_error">
|
||||
<strong><?php _e( 'ERROR'); ?>: </strong>
|
||||
<?php print $error_message; ?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a login button (link)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function make_login_button() {
|
||||
$text = apply_filters( 'openid-connect-generic-login-button-text', __( 'Login with OpenID Connect' ) );
|
||||
$href = $this->client_wrapper->get_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();
|
||||
}
|
||||
}
|
@ -0,0 +1,223 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple class for logging messages to the options table
|
||||
*/
|
||||
if ( !class_exists( 'WP_Option_Logger' ) ) :
|
||||
|
||||
class WP_Option_Logger {
|
||||
|
||||
// wp option name/key
|
||||
private $option_name;
|
||||
|
||||
// default message type
|
||||
private $default_message_type;
|
||||
|
||||
// the number of items to keep in the log
|
||||
private $log_limit;
|
||||
|
||||
// whether or not the
|
||||
private $logging_enabled;
|
||||
|
||||
// internal cache of logs
|
||||
private $logs;
|
||||
|
||||
/**
|
||||
* Setup the logger according to the needs of the instance
|
||||
*
|
||||
* @param string $option_name
|
||||
* @param string $default_message_type
|
||||
* @param bool|TRUE $logging_enabled
|
||||
* @param int $log_limit
|
||||
*/
|
||||
function __construct( $option_name, $default_message_type = 'none', $logging_enabled = true, $log_limit = 1000 ){
|
||||
$this->option_name = $option_name;
|
||||
$this->default_message_type = $default_message_type;
|
||||
$this->logging_enabled = (bool) $logging_enabled;
|
||||
$this->log_limit = (int) $log_limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe logger to a set of filters
|
||||
*
|
||||
* @param $filter_names
|
||||
* @param int $priority
|
||||
*/
|
||||
function log_filters( $filter_names, $priority = 10 ){
|
||||
if ( ! is_array( $filter_names ) ) {
|
||||
$filter_names = array( $filter_names );
|
||||
}
|
||||
|
||||
foreach ( $filter_names as $filter ){
|
||||
add_filter( $filter, array( $this, 'log_hook' ), $priority );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe logger to a set of actions
|
||||
*
|
||||
* @param $action_names
|
||||
* @param $priority
|
||||
*/
|
||||
function log_actions( $action_names, $priority ){
|
||||
if ( ! is_array( $action_names ) ) {
|
||||
$action_names = array( $action_names );
|
||||
}
|
||||
|
||||
foreach ( $action_names as $action ){
|
||||
add_filter( $action, array( $this, 'log_hook' ), $priority );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the data
|
||||
*
|
||||
* @param null $arg1
|
||||
* @return null
|
||||
*/
|
||||
function log_hook( $arg1 = null ){
|
||||
$this->log( func_get_args(), current_filter() );
|
||||
return $arg1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an array of data to the logs
|
||||
*
|
||||
* @param $data array
|
||||
* @return bool
|
||||
*/
|
||||
public function log( $data, $type = null ) {
|
||||
if ( (bool) $this->logging_enabled ) {
|
||||
$logs = $this->get_logs();
|
||||
$logs[] = $this->make_message( $data, $type );
|
||||
$logs = $this->upkeep_logs( $logs );
|
||||
return $this->save_logs( $logs );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all log messages
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_logs() {
|
||||
if ( is_null( $this->logs ) ) {
|
||||
$this->logs = get_option( $this->option_name, array() );
|
||||
}
|
||||
|
||||
return $this->logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message array containing the data and other information
|
||||
*
|
||||
* @param $data (mixed)
|
||||
* @return array
|
||||
*/
|
||||
private function make_message( $data, $type ){
|
||||
// determine the type of message
|
||||
if ( empty( $type ) ) {
|
||||
$this->default_message_type;
|
||||
|
||||
if ( is_array( $data ) && isset( $data['type'] ) ){
|
||||
$type = $data['type'];
|
||||
}
|
||||
else if ( is_wp_error( $data ) ){
|
||||
$type = $data->get_error_code();
|
||||
}
|
||||
}
|
||||
|
||||
// construct our message
|
||||
$message = array(
|
||||
'type' => $type,
|
||||
'time' => time(),
|
||||
'user_ID' => get_current_user_id(),
|
||||
'uri' => $_SERVER['REQUEST_URI'],
|
||||
'data' => $data,
|
||||
);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep our log count under the limit
|
||||
*
|
||||
* @param $message array - extra data about the message
|
||||
* @return array
|
||||
*/
|
||||
private function upkeep_logs( $logs ) {
|
||||
$items_to_remove = count( $logs ) - $this->log_limit;
|
||||
|
||||
if ( $items_to_remove > 0 ){
|
||||
// keep only the last $log_limit messages from the end
|
||||
$logs = array_slice( $logs, ( $items_to_remove * -1) );
|
||||
}
|
||||
|
||||
return $logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the log messages
|
||||
*
|
||||
* @param $logs
|
||||
* @return bool
|
||||
*/
|
||||
private function save_logs( $logs ){
|
||||
// save our logs
|
||||
$this->logs = $logs;
|
||||
return update_option( $this->option_name, $logs, FALSE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a simple html table of all the logs
|
||||
*
|
||||
* @param array $logs
|
||||
* @return string
|
||||
*/
|
||||
public function get_logs_table( $logs = array() ){
|
||||
if ( empty( $logs ) ) {
|
||||
$logs = $this->get_logs();
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<table class="wp-list-table widefat fixed striped posts">
|
||||
<thead>
|
||||
<th>Details</th>
|
||||
<th style="width: 85%;">Data</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $logs as $log ) { ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
<label><?php _e( 'Type' ); ?>: </label>
|
||||
<?php print $log['type']; ?>
|
||||
</div>
|
||||
<div>
|
||||
<label><?php _e( 'Date' ); ?>: </label>
|
||||
<?php print date( 'Y-m-d H:i:s', $log['time'] ); ?>
|
||||
</div>
|
||||
<div>
|
||||
<label><?php _e( 'User' ); ?>: </label>
|
||||
<?php print ( $log['user_ID'] ) ? get_userdata( $log['user_ID'] )->user_login : '0'; ?>
|
||||
</div>
|
||||
<div>
|
||||
<label><?php _e( 'URI: ' ); ?>: </label>
|
||||
<?php print $log['uri']; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><?php var_dump( $log['data'] ); ?></td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
$output = ob_get_clean();
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
if ( ! class_exists( 'WP_Option_Settings' ) ) :
|
||||
|
||||
class WP_Option_Settings {
|
||||
|
||||
// wp option name/key
|
||||
private $option_name;
|
||||
|
||||
// stored option values array
|
||||
private $values = array();
|
||||
|
||||
// default plugin settings values
|
||||
private $default_settings;
|
||||
|
||||
function __construct( $option_name, $default_settings = array() ){
|
||||
$this->option_name = $option_name;
|
||||
$this->default_settings = $default_settings;
|
||||
$this->values = get_option( $this->option_name, $this->default_settings );
|
||||
}
|
||||
|
||||
function __get( $key ){
|
||||
if ( isset( $this->values[ $key ] ) ) {
|
||||
return $this->values[ $key ];
|
||||
}
|
||||
}
|
||||
|
||||
function __set( $key, $value ){
|
||||
$this->values[ $key ] = $value;
|
||||
}
|
||||
|
||||
function __isset( $key ){
|
||||
return isset( $this->values[ $key ] );
|
||||
}
|
||||
|
||||
function get_option_name() {
|
||||
return $this->option_name;
|
||||
}
|
||||
|
||||
function save(){
|
||||
update_option( $this->option_name, $this->values );
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
Loading…
Reference in New Issue