Seditio Source
Root |
./othercms/ips_4.3.4/system/Login/Handler/OAuth2/Custom.php
<?php
/**
 * @brief        Custom OAuth 2 Login Handler
 * @author        <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
 * @copyright    (c) 2001 - 2016 Invision Power Services, Inc.
 * @license        http://www.invisionpower.com/legal/standards/
 * @package        IPS Community Suite
 * @since        31 May 2017
 * @version        SVN_VERSION_NUMBER
 */

namespace IPS\Login\Handler\OAuth2;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
   
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
    exit;
}

/**
 * Custom OAuth 2 Login Handler
 */
class _Custom extends \IPS\Login\Handler\OAuth2
{
   
/**
     * @brief    Can we have multiple instances of this handler?
     */
   
public static $allowMultiple = TRUE;
   
   
/**
     * Get title
     *
     * @return    string
     */
   
public static function getTitle()
    {
        return
'login_handler_custom_oauth';
    }
   
   
/**
     * ACP Settings Form
     *
     * @return    array    List of settings to save - settings will be stored to core_login_methods.login_settings DB field
     * @code
         return array( 'savekey'    => new \IPS\Helpers\Form\[Type]( ... ), ... );
     * @endcode
     */
   
public function acpForm()
    {
       
$return = array();
       
       
$return[] = array( 'login_handler_oauth_settings', 'login_handler_custom_oauth_info' );
       
       
$return['grant_type'] = new \IPS\Helpers\Form\Radio( 'oauth_custom_grant_type', isset( $this->settings['grant_type'] ) ? $this->settings['grant_type'] : 'authorization_code', TRUE, array(
           
'options' => array(
               
'authorization_code'    => 'client_grant_type_authorization_code',
               
'implicit'                => 'client_grant_type_implicit',
               
'password'                => 'client_grant_type_password',
            ),
           
'toggles' => array(
               
'authorization_code'    => array( 'authorization_endpoint', 'authorization_endpoint_secure', 'button_color', 'button_text', 'client_secret' ),
               
'implicit'                => array( 'authorization_endpoint', 'authorization_endpoint_secure', 'button_color', 'button_text' ),
               
'password'                => array( 'client_secret', 'oauth_custom_auth_types', 'forgot_password_url' )
            )
        ) );
       
       
$accountManagementSettings = array();
       
$active = 'return';
        foreach (
parent::acpForm() as $k => $v )
        {
            if (
$v === 'account_management_settings' )
            {
               
$active = 'accountManagementSettings';
            }
            if ( !
is_string( $v ) and !is_array( $v ) )
            {
                ${
$active}[ $k ] = $v;
            }
        }
       
       
$return['authentication_type'] = new \IPS\Helpers\Form\Radio( 'oauth_custom_authentication_type', isset( $this->settings['authentication_type'] ) ? $this->settings['authentication_type'] : static::AUTHENTICATE_HEADER, TRUE, array(
           
'options' => array(
                static::
AUTHENTICATE_HEADER    => 'oauth_custom_authentication_type_header',
                static::
AUTHENTICATE_POST    => 'oauth_custom_authentication_type_post',
            )
        ) );
       
       
$return['scopes'] = new \IPS\Helpers\Form\Stack( 'oauth_scopes_to_request', isset( $this->settings['scopes'] ) ? $this->settings['scopes'] : array(), FALSE, array() );
       
       
$authorizationEndpointValidation = function( $val )
        {
            if ( \
IPS\OAUTH_REQUIRES_HTTPS and $val and $val instanceof \IPS\Http\Url )
            {
                if (
$val->data[ \IPS\Http\Url::COMPONENT_SCHEME ] !== 'https' )
                {
                    throw new \
DomainException('authorization_endpoint_https');
                }
                if (
$val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] )
                {
                    throw new \
DomainException('authorization_endpoint_fragment');
                }
            }
        };
       
       
$return['authorization_endpoint'] = new \IPS\Helpers\Form\Url( 'oauth_authorization_endpoint', isset( $this->settings['authorization_endpoint'] ) ? $this->settings['authorization_endpoint'] : NULL, NULL, array( 'placeholder' => 'https://example.com/oauth/authorize' ), $authorizationEndpointValidation, NULL, NULL, 'authorization_endpoint' );
       
$return['authorization_endpoint_secure'] = new \IPS\Helpers\Form\Url( 'oauth_authorization_endpoint_secure', isset( $this->settings['authorization_endpoint_secure'] ) ? $this->settings['authorization_endpoint_secure'] : NULL, NULL, array( 'nullLang' => 'oauth_authorization_endpoint_same', 'placeholder' => 'https://example.com/oauth/authorize/?prompt=login' ), $authorizationEndpointValidation, NULL, NULL, 'authorization_endpoint_secure' );
       
$return['token_endpoint'] = new \IPS\Helpers\Form\Url( 'oauth_token_endpoint', isset( $this->settings['token_endpoint'] ) ? $this->settings['token_endpoint'] : NULL, TRUE, array( 'placeholder' => 'https://www.example.com/oauth/token' ) );
       
$return['user_endpoint'] = new \IPS\Helpers\Form\Url( 'oauth_user_endpoint', isset( $this->settings['user_endpoint'] ) ? $this->settings['user_endpoint'] : NULL, TRUE, array( 'placeholder' => 'https://www.example.com/oauth/me' ) );
       
$return['uid_field'] = new \IPS\Helpers\Form\Text( 'oauth_custom_uid_field', isset( $this->settings['uid_field'] ) ? $this->settings['uid_field'] : NULL, TRUE, array() );
       
$return['name_field'] = new \IPS\Helpers\Form\Text( 'oauth_custom_name_field', isset( $this->settings['name_field'] ) ? $this->settings['name_field'] : NULL, FALSE, array(), NULL, NULL, NULL, 'login_real_name' );
       
$return['email_field'] = new \IPS\Helpers\Form\Text( 'oauth_custom_email_field', isset( $this->settings['email_field'] ) ? $this->settings['email_field'] : NULL, FALSE, array(), NULL, NULL, NULL, 'login_real_email' );
       
$return['photo_field'] = new \IPS\Helpers\Form\Text( 'oauth_custom_photo_field', isset( $this->settings['photo_field'] ) ? $this->settings['photo_field'] : NULL );
        if ( \
IPS\Settings::i()->allow_forgot_password == 'normal' or \IPS\Settings::i()->allow_forgot_password == 'handler' )
        {
           
$return['forgot_password_url'] = new \IPS\Helpers\Form\Url( 'handler_forgot_password_url', isset( $this->settings['forgot_password_url'] ) ? $this->settings['forgot_password_url'] : NULL, FALSE, array(), NULL, NULL, NULL, 'forgot_password_url' );
            \
IPS\Member::loggedIn()->language()->words['handler_forgot_password_url_desc'] = \IPS\Member::loggedIn()->language()->addToStack( \IPS\Settings::i()->allow_forgot_password == 'normal' ? 'handler_forgot_password_url_desc_normal' : 'handler_forgot_password_url_deschandler' );
        }
       
       
$return[] = 'login_handler_oauth_ui';
       
$return['auth_types'] = new \IPS\Helpers\Form\Select( 'oauth_custom_auth_types', isset( $this->settings['auth_types'] ) ? $this->settings['auth_types'] : ( \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL ), TRUE, array( 'options' => array(
            \
IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL => 'username_or_email',
            \
IPS\Login::AUTH_TYPE_EMAIL    => 'email_address',
            \
IPS\Login::AUTH_TYPE_USERNAME => 'username',
        ) ),
NULL, NULL, NULL, 'oauth_custom_auth_types' );
       
$return['button_color'] = new \IPS\Helpers\Form\Color( 'oauth_custom_button_color', isset( $this->settings['button_color'] ) ? $this->settings['button_color'] : '#478F79', NULL, array(), NULL, NULL, NULL, 'button_color' );        
       
$return['button_text'] = new \IPS\Helpers\Form\Translatable( 'oauth_custom_button_text',  NULL, NULL, array( 'placeholder' => \IPS\Member::loggedIn()->language()->addToStack('oauth_custom_button_text_custom_placeholder'), 'app' => 'core', 'key' => ( $this->id ? "core_custom_oauth_{$this->id}" : NULL ) ), NULL, NULL, NULL, 'button_text' );
       
$return['button_icon'] = new \IPS\Helpers\Form\Upload( 'oauth_custom_button_icon',  ( isset( $this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] ) : NULL, FALSE, array( 'storageExtension' => 'core_Login' ), NULL, NULL, NULL, 'button_icon' );
       
       
$return[] = 'account_management_settings';
        foreach (
$accountManagementSettings as $k => $v )
        {
           
$return[ $k ] = $v;
        }
       
