Making WordPress.org

Changeset 9941


Ignore:
Timestamp:
05/29/2020 03:14:48 AM (5 years ago)
Author:
dd32
Message:

Login: Add remote-login functionality to the SSO login code.

This will allow for instances of WordPress in the WordPress.org network (and using it's user tables) to login via login.wordpress.org.

See #5239.

Location:
sites/trunk/common/includes/wporg-sso
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/common/includes/wporg-sso/class-wporg-sso.php

    r7695 r9941  
    22if ( ! class_exists( 'WPOrg_SSO' ) ) {
    33    /**
    4      * Single Sign-On (SSO) handling for WordPress/bbPress instances under *.wordpress.org.
     4     * Single Sign-On (SSO) handling for WordPress/bbPress instances on wordpress.org.
    55     *
    66     * @author stephdau
     
    1010
    1111        const SUPPORT_EMAIL = 'forum-password-resets@wordpress.org';
     12
     13        const VALID_HOSTS = [
     14            'wordpress.org',
     15            'bbpress.org',
     16            'buddypress.org',
     17            'wordcamp.org'
     18        ];
    1219
    1320        public $sso_host_url;
     
    7683            }
    7784
     85            if ( ! preg_match( '!wordpress\.org$!', $this->host ) ) {
     86                $login_url = add_query_arg( 'from', $this->host, $login_url );
     87            }
     88
    7889            return $login_url;
    7990
     
    117128                // We didn't get a redirect_to, but we got a referrer, use that if a valid target.
    118129                $redirect_to_referrer = $_SERVER['HTTP_REFERER'];
    119                 if ( $this->_is_valid_targeted_domain( $redirect_to_referrer ) ) {
     130                if ( $this->_is_valid_targeted_domain( $redirect_to_referrer ) && self::SSO_HOST != parse_url( $redirect_to_referrer, PHP_URL_HOST ) ) {
    120131                    $redirect_to = $redirect_to_referrer;
    121132                }
    122             } else {
     133            } elseif ( self::SSO_HOST !== $this->host ) {
    123134                // Otherwise, attempt to guess the parent dir of where they came from and validate that.
    124135                $redirect_to_source_parent = preg_replace( '/\/[^\/]+\.php\??.*$/', '/', "https://{$this->host}{$_SERVER['REQUEST_URI']}" );
     
    132143
    133144        /**
    134          * Tests if the passed host/domain, or URL, is part of the WordPress.org domain.
     145         * Tests if the passed host/domain, or URL, is part of the WordPress.org network.
    135146         *
    136          * @param unknown $string A domain, hostname, or URL
     147         * @param unknown $host A domain, hostname, or URL
    137148         * @return boolean True is ok, false if not
    138149         */
    139         protected function _is_valid_targeted_domain( $string ) {
    140             if ( empty( $string ) || ! is_string( $string ) ) {
    141                 $string = '';
     150        protected function _is_valid_targeted_domain( $host ) {
     151            if ( empty( $host ) || ! is_string( $host ) || ! strstr( $host, '.' ) ) {
     152                return false;
    142153            }
    143154
    144             if ( strstr( $string , '/' ) ) {
    145                 $url = parse_url( $string );
    146                 $host = ( ! empty( $url['host'] ) ) ? $url['host'] : '';
    147             } else {
    148                 $host = $string;
     155            if ( strstr( $host, '/' ) ) {
     156                $host = parse_url( $host, PHP_URL_HOST );
    149157            }
    150158
    151             if ( ! empty( $host ) && strstr( $host , '.' ) ) {
    152                 return ( preg_match( '/^(.+\.)?wordpress\.org$/', $host ) ) ? true : false;
     159            if ( in_array( $host, self::VALID_HOSTS, true ) ) {
     160                return true;
    153161            }
    154162
    155             return false;
     163            // If not a top-level domain, shrink it down and try again.
     164            $top_level_host = implode( '.', array_slice( explode( '.', $host ), -2 ) );
     165
     166            return in_array( $top_level_host, self::VALID_HOSTS, true );
    156167        }
    157168
  • sites/trunk/common/includes/wporg-sso/wp-plugin.php

    r9833 r9941  
    6767                    add_filter( 'register_url', [ $this, 'add_locale' ], 21 );
    6868                    add_filter( 'lostpassword_url', [ $this, 'add_locale' ], 21 );
     69                } else {
     70                    add_filter( 'login_redirect', [ $this, 'maybe_add_remote_login_bounce_to_post_login_url' ], 10, 3 );
    6971                }
    7072            }
     
    186188                if ( preg_match( '!/wp-login\.php$!', $this->script ) ) {
    187189                    // Don't redirect the 'confirmaction' wp-login handlers to login.wordpress.org.
    188                     if ( isset( $_GET['action']  ) && empty( $_POST ) && 'confirmaction' == $_GET['action'] ) {
     190                    if ( isset( $_GET['action'] ) && empty( $_POST ) && 'confirmaction' == $_GET['action'] ) {
    189191                        return;
     192                    }
     193
     194                    // Allow logout on non-dotorg hosts.
     195                    if ( isset( $_GET['action'] ) && empty( $_POST ) && 'logout' == $_GET['action'] ) {
     196                        if ( ! preg_match( '!wordpress\.org$!', $_SERVER['HTTP_HOST'] ) ) {
     197                            return;
     198                        }
     199                    }
     200
     201                    // Remote SSO login?
     202                    if ( isset( $_GET['action'] ) && 'remote-login' === $_GET['action'] && ! empty( $_GET['sso_token'] ) ) {
     203                        $this->_maybe_perform_remote_login();
    190204                    }
    191205
     
    200214                    // Pay extra attention to the post-process redirect_to
    201215                    $redirect_to_sso_login = add_query_arg( 'redirect_to', urlencode( $redirect_req ), $redirect_to_sso_login );
     216                    if ( ! preg_match( '!wordpress\.org$!', $this->host ) ) {
     217                        $redirect_to_sso_login = add_query_arg( 'from', $this->host, $redirect_to_sso_login );
     218                    }
    202219
    203220                    // And actually redirect to the SSO login
     
    244261                                // Else let the theme render, or redirect if logged in
    245262                                if ( is_user_logged_in() ) {
    246                                     $this->_redirect_to_profile();
     263                                    $this->_redirect_to_source_or_profile();
    247264                                } else {
    248265                                    if ( empty( $_GET['screen'] ) ) {
     
    259276                        } else if ( is_user_logged_in() ) {
    260277                            // Otherwise, redirect to the their profile.
    261                             $this->_redirect_to_profile();
     278                            $this->_redirect_to_source_or_profile();
    262279                        }
    263280                    } elseif ( ( is_admin() && is_super_admin() ) || 0 === strpos( $_SERVER['REQUEST_URI'], '/wp-json' ) || 0 === strpos( $_SERVER['REQUEST_URI'], '/xmlrpc.php' ) ) {
     
    265282                    } elseif ( is_user_logged_in() ) {
    266283                        // Logged in catch all, before last fallback
    267                         $this->_redirect_to_profile();
     284                        $this->_redirect_to_source_or_profile();
    268285                    } else {
    269286                        // Otherwise, redirect to the login screen.
     
    365382
    366383        /**
    367          * Redirects the user to her/his (support) profile.
    368          */
    369         protected function _redirect_to_profile() {
    370             if ( ! is_user_logged_in() ) {
    371                 return;
    372             }
    373 
    374             if ( ! empty( $_GET['redirect_to'] ) ) {
    375                 $this->_safe_redirect( $this->_get_safer_redirect_to() );
     384         * Redirects the user back to where they came from (or w.org profile)
     385         */
     386        protected function _redirect_to_source_or_profile() {
     387            $redirect = $this->_get_safer_redirect_to();
     388
     389            if ( $redirect ) {
     390                $this->_safe_redirect( $this->_maybe_add_remote_login_bounce( $redirect ) );
     391            } elseif ( is_user_logged_in() ) {
     392                $this->_safe_redirect( 'https://profiles.wordpress.org/' . wp_get_current_user()->user_nicename . '/' );
    376393            } else {
    377                 $this->_safe_redirect( 'https://profiles.wordpress.org/' . wp_get_current_user()->user_nicename . '/' );
    378             }
     394                $this->_safe_redirect( 'https://wordpress.org/' );
     395            }
     396        }
     397
     398        /**
     399         * Logs in a user on the current domain on a remote-login action.
     400         */
     401        protected function _maybe_perform_remote_login() {
     402            $remote_token = wp_unslash( $_GET['sso_token'] );
     403            if ( ! is_string( $remote_token ) || 3 !== substr_count( $remote_token, '|' ) ) {
     404                wp_die( 'Invalid token.' );
     405            }
     406
     407            list( $user_id, $sso_hash, $valid_until, $remember_me ) = explode( '|', $remote_token, 4 );
     408
     409            $remote_expiration_valid = (
     410                // +/- 5s on a 5s timeout.
     411                $valid_until >= ( time() - 5 ) &&
     412                $valid_until <= ( time() + 10 )
     413            );
     414
     415            $valid_remote_hash = false;
     416            $user = get_user_by( 'id', $user_id );
     417            if ( $user ) {
     418                $valid_remote_hash = hash_equals(
     419                    $this->_generate_remote_login_hash( $user, $valid_until, $remember_me ),
     420                    $sso_hash
     421                );
     422            }
     423
     424            if ( $remote_expiration_valid && $valid_remote_hash ) {
     425                wp_set_current_user( (int) $user_id );
     426                wp_set_auth_cookie( (int) $user_id, (bool) $remember_me );
     427
     428                if ( isset( $_GET['redirect_to'] ) ) {
     429                    $this->_safe_redirect( wp_unslash( $_GET['redirect_to'] ) );
     430                } else {
     431                    $this->_safe_redirect( home_url( '/' ) );
     432                }
     433                exit;
     434            }
     435
     436            return false;
     437        }
     438
     439        public function maybe_add_remote_login_bounce_to_post_login_url( $redirect, $requested, $user ) {
     440            return $this->_maybe_add_remote_login_bounce( $redirect, $user );
     441        }
     442
     443        protected function _maybe_add_remote_login_bounce( $redirect, $user = false ) {
     444            if ( ! $user ) {
     445                $user = wp_get_current_user();
     446            }
     447
     448            // If it's on a different _supported_ host, bounce through the remote-login.
     449            $redirect_host = parse_url( $redirect, PHP_URL_HOST );
     450
     451            if ( $user && $this->_is_valid_targeted_domain( $redirect_host ) && ! preg_match( '!wordpress.org$!i', $redirect_host ) ) {
     452
     453                // Fetch auth cookie parts to find out if the user has selected 'remember me'.
     454                $auth_cookie_parts = wp_parse_auth_cookie( '', 'secure_auth' );
     455
     456                $valid_until = time() + 5; // Super short timeout.
     457                $remember_me = ! empty( $_POST['rememberme'] ) || ( $auth_cookie_parts && $auth_cookie_parts['expiration'] >= ( time() + ( 2 * DAY_IN_SECONDS ) ) );
     458
     459                $hash        = $this->_generate_remote_login_hash( $user, $valid_until, $remember_me );
     460                $sso_token   = $user->ID . '|' . $hash . '|' . $valid_until . '|' . $remember_me;
     461
     462                $redirect = add_query_arg(
     463                    array(
     464                        'action'      => 'remote-login',
     465                        'sso_token'   => urlencode( $sso_token ),
     466                        'redirect_to' => urlencode( $redirect ),
     467                    ),
     468                    'https://' . $redirect_host . '/wp-login.php' // Assume that wp-login exists and is accessible
     469                );
     470            }
     471
     472            return $redirect;
     473        }
     474
     475        /**
     476         * Generate a hash for remote-login for non-wordpress.org domains
     477         */
     478        protected function _generate_remote_login_hash( $user, $valid_until, $remember_me = false ) {
     479            // re-use the same frag that Auth cookies use to invalidate sessions.
     480            $pass_frag = substr( $user->user_pass, 8, 4 );
     481            $key       = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $valid_until );
     482            $hash      = hash_hmac( 'sha256', $user->user_login . '|' . $valid_until . '|' . (int) $remember_me, $key );
     483
     484            return $hash;
    379485        }
    380486    }
Note: See TracChangeset for help on using the changeset viewer.