summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Grothoff <christian@grothoff.org>2020-10-13 01:04:44 +0200
committerChristian Grothoff <christian@grothoff.org>2020-10-13 01:04:44 +0200
commitf8c893781da44156f282f6847ab0e24bc67bda70 (patch)
tree0111fe3369e09db8c2939b2624a3d1b61877a65e
parent036f4b4c24a951a0072a94b6d7084573579c0240 (diff)
downloadwoocommerce-taler-f8c893781da44156f282f6847ab0e24bc67bda70.tar.gz
woocommerce-taler-f8c893781da44156f282f6847ab0e24bc67bda70.tar.bz2
woocommerce-taler-f8c893781da44156f282f6847ab0e24bc67bda70.zip
add current modified OIDC plugin code
-rwxr-xr-xoidc/openid-connect-generic-client-wrapper.php800
-rwxr-xr-xoidc/openid-connect-generic-client.php430
-rwxr-xr-xoidc/openid-connect-generic-option-logger.php246
-rwxr-xr-xoidc/openid-connect-generic-option-settings.php60
-rwxr-xr-xoidc/openid-connect-generic-settings-page.php467
5 files changed, 2003 insertions, 0 deletions
diff --git a/oidc/openid-connect-generic-client-wrapper.php b/oidc/openid-connect-generic-client-wrapper.php
new file mode 100755
index 0000000..3823a3d
--- /dev/null
+++ b/oidc/openid-connect-generic-client-wrapper.php
@@ -0,0 +1,800 @@
+<?php
+
+class OpenID_Connect_Generic_Client_Wrapper {
+
+ private $client;
+
+ // settings object
+ private $settings;
+
+ // logger object
+ private $logger;
+
+ // token refresh info cookie key
+ private $cookie_token_refresh_key = 'openid-connect-generic-refresh';
+
+ // user redirect cookie key
+ public $cookie_redirect_key = 'openid-connect-generic-redirect';
+
+ // WP_Error if there was a problem, or false if no error
+ private $error = false;
+
+ /**
+ * Inject necessary objects and services into the client
+ *
+ * @param \OpenID_Connect_Generic_Client $client
+ * @param \OpenID_Connect_Generic_Option_Settings $settings
+ * @param \OpenID_Connect_Generic_Option_Logger $logger
+ */
+ function __construct( OpenID_Connect_Generic_Client $client, OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ){
+ $this->client = $client;
+ $this->settings = $settings;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Hook the client into WP
+ *
+ * @param \OpenID_Connect_Generic_Client $client
+ * @param \OpenID_Connect_Generic_Option_Settings $settings
+ * @param \OpenID_Connect_Generic_Option_Logger $logger
+ *
+ * @return \OpenID_Connect_Generic_Client_Wrapper
+ */
+ static public function register( OpenID_Connect_Generic_Client $client, OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ){
+ $client_wrapper = new self( $client, $settings, $logger );
+
+ // integrated logout
+ if ( $settings->endpoint_end_session ) {
+ add_filter( 'allowed_redirect_hosts', array( $client_wrapper, 'update_allowed_redirect_hosts' ), 99, 1 );
+ add_filter( 'logout_redirect', array( $client_wrapper, 'get_end_session_logout_redirect_url' ), 99, 3 );
+ }
+
+ // 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' ) );
+ }
+
+ if ( $settings->alternate_redirect_uri ){
+ // provide an alternate route for authentication_request_callback
+ add_rewrite_rule( '^openid-connect-authorize/?', 'index.php?openid-connect-authorize=1', 'top' );
+ add_rewrite_tag( '%openid-connect-authorize%', '1' );
+ add_action( 'parse_request', array( $client_wrapper, 'alternate_redirect_uri_parse_request' ) );
+ }
+
+ // verify token for any logged in user
+ if ( is_user_logged_in() ) {
+ add_action( 'wp_loaded', array($client_wrapper, 'ensure_tokens_still_fresh'));
+ }
+
+ return $client_wrapper;
+ }
+
+ /**
+ * Implements WP action - parse_request
+ *
+ * @param $query
+ *
+ * @return mixed
+ */
+ function alternate_redirect_uri_parse_request( $query ){
+ if ( isset( $query->query_vars['openid-connect-authorize'] ) &&
+ $query->query_vars['openid-connect-authorize'] === '1' )
+ {
+ $this->authentication_request_callback();
+ exit;
+ }
+
+ return $query;
+ }
+
+ /**
+ * Get the authentication url from the client
+ *
+ * @return string
+ */
+ function get_authentication_url(){
+ return $this->client->make_authentication_url();
+ }
+
+ /**
+ * Handle retrieval and validation of refresh_token
+ */
+ function ensure_tokens_still_fresh() {
+ if ( ! is_user_logged_in() ) {
+ return;
+ }
+
+ $user_id = wp_get_current_user()->ID;
+ $manager = WP_Session_Tokens::get_instance( $user_id );
+ $token = wp_get_session_token();
+ $session = $manager->get( $token );
+
+ if ( ! isset( $session[ $this->cookie_token_refresh_key ] ) ) {
+ // not an OpenID-based session
+ return;
+ }
+
+ $current_time = current_time( 'timestamp', true );
+ $refresh_token_info = $session[ $this->cookie_token_refresh_key ];
+
+ $next_access_token_refresh_time = $refresh_token_info[ 'next_access_token_refresh_time' ];
+
+ if ( $current_time < $next_access_token_refresh_time ) {
+ return;
+ }
+
+ $refresh_token = $refresh_token_info[ 'refresh_token' ];
+ $refresh_expires = $refresh_token_info[ 'refresh_expires' ];
+
+ if ( ! $refresh_token || ( $refresh_expires && $current_time > $refresh_expires ) ) {
+ wp_logout();
+
+ if ( $this->settings->redirect_on_logout ) {
+ $this->error_redirect( new WP_Error( 'access-token-expired', __( 'Session expired. Please login again.' ) ) );
+ }
+
+ return;
+ }
+
+ $token_result = $this->client->request_new_tokens( $refresh_token );
+
+ if ( is_wp_error( $token_result ) ) {
+ wp_logout();
+ $this->error_redirect( $token_result );
+ }
+
+ $token_response = $this->client->get_token_response( $token_result );
+
+ if ( is_wp_error( $token_response ) ) {
+ wp_logout();
+ $this->error_redirect( $token_response );
+ }
+
+ $this->save_refresh_token( $manager, $token, $token_response );
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Add the end_session endpoint to WP core's whitelist of redirect hosts
+ *
+ * @param array $allowed
+ *
+ * @return array
+ */
+ function update_allowed_redirect_hosts( array $allowed ) {
+ $host = parse_url( $this->settings->endpoint_end_session, PHP_URL_HOST );
+ if ( ! $host ) {
+ return false;
+ }
+
+ $allowed[] = $host;
+ return $allowed;
+ }
+
+ /**
+ * Handle the logout redirect for end_session endpoint
+ *
+ * @param $redirect_url
+ *
+ * @return string
+ */
+ function get_end_session_logout_redirect_url( $redirect_url, $requested_redirect_to, $user ) {
+ $url = $this->settings->endpoint_end_session;
+ $query = parse_url( $url, PHP_URL_QUERY );
+ $url .= $query ? '&' : '?';
+
+ // prevent redirect back to the IdP when logging out in auto mode
+ if ( $this->settings->login_type === 'auto' && $redirect_url === 'wp-login.php?loggedout=true' ) {
+ $redirect_url = '';
+ }
+
+ $token_response = $user->get('openid-connect-generic-last-token-response');
+ if (! $token_response ) {
+ // happens if non-openid login was used
+ return $redirect_url;
+ }
+ else if ( ! parse_url( $redirect_url, PHP_URL_HOST ) ) {
+ // convert to absolute url if needed. site_url() to be friendly with non-standard (Bedrock) layout
+ $redirect_url = site_url( $redirect_url );
+ }
+
+ $claim = $user->get( 'openid-connect-generic-last-id-token-claim' );
+
+ if ( isset( $claim['iss'] ) && $claim['iss'] == 'https://accounts.google.com' ) {
+ /* Google revoke endpoint
+ 1. expects the *access_token* to be passed as "token"
+ 2. does not support redirection (post_logout_redirect_uri)
+ So just redirect to regular WP logout URL.
+ (we would *not* disconnect the user from any Google service even if he was
+ initially disconnected to them) */
+ return $redirect_url;
+ }
+ else {
+ return $url . sprintf( 'id_token_hint=%s&post_logout_redirect_uri=%s', $token_response['id_token'], urlencode( $redirect_url ) );
+ }
+ }
+
+ /**
+ * Modify outgoing requests according to settings
+ *
+ * @param $request
+ * @param $op
+ *
+ * @return mixed
+ */
+ function alter_request( $request, $op ) {
+ if ( !empty( $this->settings->http_request_timeout ) && is_numeric( $this->settings->http_request_timeout ) ) {
+ $request['timeout'] = intval( $this->settings->http_request_timeout );
+ }
+
+ 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() {
+ $client = $this->client;
+
+ // start the authentication flow
+ $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 );
+
+ // allow for other plugins to alter data before validation
+ $token_response = apply_filters( 'openid-connect-modify-token-response-before-validation', $token_response );
+
+ 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 );
+
+ // allow for other plugins to alter data before validation
+ $id_token_claim = apply_filters( 'openid-connect-modify-id-token-claim-before-validation', $id_token_claim );
+
+ if ( is_wp_error( $id_token_claim ) ){
+ $this->error_redirect( $id_token_claim );
+ }
+
+ // validate our id_token has required values
+ $valid = $client->validate_id_token_claim( $id_token_claim );
+
+ if ( is_wp_error( $valid ) ){
+ $this->error_redirect( $valid );
+ }
+
+ // if userinfo endpoint is set, exchange the token_response for a user_claim
+ if ( !empty( $this->settings->endpoint_userinfo ) && isset( $token_response['access_token'] )) {
+ $user_claim = $client->get_user_claim( $token_response );
+ } else {
+ $user_claim = $id_token_claim;
+ }
+
+ if ( is_wp_error( $user_claim ) ){
+ $this->error_redirect( $user_claim );
+ }
+
+ // validate our user_claim has required values
+ $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
+ */
+ $subject_identity = $client->get_subject_identity( $id_token_claim );
+ $user = $this->get_user_by_identity( $subject_identity );
+
+ if ( ! $user ) {
+ if ( $this->settings->create_if_does_not_exist ) {
+ $user = $this->create_new_user( $subject_identity, $user_claim );
+ if ( is_wp_error( $user ) ) {
+ $this->error_redirect( $user );
+ }
+ }
+ else {
+ $this->error_redirect( new WP_Error( 'identity-not-map-existing-user', __( "User identity is not link to an existing WordPress user"), $user_claim ) );
+ }
+ }
+ else {
+ // allow plugins / themes to take action using current claims on existing user (e.g. update role)
+ do_action( 'openid-connect-generic-update-user-using-current-claim', $user, $user_claim );
+ }
+
+ // validate the found / created user
+ $valid = $this->validate_user( $user );
+
+ if ( is_wp_error( $valid ) ){
+ $this->error_redirect( $valid );
+ }
+
+ // login the found / created user
+ $this->login_user( $user, $token_response, $id_token_claim, $user_claim, $subject_identity );
+
+ do_action( 'openid-connect-generic-user-logged-in', $user );
+
+ // log our success
+ $this->logger->log( "Successful login for: {$user->user_login} ({$user->ID})", 'login-success' );
+
+ // redirect back to the origin page if enabled
+ $redirect_url = isset( $_COOKIE[ $this->cookie_redirect_key ] ) ? esc_url_raw( $_COOKIE[ $this->cookie_redirect_key ] ) : false;
+
+ if( $this->settings->redirect_user_back && !empty( $redirect_url ) ) {
+ do_action( 'openid-connect-generic-redirect-user-back', $redirect_url, $user );
+ setcookie( $this->cookie_redirect_key, $redirect_url, 1, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
+ wp_redirect( $redirect_url );
+ }
+ // otherwise, go home!
+ else {
+ wp_redirect( home_url() );
+ }
+
+ exit;
+ }
+
+ /**
+ * Validate the potential WP_User
+ *
+ * @param $user
+ *
+ * @return true|\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;
+ }
+
+ /**
+ * Record user meta data, and provide an authorization cookie
+ *
+ * @param $user
+ */
+ function login_user( $user, $token_response, $id_token_claim, $user_claim, $subject_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 );
+
+ // Create the WP session, so we know its token
+ $expiration = time() + apply_filters( 'auth_cookie_expiration', 2 * DAY_IN_SECONDS, $user->ID, false );
+ $manager = WP_Session_Tokens::get_instance( $user->ID );
+ $token = $manager->create( $expiration );
+
+ // Save the refresh token in the session
+ $this->save_refresh_token( $manager, $token, $token_response );
+
+ // you did great, have a cookie!
+ wp_set_auth_cookie( $user->ID, false, '', $token);
+ do_action( 'wp_login', $user->user_login, $user );
+ }
+
+ /**
+ * Save refresh token to WP session tokens
+ *
+ * @param $manager
+ * @param $token
+ * @param $token_response
+ */
+ function save_refresh_token( $manager, $token, $token_response ) {
+ $session = $manager->get($token);
+ $now = current_time( 'timestamp' , true );
+ $session[$this->cookie_token_refresh_key] = array(
+ 'next_access_token_refresh_time' => $token_response['expires_in'] + $now,
+ 'refresh_token' => isset( $token_response[ 'refresh_token' ] ) ? $token_response[ 'refresh_token' ] : false,
+ 'refresh_expires' => false,
+ );
+ if ( isset( $token_response[ 'refresh_expires_in' ] ) ) {
+ $refresh_expires_in = $token_response[ 'refresh_expires_in' ];
+ if ($refresh_expires_in > 0) {
+ // leave enough time for the actual refresh request to go through
+ $refresh_expires = $now + $refresh_expires_in - 5;
+ $session[$this->cookie_token_refresh_key]['refresh_expires'] = $refresh_expires;
+ }
+ }
+ $manager->update($token, $session);
+ return;
+ }
+
+ /**
+ * Get the user that has meta data matching a
+ *
+ * @param $subject_identity
+ *
+ * @return false|\WP_User
+ */
+ function get_user_by_identity( $subject_identity ){
+ // look for user by their openid-connect-generic-subject-identity value
+ $user_query = new WP_User_Query( array(
+ 'meta_query' => array(
+ array(
+ 'key' => 'openid-connect-generic-subject-identity',
+ 'value' => $subject_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 ) {
+ // allow settings to take first stab at username
+ if ( !empty( $this->settings->identity_key ) && isset( $user_claim[ $this->settings->identity_key ] ) ) {
+ $desired_username = $user_claim[ $this->settings->identity_key ];
+ }
+ else 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
+ $transliterated_username = iconv( 'UTF-8', 'ASCII//TRANSLIT', $desired_username );
+ if ( empty( $transliterated_username ) ) {
+ return new WP_Error( 'username-transliteration-failed', __( "Username $desired_username could not be transliterated" ), $desired_username );
+ }
+ $normalized_username = strtolower( preg_replace( '/[^a-zA-Z0-9 _.\-@]/', '', $transliterated_username ) );
+ if ( empty( $normalized_username ) ) {
+ return new WP_Error( 'username-normalization-failed', __( "Username $transliterated_username could not be normalized" ), $transliterated_username );
+ }
+
+ // copy the username for incrementing
+ $username = $normalized_username;
+
+ if ( ! $this->settings->link_existing_users ) {
+ // original user gets "name"
+ // second user gets "name2"
+ // etc
+ $count = 1;
+ while ( username_exists( $username ) ) {
+ $count ++;
+ $username = $normalized_username . $count;
+ }
+ }
+
+ return $username;
+ }
+
+ /**
+ * Get a nickname
+ *
+ * @param $user_claim array
+ *
+ * @return string
+ */
+ private function get_nickname_from_claim( $user_claim ) {
+ $desired_nickname = null;
+ // allow settings to take first stab at nickname
+ if ( !empty( $this->settings->nickname_key ) && isset( $user_claim[ $this->settings->nickname_key ] ) ) {
+ $desired_nickname = $user_claim[ $this->settings->nickname_key ];
+ }
+ return $desired_nickname;
+ }
+
+ /**
+ * Build a string from the user claim according to the specified format.
+ *
+ * @param $format string
+ * @param $user_claim array
+ *
+ * @return string
+ */
+ private function format_string_with_claim( $format, $user_claim, $error_on_missing_key = false ) {
+ $matches = null;
+ $string = '';
+ $i = 0;
+ if ( preg_match_all( '/\{[^}]*\}/u', $format, $matches, PREG_OFFSET_CAPTURE ) ) {
+ foreach ( $matches[ 0 ] as $match ) {
+ $key = substr($match[ 0 ], 1, -1);
+ $string .= substr( $format, $i, $match[ 1 ] - $i );
+ if ( ! isset( $user_claim[ $key ] ) ) {
+ if ( $error_on_missing_key ) {
+ return new WP_Error( 'incomplete-user-claim', __( 'User claim incomplete' ),
+ array('message'=>'Unable to find key: '.$key.' in user_claim',
+ 'hint'=>'Verify OpenID Scope includes a scope with the attributes you need',
+ 'user_claim'=>$user_claim,
+ 'format'=>$format) );
+ }
+ } else {
+ $string .= $user_claim[ $key ];
+ }
+ $i = $match[ 1 ] + strlen( $match[ 0 ] );
+ }
+ }
+ $string .= substr( $format, $i );
+ return $string;
+ }
+
+ /**
+ * Get a displayname
+ *
+ * @param $user_claim array
+ *
+ * @return string
+ */
+ private function get_displayname_from_claim( $user_claim, $error_on_missing_key = false ) {
+ if ( ! empty( $this->settings->displayname_format ) ) {
+ return $this->format_string_with_claim( $this->settings->displayname_format, $user_claim, $error_on_missing_key );
+ }
+ return null;
+ }
+
+ /**
+ * Get an email
+ *
+ * @param $user_claim array
+ *
+ * @return string
+ */
+ private function get_email_from_claim( $user_claim, $error_on_missing_key = false ) {
+ if ( ! empty( $this->settings->email_format ) ) {
+ return $this->format_string_with_claim( $this->settings->email_format, $user_claim, $error_on_missing_key );
+ }
+ return null;
+ }
+
+ /**
+ * Create a new user from details in a user_claim
+ *
+ * @param $subject_identity
+ * @param $user_claim
+ *
+ * @return \WP_Error | \WP_User
+ */
+ function create_new_user( $subject_identity, $user_claim ) {
+ $user_claim = apply_filters( 'openid-connect-generic-alter-user-claim', $user_claim );
+
+ // default username & email to the subject identity
+ $username = $subject_identity;
+ $email = $subject_identity;
+ $nickname = $subject_identity;
+ $displayname = $subject_identity;
+
+ $values_missing = false;
+
+ // allow claim details to determine username, email, nickname and displayname.
+ $_email = $this->get_email_from_claim( $user_claim, true );
+ if ( is_wp_error( $_email ) ) {
+ $values_missing = true;
+ } else if ( $_email !== null ) {
+ $email = $_email;
+ }
+
+ $_username = $this->get_username_from_claim( $user_claim );
+ if ( is_wp_error( $_username ) ) {
+ $values_missing = true;
+ } else if ( $_username !== null ) {
+ $username = $_username;
+ }
+
+ $_nickname = $this->get_nickname_from_claim( $user_claim );
+ if ( is_wp_error( $_nickname ) ) {
+ $values_missing = true;
+ } else if ( $_nickname !== null) {
+ $nickname = $_nickname;
+ }
+
+ $_displayname = $this->get_displayname_from_claim( $user_claim, true );
+ if ( is_wp_error( $_displayname ) ) {
+ $values_missing = true;
+ } else if ( $_displayname !== null ) {
+ $displayname = $_displayname;
+ }
+
+ // attempt another request for userinfo if some values are missing
+ if ( $values_missing && isset( $token_response['access_token'] ) && !empty( $this->settings->endpoint_userinfo) ) {
+ $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 );
+ }
+
+ $_email = $this->get_email_from_claim( $user_claim, true );
+ if ( is_wp_error( $_email ) ) {
+ return $_email;
+ } else if ( $_email !== null ) {
+ $email = $_email;
+ }
+
+ $_username = $this->get_username_from_claim( $user_claim );
+ if ( is_wp_error( $_username ) ) {
+ return $_username;
+ } else if ( $_username !== null ) {
+ $username = $_username;
+ }
+
+ $_nickname = $this->get_nickname_from_claim( $user_claim );
+ if ( is_wp_error( $_nickname ) ) {
+ return $_nickname;
+ } else if ( $_nickname === null) {
+ $nickname = $username;
+ }
+
+ $_displayname = $this->get_displayname_from_claim( $user_claim, true );
+ if ( is_wp_error( $_displayname ) ) {
+ return $_displayname;
+ } else if ( $_displayname === null ) {
+ $displayname = $nickname;
+ }
+
+ // before trying to create the user, first check if a user with the same email already exists
+ if( $this->settings->link_existing_users ) {
+ if ( $this->settings->identify_with_username) {
+ $uid = username_exists( $username );
+ } else {
+ $uid = email_exists( $email );
+ }
+ if ( $uid ) {
+ $user = $this->update_existing_user( $uid, $subject_identity );
+ do_action( 'openid-connect-generic-update-user-using-current-claim', $user, $user_claim );
+ return $user;
+ }
+ }
+
+ // 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 );
+ }
+
+ $user_data = array(
+ 'user_login' => $username,
+ 'user_pass' => wp_generate_password( 32, true, true ),
+ 'user_email' => $email,
+ 'display_name' => $displayname,
+ 'nickname' => $nickname,
+ 'first_name' => isset( $user_claim[ 'given_name' ] ) ? $user_claim[ 'given_name' ]: '',
+ 'last_name' => isset( $user_claim[ 'family_name' ] ) ? $user_claim[ 'family_name' ]: '',
+ );
+ $user_data = apply_filters( 'openid-connect-generic-alter-user-data', $user_data, $user_claim );
+
+ // create the new user
+ $uid = wp_insert_user( $user_data );
+
+ // 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 );
+ }
+
+ // retrieve our new user
+ $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-subject-identity', (string) $subject_identity, true );
+
+ // log the results
+ $this->logger->log( "New user created: {$user->user_login} ($uid)", 'success' );
+
+ // allow plugins / themes to take action on new user creation
+ do_action( 'openid-connect-generic-user-create', $user, $user_claim );
+
+ return $user;
+ }
+
+ /**
+ * Update an existing user with OpenID Connect meta data
+ *
+ * @param $uid
+ * @param $subject_identity
+ *
+ * @return \WP_Error | \WP_User
+ */
+ function update_existing_user( $uid, $subject_identity ) {
+ // add the OpenID Connect meta data
+ update_user_meta( $uid, 'openid-connect-generic-subject-identity', (string) $subject_identity );
+
+ // allow plugins / themes to take action on user update
+ do_action( 'openid-connect-generic-user-update', $uid );
+
+ // return our updated user
+ return get_user_by( 'id', $uid );
+ }
+}
diff --git a/oidc/openid-connect-generic-client.php b/oidc/openid-connect-generic-client.php
new file mode 100755
index 0000000..100d3a1
--- /dev/null
+++ b/oidc/openid-connect-generic-client.php
@@ -0,0 +1,430 @@
+<?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;
+
+ // logger object
+ private $logger;
+
+ /**
+ * Client constructor
+ *
+ * @param $client_id
+ * @param $client_secret
+ * @param $scope
+ * @param $endpoint_login
+ * @param $endpoint_userinfo
+ * @param $endpoint_token
+ * @param $redirect_uri
+ * @param $state_time_limit time states are valid in seconds
+ */
+ function __construct( $client_id, $client_secret, $scope, $endpoint_login, $endpoint_userinfo, $endpoint_token, $redirect_uri, $state_time_limit, $logger){
+ $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;
+ $this->state_time_limit = $state_time_limit;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Create a single use authentication url
+ *
+ * @return string
+ */
+ function make_authentication_url() {
+ $separator = '?';
+ if ( stripos( $this->endpoint_login, '?' ) !== FALSE ) {
+ $separator = '&';
+ }
+ $url = sprintf( '%1$s%2$sresponse_type=code&scope=%3$s&client_id=%4$s&state=%5$s&redirect_uri=%6$s',
+ $this->endpoint_login,
+ $separator,
+ rawurlencode( $this->scope ),
+ rawurlencode( $this->client_id ),
+ $this->new_state(),
+ rawurlencode( $this->redirect_uri )
+ );
+
+ $this->logger->log( apply_filters( 'openid-connect-generic-auth-url', $url ), 'make_authentication_url' );
+ return apply_filters( 'openid-connect-generic-auth-url', $url );
+ }
+
+ /**
+ * Validate the request for login authentication
+ *
+ * @param $request
+ *
+ * @return array|\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']) ) {
+ do_action( 'openid-connect-generic-no-state-provided' );
+ return new WP_Error( 'missing-state', __( 'Missing state.' ), $request );
+ }
+
+ if ( ! $this->check_state( $request['state'] ) ) {
+ return new WP_Error( 'invalid-state', __( 'Invalid state.' ), $request );
+ }
+
+ return $request;
+ }
+
+ /**
+ * Get the authorization code from the request
+ *
+ * @param $request array
+ *
+ * @return string|\WP_Error
+ */
+ 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 ) {
+
+ // Add Host header - required for when the openid-connect endpoint is behind a reverse-proxy
+ $parsed_url = parse_url($this->endpoint_token);
+ $host = $parsed_url['host'];
+
+ $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,
+ ),
+ 'headers' => array( 'Host' => $host )
+ );
+
+ // 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
+ $this->logger->log( $this->endpoint_token, 'request_authentication_token' );
+ $response = wp_remote_post( $this->endpoint_token, $request );
+
+ if ( is_wp_error( $response ) ){
+ $response->add( 'request_authentication_token' , __( 'Request for authentication token failed.' ) );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Using the refresh token, request new tokens from the idp
+ *
+ * @param $refresh_token - refresh token previously obtained from token response.
+ *
+ * @return array|\WP_Error
+ */
+ function request_new_tokens( $refresh_token ) {
+ $request = array(
+ 'body' => array(
+ 'refresh_token' => $refresh_token,
+ 'client_id' => $this->client_id,
+ 'client_secret' => $this->client_secret,
+ 'grant_type' => 'refresh_token'
+ )
+ );
+
+ // allow modifications to the request
+ $request = apply_filters( 'openid-connect-generic-alter-request', $request, 'refresh-token' );
+
+ // call the server and ask for new tokens
+ $this->logger->log( $this->endpoint_token, 'request_new_tokens' );
+ $response = wp_remote_post( $this->endpoint_token, $request );
+
+ if ( is_wp_error( $response ) ) {
+ $response->add( 'refresh_token' , __( 'Refresh token failed.' ) );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Extract and decode the token body of a token 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_result );
+ }
+
+ // extract token response from token
+ $token_response = json_decode( $token_result['body'], true );
+
+ if ( isset( $token_response[ 'error' ] ) ) {
+ $error = $token_response[ 'error' ];
+ $error_description = $error;
+ if ( isset( $token_response[ 'error_description' ] ) ) {
+ $error_description = $token_response[ 'error_description' ];
+ }
+ return new WP_Error( $error, $error_description, $token_result );
+ }
+
+ return $token_response;
+ }
+
+
+ /**
+ * Exchange an access_token for a user_claim from the userinfo endpoint
+ *
+ * @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' );
+
+ // section 5.3.1 of the spec recommends sending the access token using the authorization header
+ // a filter may or may not have already added headers - make sure they exist then add the token
+ if ( !array_key_exists( 'headers', $request ) || !is_array( $request['headers'] ) ) {
+ $request['headers'] = array();
+ }
+
+ $request['headers']['Authorization'] = 'Bearer '.$access_token;
+
+ // Add Host header - required for when the openid-connect endpoint is behind a reverse-proxy
+ $parsed_url = parse_url($this->endpoint_userinfo);
+ $host = $parsed_url['host'];
+
+ if ( !empty( $parsed_url['port'] ) ) {
+ $host.= ":{$parsed_url['port']}";
+ }
+
+ $request['headers']['Host'] = $host;
+
+ // attempt the request including the access token in the query string for backwards compatibility
+ $this->logger->log( $this->endpoint_userinfo, 'request_userinfo' );
+ $response = wp_remote_post( $this->endpoint_userinfo, $request );
+
+ if ( is_wp_error( $response ) ){
+ $response->add( 'request_userinfo' , __( 'Request for userinfo failed.' ) );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Generate a new state, save it as a transient,
+ * and return the state hash.
+ *
+ * @return string
+ */
+ function new_state() {
+ // new state w/ timestamp
+ $state = md5( mt_rand() . microtime( true ) );
+ set_transient( 'openid-connect-generic-state--' . $state, $state, $this->state_time_limit );
+
+ return $state;
+ }
+
+ /**
+ * Check the existence of a given state transient.
+ *
+ * @param $state
+ *
+ * @return bool
+ */
+ function check_state( $state ) {
+
+ $state_found = true;
+
+ if ( ! get_option( '_transient_openid-connect-generic-state--' . $state ) ) {
+ do_action( 'openid-connect-generic-state-not-found', $state );
+ $state_found = false;
+ }
+
+ $valid = get_transient( 'openid-connect-generic-state--' . $state );
+
+ if ( ! $valid && $state_found ) {
+ do_action( 'openid-connect-generic-state-expired', $state );
+ }
+
+ return !!$valid;
+ }
+
+ /**
+ * Ensure that the token meets basic requirements
+ *
+ * @param $token_response
+ *
+ * @return bool|\WP_Error
+ */
+ function validate_token_response( $token_response ){
+ // we need to ensure 2 specific items exist with the token response in order
+ // to proceed with confidence: id_token and token_type == 'Bearer'
+ if ( ! isset( $token_response['id_token'] ) ||
+ ! isset( $token_response['token_type'] ) || strcasecmp( $token_response['token_type'], 'Bearer' )
+ ) {
+ return new WP_Error( 'invalid-token-response', 'Invalid token response', $token_response );
+ }
+
+ return true;
+ }
+
+ /**
+ * Extract the id_token_claim from the token_response
+ *
+ * @param $token_response
+ *
+ * @return array|\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( 'missing-identity-token', __( 'Missing identity token' ), $token_response );
+ }
+
+ // Extract the id_token's claims from the token
+ $id_token_claim = json_decode(
+ base64_decode(
+ str_replace( // because token is encoded in base64 URL (and not just base64)
+ array('-', '_'),
+ array('+', '/'),
+ $tmp[1]
+ )
+ )
+ , true
+ );
+
+ return $id_token_claim;
+ }
+
+ /**
+ * Ensure the id_token_claim contains the required values
+ *
+ * @param $id_token_claim
+ *
+ * @return bool|\WP_Error
+ */
+ function validate_id_token_claim( $id_token_claim ){
+ if ( ! is_array( $id_token_claim ) ) {
+ return new WP_Error( 'bad-id-token-claim', __( 'Bad 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;
+ }
+
+ /**
+ * Attempt to exchange the access_token for a user_claim
+ *
+ * @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;
+ }
+
+ /**
+ * Make sure the user_claim has all required values, and that the subject
+ * identity matches of the id_token matches that of the 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 );
+ }
+
+ // allow for errors from the IDP
+ if ( isset( $user_claim['error'] ) ) {
+ $message = __( 'Error from the IDP' );
+ if ( !empty( $user_claim['error_description'] ) ) {
+ $message = $user_claim['error_description'];
+ }
+ return new WP_Error( 'invalid-user-claim-' . $user_claim['error'], $message, $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( 'incorrect-user-claim', __( 'Incorrect 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 access' ), $login_user );
+ }
+
+ return true;
+ }
+
+ /**
+ * Retrieve the subject identity from the id_token
+ *
+ * @param $id_token_claim array
+ *
+ * @return mixed
+ */
+ function get_subject_identity( $id_token_claim ){
+ return $id_token_claim['sub'];
+ }
+}
diff --git a/oidc/openid-connect-generic-option-logger.php b/oidc/openid-connect-generic-option-logger.php
new file mode 100755
index 0000000..3ea7b1d
--- /dev/null
+++ b/oidc/openid-connect-generic-option-logger.php
@@ -0,0 +1,246 @@
+<?php
+/**
+ * Simple class for logging messages to the options table
+ */
+class OpenID_Connect_Generic_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 mixed
+ * @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;
+ }
+
+ /**
+ * Get the name of the option where this log is stored
+ *
+ * @return string
+ */
+ public function get_option_name(){
+ return $this->option_name;
+ }
+
+ /**
+ * Create a message array containing the data and other information
+ *
+ * @param $data mixed
+ * @param $type
+ *
+ * @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' => preg_replace('/code=([^&]+)/i', 'code=', $_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 );
+ }
+
+ /**
+ * Clear all log messages
+ */
+ public function clear_logs(){
+ $this->save_logs( array() );
+ }
+
+ /**
+ * 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();
+ }
+ $logs = array_reverse( $logs );
+
+ ini_set( 'xdebug.var_display_max_depth', -1 );
+
+ ob_start();
+ ?>
+ <style type="text/css">
+ #logger-table .col-data { width: 85% }
+ #logger-table .col-details div { padding: 4px 0; border-bottom: 1px solid #bbb; }
+ #logger-table .col-details label { font-weight: bold; }
+ </style>
+ <table id="logger-table" class="wp-list-table widefat fixed striped posts">
+ <thead>
+ <th class="col-details">Details</th>
+ <th class="col-data">Data</th>
+ </thead>
+ <tbody>
+ <?php foreach ( $logs as $log ) { ?>
+ <tr>
+ <td class="col-details">
+ <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 ( get_userdata( $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 class="col-data"><pre><?php var_dump( $log['data'] ); ?></pre></td>
+ </tr>
+ <?php } ?>
+ </tbody>
+ </table>
+ <?php
+ $output = ob_get_clean();
+
+ return $output;
+ }
+}
diff --git a/oidc/openid-connect-generic-option-settings.php b/oidc/openid-connect-generic-option-settings.php
new file mode 100755
index 0000000..7b9b9a0
--- /dev/null
+++ b/oidc/openid-connect-generic-option-settings.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Class OpenId_Connect_Generic_Option_Settings
+ */
+class OpenID_Connect_Generic_Option_Settings {
+
+ // wp option name/key
+ private $option_name;
+
+ // stored option values array
+ private $values;
+
+ // default plugin settings values
+ private $default_settings;
+
+ /**
+ * @param $option_name
+ * @param array $default_settings
+ * @param bool|TRUE $granular_defaults
+ */
+ function __construct( $option_name, $default_settings = array(), $granular_defaults = true ){
+ $this->option_name = $option_name;
+ $this->default_settings = $default_settings;
+ $this->values = get_option( $this->option_name, $this->default_settings );
+
+ if ( $granular_defaults ) {
+ $this->values = array_replace_recursive( $this->default_settings, $this->values );
+ }
+ }
+
+ 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 __unset( $key ){
+ unset( $this->values[ $key ]);
+ }
+
+ function get_values(){
+ return $this->values;
+ }
+
+ function get_option_name() {
+ return $this->option_name;
+ }
+
+ function save(){
+ update_option( $this->option_name, $this->values );
+ }
+}
diff --git a/oidc/openid-connect-generic-settings-page.php b/oidc/openid-connect-generic-settings-page.php
new file mode 100755
index 0000000..5178521
--- /dev/null
+++ b/oidc/openid-connect-generic-settings-page.php
@@ -0,0 +1,467 @@
+<?php
+
+/**
+ * Class OpenID_Connect_Generic_Settings_Page.
+ * Admin settings page.
+ */
+class OpenID_Connect_Generic_Settings_Page {
+
+ // local copy of the settings provided by the base plugin
+ private $settings;
+
+ // 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 OpenID_Connect_Generic_Option_Settings $settings
+ * @param OpenID_Connect_Generic_Option_Logger $logger
+ */
+ function __construct( OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ) {
+ $this->settings = $settings;
+ $this->logger = $logger;
+ $this->settings_field_group = $this->settings->get_option_name() . '-group';
+
+ /*
+ * Simple settings fields simply have:
+ *
+ * - title
+ * - description
+ * - type ( checkbox | text | select )
+ * - section - settings/option page section ( client_settings | authorization_settings )
+ * - example (optional example will appear beneath description and be wrapped in <code>)
+ */
+ $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',
+ ),
+ '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 offline_access',
+ 'type' => 'text',
+ 'section' => 'client_settings',
+ ),
+ 'endpoint_login' => array(
+ 'title' => __( 'Login Endpoint URL' ),
+ 'description' => __( 'Identify provider authorization endpoint.' ),
+ 'example' => 'https://example.com/oauth2/authorize',
+ 'type' => 'text',
+ 'section' => 'client_settings',
+ ),
+ 'endpoint_userinfo' => array(
+ 'title' => __( 'Userinfo Endpoint URL' ),
+ 'description' => __( 'Identify provider User information endpoint.' ),
+ 'example' => 'https://example.com/oauth2/UserInfo',
+ 'type' => 'text',
+ 'section' => 'client_settings',
+ ),
+ 'endpoint_token' => array(
+ 'title' => __( 'Token Validation Endpoint URL' ),
+ 'description' => __( 'Identify provider token endpoint.' ),
+ 'example' => 'https://example.com/oauth2/token',
+ 'type' => 'text',
+ 'section' => 'client_settings',
+ ),
+ 'endpoint_end_session' => array(
+ 'title' => __( 'End Session Endpoint URL' ),
+ 'description' => __( 'Identify provider logout endpoint.' ),
+ 'example' => 'https://example.com/oauth2/logout',
+ 'type' => 'text',
+ 'section' => 'client_settings',
+ ),
+ 'identity_key' => array(
+ 'title' => __( 'Identity Key' ),
+ '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".' ),
+ 'example' => 'preferred_username',
+ '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.<br><strong>Not recommended for production sites.</strong>' ),
+ 'type' => 'checkbox',
+ 'section' => 'client_settings',
+ ),
+ 'http_request_timeout' => array(
+ 'title' => __( 'HTTP Request Timeout' ),
+ 'description' => __( 'Set the timeout for requests made to the IDP. Default value is 5.' ),
+ 'example' => 30,
+ 'type' => 'text',
+ 'section' => 'client_settings',
+ ),
+ 'enforce_privacy' => array(
+ 'title' => __( 'Enforce Privacy' ),
+ 'description' => __( 'Require users be logged in to see the site.' ),
+ 'type' => 'checkbox',
+ 'section' => 'authorization_settings',
+ ),
+ 'alternate_redirect_uri' => array(
+ 'title' => __( 'Alternate Redirect URI' ),
+ '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.' ),
+ 'type' => 'checkbox',
+ 'section' => 'authorization_settings',
+ ),
+ 'nickname_key' => array(
+ 'title' => __( 'Nickname Key' ),
+ 'description' => __( 'Where in the user claim array to find the user\'s nickname. Possible standard values: preferred_username, name, or sub.' ),
+ 'example' => 'preferred_username',
+ 'type' => 'text',
+ 'section' => 'client_settings',
+ ),
+ 'email_format' => array(
+ 'title' => __( 'Email Formatting' ),
+ 'description' => __( 'String from which the user\'s email address is built. Specify "{email}" as long as the user claim contains an email claim.' ),
+ 'example' => '{email}',
+ 'type' => 'text',
+ 'section' => 'client_settings',
+ ),
+ 'displayname_format' => array(
+ 'title' => __( 'Display Name Formatting' ),
+ 'description' => __( 'String from which the user\'s display name is built.' ),
+ 'example' => '{given_name} {family_name}',
+ 'type' => 'text',
+ 'section' => 'client_settings',
+ ),
+ 'identify_with_username' => array(
+ 'title' => __( 'Identify with User Name' ),
+ 'description' => __( 'If checked, the user\'s identity will be determined by the user name instead of the email address.' ),
+ 'type' => 'checkbox',
+ 'section' => 'client_settings',
+ ),
+ 'state_time_limit' => array(
+ 'title' => __( 'State time limit' ),
+ 'description' => __( 'State valid time in seconds. Defaults to 180' ),
+ 'type' => 'number',
+ 'section' => 'client_settings',
+ ),
+ 'link_existing_users' => array(
+ 'title' => __( 'Link Existing Users' ),
+ '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.' ),
+ 'type' => 'checkbox',
+ 'section' => 'user_settings',
+ ),
+ 'create_if_does_not_exist' => array(
+ 'title' => __( 'Create user if does not exist' ),
+ 'description' => __( 'If the user identity is not link 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 link to an existing Wordpress user then the authentication failed' ),
+ 'type' => 'checkbox',
+ 'section' => 'user_settings',
+ ),
+ 'redirect_user_back' => array(
+ 'title' => __( 'Redirect Back to Origin Page' ),
+ '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.' ),
+ 'type' => 'checkbox',
+ 'section' => 'user_settings',
+ ),
+ 'redirect_on_logout' => array(
+ 'title' => __( 'Redirect to the login screen session is expired' ),
+ 'description' => __( 'When enabled, this will automatically redirect the user back to the WordPress login page if their access token has expired.' ),
+ 'type' => 'checkbox',
+ 'section' => 'user_settings',
+ ),
+ 'enable_logging' => array(
+ 'title' => __( 'Enable Logging' ),
+ 'description' => __( 'Very simple log messages for debugging purposes.' ),
+ 'type' => 'checkbox',
+ 'section' => 'log_settings',
+ ),
+ 'log_limit' => array(
+ 'title' => __( 'Log Limit' ),
+ 'description' => __( 'Number of items to keep in the log. These logs are stored as an option in the database, so space is limited.' ),
+ 'type' => 'number',
+ 'section' => 'log_settings',
+ ),
+ );
+
+ $fields = apply_filters( 'openid-connect-generic-settings-fields', $fields );
+
+ // some simple pre-processing
+ foreach ( $fields as $key => &$field ) {
+ $field['key'] = $key;
+ $field['name'] = $this->settings->get_option_name() . '[' . $key . ']';
+ }
+
+ // allow alterations of the fields
+ $this->settings_fields = $fields;
+ }
+
+ /**
+ * @param \OpenID_Connect_Generic_Option_Settings $settings
+ * @param \OpenID_Connect_Generic_Option_Logger $logger
+ *
+ * @return \OpenID_Connect_Generic_Settings_Page
+ */
+ static public function register( OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ){
+ $settings_page = new self( $settings, $logger );
+
+ // add our options page the the admin menu
+ add_action( 'admin_menu', array( $settings_page, 'admin_menu' ) );
+
+ // register our settings
+ add_action( 'admin_init', array( $settings_page, 'admin_init' ) );
+
+ return $settings_page;
+ }
+
+ /**
+ * 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, $this->settings->get_option_name(), array(
+ $this,
+ 'sanitize_settings'
+ ) );
+
+ add_settings_section( 'client_settings',
+ __( 'Client Settings' ),
+ array( $this, 'client_settings_description' ),
+ $this->options_page_name
+ );
+
+ add_settings_section( 'user_settings',
+ __( 'WordPress User Settings' ),
+ array( $this, 'user_settings_description' ),
+ $this->options_page_name
+ );
+
+ add_settings_section( 'authorization_settings',
+ __( 'Authorization Settings' ),
+ array( $this, 'authorization_settings_description' ),
+ $this->options_page_name
+ );
+
+ add_settings_section( 'log_settings',
+ __( 'Log Settings' ),
+ array( $this, 'log_settings_description' ),
+ $this->options_page_name
+ );
+
+ // preprocess fields and add them to the page
+ foreach ( $this->settings_fields as $key => $field ) {
+ // 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() {
+ $redirect_uri = admin_url( 'admin-ajax.php?action=openid-connect-authorize' );
+
+ if ( $this->settings->alternate_redirect_uri ){
+ $redirect_uri = site_url( '/openid-connect-authorize' );
+ }
+ ?>
+ <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();
+
+ // simple debug to view settings array
+ if ( isset( $_GET['debug'] ) ) {
+ var_dump( $this->settings->get_values() );
+ }
+ ?>
+ </form>
+
+ <h4><?php _e( 'Notes' ); ?></h4>
+
+ <p class="description">
+ <strong><?php _e( 'Redirect URI' ); ?></strong>
+ <code><?php print $redirect_uri; ?></code>
+ </p>
+ <p class="description">
+ <strong><?php _e( 'Login Button Shortcode' ); ?></strong>
+ <code>[openid_connect_generic_login_button]</code>
+ </p>
+ <p class="description">
+ <strong><?php _e( 'Authentication URL Shortcode' ); ?></strong>
+ <code>[openid_connect_generic_auth_url]</code>
+ </p>
+
+ <?php if ( $this->settings->enable_logging ) { ?>
+ <h2><?php _e( 'Logs' ); ?></h2>
+ <div id="logger-table-wrapper">
+ <?php print $this->logger->get_logs_table(); ?>
+ </div>
+
+ <?php } ?>
+ </div>
+ <?php
+ }
+
+ /**
+ * Output a standard text field
+ *
+ * @param $field
+ */
+ public function do_text_field( $field ) {
+ ?>
+ <input type="<?php print esc_attr( $field['type'] ); ?>"
+ 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="<?php print esc_attr( $field['name'] ); ?>" 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 = isset( $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 user_settings_description() {
+ _e( 'Modify the interaction between OpenID Connect and WordPress users' );
+ }
+
+ public function authorization_settings_description() {
+ _e( 'Control the authorization mechanics of the site' );
+ }
+
+ public function log_settings_description() {
+ _e( 'Log information about login attempts through OpenID Connect Generic' );
+ }
+}