        return
$return;
    }
   
   
/**
     * Save Handler Settings
     *
     * @param    array    $values    Values from form
     * @return    array
     */
   
public function acpFormSave( &$values )
    {
       
$return = parent::acpFormSave( $values );
       
$return['button_icon'] = (string) $return['button_icon'];
       
$return['authorization_endpoint'] = (string) $return['authorization_endpoint'];
       
$return['token_endpoint'] = (string) $return['token_endpoint'];
       
$return['user_endpoint'] = (string) $return['user_endpoint'];
        return
$return;
    }
   
   
/**
     * [Node] Format form values from add/edit form for save
     *
     * @param    array    $values    Values from the form
     * @return    array
     */
   
public function formatFormValues( $values )
    {
       
$parent = parent::formatFormValues( $values );

        if( isset(
$values['oauth_custom_button_text'] ) )
        {
            if ( !
$this->id )
            {
               
$this->save();
            }
            \
IPS\Lang::saveCustom( 'core', "core_custom_oauth_{$this->id}", $values['oauth_custom_button_text'] );
            unset(
$values['button_text'] );
        }
       
        return
$parent;
    }
   
   
/**
     * Get the button color
     *
     * @return    string
     */
   
public function buttonColor()
    {
        return
$this->settings['button_color'];
    }
   
   
