diff --git a/includes/openid-connect-generic-client-wrapper.php b/includes/openid-connect-generic-client-wrapper.php index b049b72..3922ba0 100644 --- a/includes/openid-connect-generic-client-wrapper.php +++ b/includes/openid-connect-generic-client-wrapper.php @@ -9,6 +9,7 @@ * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+ */ +use MatthiasMullie\Minify\JS; use \WP_Error as WP_Error; /** @@ -122,6 +123,13 @@ class OpenID_Connect_Generic_Client_Wrapper { add_action( 'wp_loaded', array( $client_wrapper, 'ensure_tokens_still_fresh' ) ); } + // Disable some profile settings + add_action( 'personal_options_update', array( $client_wrapper, 'disable_profile_change' ), 5 ); + add_action( 'show_user_profile', array( $client_wrapper, 'show_user_profile' ), 0 ); + + // Avatar + add_filter( 'get_avatar', array( $client_wrapper, 'get_avatar' ), 100, 5); + return $client_wrapper; } @@ -238,6 +246,76 @@ class OpenID_Connect_Generic_Client_Wrapper { return apply_filters( 'openid-connect-generic-auth-url', $url ); } + public function disable_profile_change( $user_id ) { + $user = get_user_by('id', $user_id ); + $_POST['email'] = $user->user_email; + // $_POST['nickname'] = $user->user_nickname; + } + + public function show_user_profile( $user ) { + if ( !empty( $this->settings->profile_edit_url ) ) { + $profileUrl = $this->settings->profile_edit_url; + $clientId = $this->settings->client_id; + $currentUrl = admin_url( 'profile.php' ); + if ( strpos( $profileUrl, '?' ) === false ) { + $profileUrl .= '?'; + } else { + $profileUrl .= '&'; + } + $profileUrl .= http_build_query( array( + 'referrer' => $clientId, + 'referrer_uri' => $currentUrl, + ) ); +?> + +

全局用户资料

+ + + + + + + + +comment_type ) && '' != $id_or_email->comment_type && 'comment' != $id_or_email->comment_type ) + return false; + + if ( !empty( $id_or_email->user_id ) ) { + $id = (int) $id_or_email->user_id; + $user = get_userdata( $id ); + } + } + if ($user) { + $avatarSrc = get_user_meta( $user->ID , 'openid-connect-generic-avatar', true ); + if ( !empty($avatarSrc) ) { + return "{$alt}"; + } + } + return false; + } + /** * Handle retrieval and validation of refresh_token. * @@ -358,35 +436,12 @@ class OpenID_Connect_Generic_Client_Wrapper { $url .= $query ? '&' : '?'; // Prevent redirect back to the IDP when logging out in auto mode. - if ( 'auto' === $this->settings->login_type && strpos( $redirect_url, 'wp-login.php?loggedout=true' ) ) { + if ( 'auto' === $this->settings->login_type && strpos( $redirect_url, 'wp-login.php' ) ) { // By default redirect back to the site home. $redirect_url = home_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'] ) && 'https://accounts.google.com' == $claim['iss'] ) { - /* - * 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 ) ); - } + return $url . 'redirect_uri=' . urlencode($redirect_url); } /** @@ -522,6 +577,7 @@ class OpenID_Connect_Generic_Client_Wrapper { } 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 ); + $this->update_user_profile( $user, $user_claim ); } // Validate the found / created user. @@ -597,7 +653,29 @@ class OpenID_Connect_Generic_Client_Wrapper { 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 ); + $remember = isset($id_token_claim['remember_me']) && $id_token_claim['remember_me']; + if ( $remember ) { + /** + * Filters the duration of the authentication cookie expiration period. + * + * @since 2.8.0 + * + * @param int $length Duration of the expiration period in seconds. + * @param int $user_id User ID. + * @param bool $remember Whether to remember the user login. Default false. + */ + $expiration = time() + apply_filters( 'auth_cookie_expiration', 14 * DAY_IN_SECONDS, $user->ID, $remember ); + + /* + * Ensure the browser will continue to send the cookie after the expiration time is reached. + * Needed for the login grace period in wp_validate_auth_cookie(). + */ + $expire = $expiration + ( 12 * HOUR_IN_SECONDS ); + } else { + /** This filter is documented in wp-includes/pluggable.php */ + $expiration = time() + apply_filters( 'auth_cookie_expiration', 2 * DAY_IN_SECONDS, $user->ID, $remember ); + $expire = 0; + } $manager = WP_Session_Tokens::get_instance( $user->ID ); $token = $manager->create( $expiration ); @@ -605,7 +683,7 @@ class OpenID_Connect_Generic_Client_Wrapper { $this->save_refresh_token( $manager, $token, $token_response ); // you did great, have a cookie! - wp_set_auth_cookie( $user->ID, false, '', $token ); + wp_set_auth_cookie( $user->ID, $remember, '', $token ); do_action( 'wp_login', $user->user_login, $user ); } @@ -831,6 +909,7 @@ class OpenID_Connect_Generic_Client_Wrapper { $email = $subject_identity; $nickname = $subject_identity; $displayname = $subject_identity; + $avatar = isset($user_claim['avatar']) ? $user_claim['avatar'] : ''; $values_missing = false; // Allow claim details to determine username, email, nickname and displayname. @@ -912,6 +991,7 @@ class OpenID_Connect_Generic_Client_Wrapper { if ( $uid ) { $user = $this->update_existing_user( $uid, $subject_identity ); do_action( 'openid-connect-generic-update-user-using-current-claim', $user, $user_claim ); + $this->update_user_profile( $user, $user_claim ); return $user; } } @@ -932,8 +1012,10 @@ class OpenID_Connect_Generic_Client_Wrapper { '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 ); @@ -950,6 +1032,7 @@ class OpenID_Connect_Generic_Client_Wrapper { // 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 ); + add_user_meta( $user->ID, 'openid-connect-generic-avatar', $avatar ); // Log the results. $this->logger->log( "New user created: {$user->user_login} ($uid)", 'success' ); @@ -978,4 +1061,34 @@ class OpenID_Connect_Generic_Client_Wrapper { // Return our updated user. return get_user_by( 'id', $uid ); } + + /** + * Update user profile from OpenID Connect Provider + * + * @param \WP_User $user The WordPress User ID. + * @param string $user_claim The user info from the IDP. + */ + public function update_user_profile( $user, $user_claim ) { + $avatar = isset($user_claim['avatar']) ? $user_claim['avatar'] : ''; + update_user_meta( $user->ID, 'openid-connect-generic-avatar', $avatar ); + + $email = $user->user_email; + if ( isset($user_claim['email'] ) ) $email = $user_claim['email']; + + $nickname = $user->nickname; + if ( isset($user_claim['name'] ) ) $nickname = $user_claim['name']; + + $displayname = $user->display_name; + if ( $displayname === $user->nickname ) $displayname = $nickname; + + $userData = [ + 'ID' => $user->ID, + 'user_login' => $user->user_login, + 'user_email' => $email, + 'nickname' => $nickname, + 'display_name' => $displayname + ]; + $userData = apply_filters( 'openid-connect-generic-alter-user-data', $userData, $user_claim ); + $user = wp_insert_user( $userData ); + } } diff --git a/includes/openid-connect-generic-option-settings.php b/includes/openid-connect-generic-option-settings.php index fbd0c44..0cfeb92 100644 --- a/includes/openid-connect-generic-option-settings.php +++ b/includes/openid-connect-generic-option-settings.php @@ -44,18 +44,22 @@ * @property string $displayname_format The key(s) in the user claim array to formulate the user's display name. * @property bool $identify_with_username The flag which indicates how the user's identity will be determined. * @property int $state_time_limit The valid time limit of the state, in seconds. Defaults to 180 seconds. - * + * @property string $profile_edit_url The IDP account management console URL. + * @property string $realm The IDP realm. + * * Plugin Settings: * - * @property bool $enforce_privacy The flag to indicates whether a user us required to be authenticated to access the site. - * @property bool $alternate_redirect_uri The flag to indicate whether to use the alternative redirect URI. - * @property bool $token_refresh_enable The flag whether to support refresh tokens by IDPs. - * @property bool $link_existing_users The flag to indicate whether to link to existing WordPress-only accounts or greturn an error. - * @property bool $create_if_does_not_exist The flag to indicate whether to create new users or not. - * @property bool $redirect_user_back The flag to indicate whether to redirect the user back to the page on which they started. - * @property bool $redirect_on_logout The flag to indicate whether to redirect to the login screen on session expiration. - * @property bool $enable_logging The flag to enable/disable logging. - * @property int $log_limit The maximum number of log entries to keep. + * @property bool $enable_webhook The flag whether receive webhook from IDPs. + * @property string $webhook_key The key for webhook authentication. + * @property bool $enforce_privacy The flag to indicates whether a user us required to be authenticated to access the site. + * @property bool $alternate_redirect_uri The flag to indicate whether to use the alternative redirect URI. + * @property bool $token_refresh_enable The flag whether to support refresh tokens by IDPs. + * @property bool $link_existing_users The flag to indicate whether to link to existing WordPress-only accounts or greturn an error. + * @property bool $create_if_does_not_exist The flag to indicate whether to create new users or not. + * @property bool $redirect_user_back The flag to indicate whether to redirect the user back to the page on which they started. + * @property bool $redirect_on_logout The flag to indicate whether to redirect to the login screen on session expiration. + * @property bool $enable_logging The flag to enable/disable logging. + * @property int $log_limit The maximum number of log entries to keep. */ class OpenID_Connect_Generic_Option_Settings { diff --git a/includes/openid-connect-generic-settings-page.php b/includes/openid-connect-generic-settings-page.php index 070eb69..eb04cd4 100644 --- a/includes/openid-connect-generic-settings-page.php +++ b/includes/openid-connect-generic-settings-page.php @@ -305,27 +305,51 @@ class OpenID_Connect_Generic_Settings_Page { 'type' => 'checkbox', 'section' => 'authorization_settings', ), - 'nickname_key' => array( + 'nickname_key' => array( 'title' => __( 'Nickname Key', 'daggerhart-openid-connect-generic' ), 'description' => __( 'Where in the user claim array to find the user\'s nickname. Possible standard values: preferred_username, name, or sub.', 'daggerhart-openid-connect-generic' ), 'example' => 'preferred_username', 'type' => 'text', 'section' => 'client_settings', ), - 'email_format' => array( + 'email_format' => array( 'title' => __( 'Email Formatting', 'daggerhart-openid-connect-generic' ), 'description' => __( 'String from which the user\'s email address is built. Specify "{email}" as long as the user claim contains an email claim.', 'daggerhart-openid-connect-generic' ), 'example' => '{email}', 'type' => 'text', 'section' => 'client_settings', ), - 'displayname_format' => array( + 'displayname_format' => array( 'title' => __( 'Display Name Formatting', 'daggerhart-openid-connect-generic' ), 'description' => __( 'String from which the user\'s display name is built.', 'daggerhart-openid-connect-generic' ), 'example' => '{given_name} {family_name}', 'type' => 'text', 'section' => 'client_settings', ), + 'profile_edit_url' => array( + 'title' => __( 'Profile Management URL', 'daggerhart-openid-connect-generic' ), + 'description' => __( 'For users to edit their profile on IDP.' ), + 'type' => 'text', + 'section' => 'client_settings', + ), + 'enable_webhook' => array( + 'title' => __( 'Enable Webhook', 'daggerhart-openid-connect-generic' ), + 'description' => __( 'Allows receive webhook from IDP.' ), + 'type' => 'checkbox', + 'section' => 'client_settings', + ), + 'webhook_key' => array( + 'title' => __( 'Webhook key', 'daggerhart-openid-connect-generic' ), + 'description' => __( 'Secret key to authentication webhook' ), + 'type' => 'text', + 'section' => 'client_settings', + ), + 'realm' => array( + 'title' => __( 'Realm', 'daggerhart-openid-connect-generic' ), + 'description' => __( 'Identify provider realm, required when enabled webhook' ), + 'type' => 'text', + 'section' => 'client_settings', + ), 'identify_with_username' => array( 'title' => __( 'Identify with User Name', 'daggerhart-openid-connect-generic' ), 'description' => __( 'If checked, the user\'s identity will be determined by the user name instead of the email address.', 'daggerhart-openid-connect-generic' ), @@ -419,6 +443,11 @@ class OpenID_Connect_Generic_Settings_Page { if ( $this->settings->alternate_redirect_uri ) { $redirect_uri = site_url( '/openid-connect-authorize' ); } + + $webhook_endpoint = rest_url( '/openid-connect/v1/webhook' ) . '/[IDP type]'; + if ( !empty($this->settings->webhook_key) ) { + $webhook_endpoint .= '?key=' . $this->settings->webhook_key; + } ?>

@@ -442,6 +471,20 @@ class OpenID_Connect_Generic_Settings_Page {

+ settings->enable_webhook) { + ?> +

+ + +

+

+ + keycloak +

+

[openid_connect_generic_login_button] diff --git a/includes/openid-connect-generic-webhook.php b/includes/openid-connect-generic-webhook.php new file mode 100644 index 0000000..22a7614 --- /dev/null +++ b/includes/openid-connect-generic-webhook.php @@ -0,0 +1,139 @@ +settings = $settings; + $this->client_wrapper = $client_wrapper; + $this->logger = $logger; + } + + public static function register(OpenID_Connect_Generic_Option_Settings $settings, + OpenID_Connect_Generic_Client_Wrapper $client_wrapper, + OpenID_Connect_Generic_Option_Logger $logger) { + $webhook = new OpenID_Connect_Generic_Webhook($settings, $client_wrapper, $logger); + + add_action('rest_api_init', [$webhook, 'register_rest_api']); + + return $webhook; + } + + public function register_rest_api() { + register_rest_route('openid-connect/v1', '/webhook/keycloak', [ + [ + 'methods' => ['GET', 'POST'], + 'callback' => [$this, 'webhook_keycloak'], + ], + 'schema' => [$this, 'webhook_keycloak_schema'], + ]); + } + + public function webhook_keycloak(WP_REST_Request $request) { + if (!$this->settings->enable_webhook) { + $this->logger->log('Webhook is disabled in setting.', 'webhook'); + return rest_ensure_response([ + 'status' => -1, + 'code' => 'ERR::WEBHOOK_DISABLED', + 'error' => 'Webhook is disabled in setting.' + ]); + } + $query = $request->get_query_params(); + if (!empty($this->settings->webhook_key) && $query['key'] != $this->settings->webhook_key) { + $this->logger->log('Webhook key is incorrect.', 'webhook'); + return rest_ensure_response([ + 'status' => -1, + 'code' => 'ERR::WEBHOOK_KEY_INCORRECT', + 'error' => 'Webhook key is incorrect.' + ]); + } + $event = $request->get_json_params(); + if (is_null($event)) { + $this->logger->log('Event body is empty.', 'webhook'); + return rest_ensure_response([ + 'status' => -1, + 'code' => 'ERR::EMPTY_EVENT_BODY', + 'error' => 'Event body is empty.', + ]); + } + if ($event['realmId'] != $this->settings->realm) { + $this->logger->log("Realm id mismatch: {$event['realmId']}", 'webhook'); + return rest_ensure_response([ + 'status' => 0, + 'warning' => "Realm id mismatch: {$event['realmId']}", + ]); + } + if (!in_array($event['type'], ['UPDATE_PROFILE', 'UPDATE_EMAIL'])) { + return rest_ensure_response([ + 'status' => 0, + 'warning' => 'Event type ignored', + ]); + } + + $subject = $event['userId']; + $user = $this->client_wrapper->get_user_by_identity($subject); + if (!$user) { + $this->logger->log("Cannot find user for subject: {$subject}", 'webhook'); + return rest_ensure_response([ + 'status' => 0, + 'warning' => "Cannot find user for subject: {$subject}", + ]); + } + if (isset($event['userInfo'])) { // Update user profile + $this->client_wrapper->update_user_profile($user, $event['userInfo']); + $this->logger->log("Updated user profile: {$user->user_login}", 'webhook'); + } + + return rest_ensure_response([ + 'status' => 1 + ]); + } + + public function webhook_keycloak_schema() { + return [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + // The title property marks the identity of the resource. + 'title' => 'keycloak-webhook', + 'type' => 'object', + 'required' => ['type', 'realmId', 'userId', 'userInfo'], + 'properties' => [ + 'type' => [ + 'description' => esc_html__('Event Type', 'daggerhart-openid-connect-generic'), + 'type' => 'string', + ], + 'realmId' => [ + 'description' => esc_html__('IDP Realm ID', 'daggerhart-openid-connect-generic'), + 'type' => 'string', + ], + 'userId' => [ + 'description' => esc_html__('IDP User ID', 'daggerhart-openid-connect-generic'), + 'type' => 'string', + ], + 'userInfo' => [ + 'description' => esc_html__('IDP User Info', 'daggerhart-openid-connect-generic'), + 'type' => 'object', + ], + ], + ]; + } +} \ No newline at end of file diff --git a/openid-connect-generic.php b/openid-connect-generic.php index fcb94dd..713896c 100644 --- a/openid-connect-generic.php +++ b/openid-connect-generic.php @@ -173,6 +173,8 @@ class OpenID_Connect_Generic { if ( is_admin() ) { OpenID_Connect_Generic_Settings_Page::register( $this->settings, $this->logger ); } + + OpenID_Connect_Generic_Webhook::register( $this->settings, $this->client_wrapper, $this->logger ); } /** @@ -342,10 +344,14 @@ class OpenID_Connect_Generic { 'identity_key' => 'preferred_username', 'nickname_key' => 'preferred_username', 'email_format' => '{email}', + 'profile_edit_url' => '', 'displayname_format' => '', 'identify_with_username' => false, + 'realm' => '', // Plugin settings. + 'enable_webhook' => false, + 'webhook_key' => '', 'enforce_privacy' => 0, 'alternate_redirect_uri' => 0, 'token_refresh_enable' => 1,