/**
     * Get the button icon
     *
     * @return    string
     */
   
public function buttonIcon()
    {
        return ( isset(
$this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] ) : NULL;
    }
   
   
/**
     * Get logo to display in information about logins with this method
     * Returns NULL for methods where it is not necessary to indicate the method, e..g Standard
     *
     * @return    \IPS\Http\Url
     */
   
public function logoForDeviceInformation()
    {
        return ( isset(
$this->settings['button_icon'] ) and $this->settings['button_icon'] ) ? \IPS\File::get( 'core_Login', $this->settings['button_icon'] )->url : NULL;
    }
   
   
/**
     * Get logo to display in user cp sidebar
     *
     * @return    \IPS\Http\Url
     */
   
public function logoForUcp()
    {
        return
$this->logoForDeviceInformation();
    }
   
   
/**
     * Get button text
     *
     * @return    string
     */
   
public function buttonText()
    {
        return
"core_custom_oauth_{$this->id}";
    }
   
   
/**
     * Grant Type
     *
     * @return    string
     */
   
protected function grantType()
    {
        return isset(
$this->settings['grant_type'] ) ? $this->settings['grant_type'] : 'authorization_code';
    }
   
   
/**
     * Should client credentials be sent as an "Authoriation" header, or as POST data?
     *
     * @return    string
     */
   
protected function _authenticationType()
    {
        return isset(
$this->settings['authentication_type'] ) ? $this->settings['authentication_type'] : static::AUTHENTICATE_HEADER;
    }
   
   
/**
     * Get scopes to request
     *
     * @param    array|NULL    $additional    Any additional scopes to request
     * @return    array
     */
   
protected function scopesToRequest( $additional=NULL )
    {
        return
$this->settings['scopes'];
    }
   
   
/**
     * Authorization Endpoint
     *
     * @param    \IPS\Login    $login    The login object
     * @return    \IPS\Http\Url
     */
   
protected function authorizationEndpoint( \IPS\Login $login )
    {
        if ( isset(
$this->settings['authorization_endpoint_secure'] ) and $this->settings['authorization_endpoint_secure'] and ( $login->type === \IPS\Login::LOGIN_ACP or $login->type === \IPS\Login::LOGIN_REAUTHENTICATE ) )
        {
            return \
IPS\Http\Url::external( $this->settings['authorization_endpoint_secure'] );
        }
       
        return \
IPS\Http\Url::external( $this->settings['authorization_endpoint'] );
    }
   
   
/**
     * Token Endpoint
     *
     * @return    \IPS\Http\Url
     */
   
protected function tokenEndpoint()
    {
        return \
IPS\Http\Url::external( $this->settings['token_endpoint'] );
    }
   
   
/**
     * Get authenticated user's identifier (may not be a number)
     *
     * @param    string    $accessToken    Access Token
     * @return    string
     */
   
protected function authenticatedUserId( $accessToken )
    {        
        if (
$userId = static::getValueFromArray( $this->_userData( $accessToken, $this->settings['uid_field'] ), $this->settings['uid_field'] ) )
        {
            return
$userId;
        }
        throw new \
Exception;
    }
   
   
/**
     * Get authenticated user's username
     * May return NULL if server doesn't support this
     *
     * @param    string    $accessToken    Access Token
     * @return    string|NULL
     */
   
protected function authenticatedUserName( $accessToken )
    {
        if ( isset(
$this->settings['name_field'] ) and $this->settings['name_field'] and $username = static::getValueFromArray( $this->_userData( $accessToken, $this->settings['name_field'] ), $this->settings['name_field'] ) )
        {
            return
$username;
        }
        return
NULL;
    }
   
   
/**
     * Get authenticated user's email address
     * May return NULL if server doesn't support this
     *
     * @param    string    $accessToken    Access Token
     * @return    string|NULL
     */
   
protected function authenticatedEmail( $accessToken )
    {
        if ( isset(
$this->settings['email_field'] ) and $this->settings['email_field'] and $email = static::getValueFromArray( $this->_userData( $accessToken, $this->settings['email_field'] ), $this->settings['email_field'] ) )
        {
            return
$email;
        }
        return
NULL;
    }
   
   
/**
     * Get user's profile photo
     * May return NULL if server doesn't support this
     *
     * @param    \IPS\Member    $member    Member
     * @return    \IPS\Http\Url|NULL
     * @throws    \IPS\Login\Exception    The token is invalid and the user needs to reauthenticate
     * @throws    \DomainException        General error where it is safe to show a message to the user
     * @throws    \RuntimeException        Unexpected error from service
     */
   
public function userProfilePhoto( \IPS\Member $member )
    {
        if ( isset(
$this->settings['photo_field'] ) and $this->settings['photo_field'] )
        {
            if ( !(
$link = $this->_link( $member ) ) )
            {
                throw new \
IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
            }
                       
            if (
$photo = static::getValueFromArray( $this->_userData( $link['token_access_token'], $this->settings['photo_field'] ), $this->settings['photo_field'] ) )
            {
                return \
IPS\Http\Url::external( $photo );
            }
        }
       
        return
NULL;
    }
   
   
/**
     * Get user's profile name
     * May return NULL if server doesn't support this
     *
     * @param    \IPS\Member    $member    Member
     * @return    string|NULL
     * @throws    \IPS\Login\Exception    The token is invalid and the user needs to reauthenticate
     * @throws    \DomainException        General error where it is safe to show a message to the user
     * @throws    \RuntimeException        Unexpected error from service
     */
   
public function userProfileName( \IPS\Member $member )
    {
        if ( !(
$link = $this->_link( $member ) ) )
        {
            throw new \
IPS\Login\Exception( NULL, \IPS\Login\Exception::INTERNAL_ERROR );
        }
       
        return
$this->authenticatedUserName( $link['token_access_token'] );
    }
   
   
/**
     * Syncing Options
     *
     * @param    \IPS\Member    $member            The member we're asking for (can be used to not show certain options iof the user didn't grant those scopes)
     * @param    bool        $defaultOnly    If TRUE, only returns which options should be enabled by default for a new account
     * @return    array
     */
   
public function syncOptions( \IPS\Member $member, $defaultOnly = FALSE )
    {
       
$return = array();
       
        if ( isset(
$this->settings['email_field'] ) and $this->settings['email_field'] and ( !isset( $this->settings['update_email_changes'] ) or $this->settings['update_email_changes'] === 'optional' ) )
        {
           
$return[] = 'email';
        }
       
        if ( isset(
$this->settings['name_field'] ) and $this->settings['name_field'] and isset( $this->settings['update_name_changes'] ) and $this->settings['update_name_changes'] === 'optional' )
        {
           
$return[] = 'name';
        }
       
        if ( isset(
$this->settings['photo_field'] ) and $this->settings['photo_field'] )
        {
           
$return[] = 'photo';
        }
       
        return
$return;
    }
   
   
/**
     * @brief    Cached user data
     */
   
protected $_cachedUserData = array();
   
   
/**
     * Get user data
     *
     * @param    string    $accessToken    Access Token
     * @throws    \IPS\Login\Exception    The token is invalid and the user needs to reauthenticate
     * @throws    \RuntimeException        Unexpected error from service
     */
   
protected function _userData( $accessToken, $expectedParam = NULL )
    {
        if ( !isset(
$this->_cachedUserData[ $accessToken ] ) )
        {
           
/* Try the most sensible way first */
           
$response = \IPS\Http\Url::external( $this->settings['user_endpoint'] )->request()
                ->
setHeaders( array(
                   
'Authorization' => "Bearer {$accessToken}"
               
) )
                ->
get()
                ->
decodeJson();
           
           
/* Check if we got what we were expecting. If we didn't, try sending the access token in the query string.
                While the spec discourages this usage, it is still valid and some providers may require it */
           
if ( $expectedParam !== NULL )
            {
                if ( static::
getValueFromArray( $response, $expectedParam ) === NULL )
                {
                   
$response = \IPS\Http\Url::external( $this->settings['user_endpoint'] )->setQueryString( 'access_token', $accessToken )->request()
                        ->
get()
                        ->
decodeJson();
                }
            }

           
/* Check for any errors */
           
if ( static::getValueFromArray( $response, $this->settings['uid_field'] ) === NULL )
            {
                \
IPS\Log::log( print_r( $response, TRUE ), 'oauth_custom' );
                throw new \
IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
            }

           
/* Set */                        
           
$this->_cachedUserData[ $accessToken ] = $response;
        }
        return
$this->_cachedUserData[ $accessToken ];
    }
   
   
/**
     * Get value from an array
     *
     * @param    array    $array    The array with the data
     * @param    string    $key    The key using[square][brackets]
     * @return    mixed
     */
   
protected static function getValueFromArray( $array, $key )
    {
        while (
$pos = mb_strpos( $key, '[' ) )
        {
           
preg_match( '/^(.+?)\[([^\]]+?)?\](.*)?$/', $key, $matches );
           
            if ( !
array_key_exists( $matches[1], $array ) )
            {
                return
NULL;
            }
               
           
$array = $array[ $matches[1] ];
           
$key = $matches[2] . $matches[3];
        }
       
        if ( !isset(
$array[ $key ] ) )
        {
            return
NULL;
        }
               
        return
$array[ $key ];
    }
   
   
/**
     * Forgot Password URL
     *
     * @return    \IPS\Http\Url|NULL
     */
   
public function forgotPasswordUrl()
    {
        return ( isset(
$this->settings['forgot_password_url'] ) and $this->settings['forgot_password_url'] ) ? \IPS\Http\Url::external( $this->settings['forgot_password_url'] ) : NULL;
    }
}