Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+129 -39
View File
@@ -12,7 +12,7 @@
namespace IPS\Login\Handler;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -29,6 +29,11 @@ abstract class _OAuth2 extends \IPS\Login\Handler
* @brief Any additional scopes to authenticate with
*/
public $additionalScopes = NULL;
/**
* @brief Does this handler support PKCE?
*/
public $pkceSupported = TRUE;
/**
* Get type
@@ -70,16 +75,30 @@ abstract class _OAuth2 extends \IPS\Login\Handler
'loggedin' => 'login_handler_show_in_ucp_loggedin',
),
) );
$nameChangesDisabled = array();
if ( $forceNameHandler = static::handlerHasForceSync( 'name', $this ) )
{
$nameChangesDisabled[] = 'force';
\IPS\Member::loggedIn()->language()->words['login_update_changes_yes_name_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'login_update_changes_yes_disabled', FALSE, array( 'sprintf' => $forceNameHandler->_title ) );
}
$return['update_name_changes'] = new \IPS\Helpers\Form\Radio( 'login_update_name_changes', isset( $this->settings['update_name_changes'] ) ? $this->settings['update_name_changes'] : 'disabled', FALSE, array( 'options' => array(
'force' => 'login_update_changes_yes',
'force' => 'login_update_changes_yes_name',
'optional' => 'login_update_changes_optional',
'disabled' => 'login_update_changes_no',
) ), NULL, NULL, NULL, 'login_update_name_changes_inc_optional' );
), 'disabled' => $nameChangesDisabled ), NULL, NULL, NULL, 'login_update_name_changes_inc_optional' );
$emailChangesDisabled = array();
if ( $forceEmailHandler = static::handlerHasForceSync( 'email', $this ) )
{
$emailChangesDisabled[] = 'force';
\IPS\Member::loggedIn()->language()->words['login_update_changes_yes_email_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'login_update_changes_yes_disabled', FALSE, array( 'sprintf' => $forceEmailHandler->_title ) );
}
$return['update_email_changes'] = new \IPS\Helpers\Form\Radio( 'login_update_email_changes', isset( $this->settings['update_email_changes'] ) ? $this->settings['update_email_changes'] : 'optional', FALSE, array( 'options' => array(
'force' => 'login_update_changes_yes',
'force' => 'login_update_changes_yes_email',
'optional' => 'login_update_changes_optional',
'disabled' => 'login_update_changes_no',
) ), NULL, NULL, NULL, 'login_update_email_changes_inc_optional' );
), 'disabled' => $emailChangesDisabled ), NULL, NULL, NULL, 'login_update_email_changes_inc_optional' );
return array_merge( $return, parent::acpForm() );
}
@@ -159,12 +178,21 @@ abstract class _OAuth2 extends \IPS\Login\Handler
/* Otherwise send them to the Authorization Endpoint */
else
{
$target = $this->authorizationEndpoint( $login )->setQueryString( array(
$data = array(
'client_id' => $this->settings['client_id'],
'response_type' => $this->grantType() === 'authorization_code' ? 'code' : 'token',
'redirect_uri' => (string) $this->redirectionEndpoint(),
'state' => $this->id . '-' . base64_encode( $login->url ) . '-' . \IPS\Session::i()->csrfKey . '-' . \IPS\Request::i()->ref,
) );
);
if ( $this->grantType() === 'authorization_code' AND $this->pkceSupported === TRUE )
{
$_SESSION['codeVerifier'] = \IPS\Login::generateRandomString( 128 );
$data['code_challenge'] = rtrim( strtr( base64_encode( pack( 'H*', hash( 'sha256', $_SESSION['codeVerifier'] ) ) ), '+/', '-_' ), '=' );
$data['code_challenge_method'] = 'S256';
}
$target = $this->authorizationEndpoint( $login )->setQueryString( $data );
if ( $scopes = $this->scopesToRequest( isset( \IPS\Request::i()->scopes ) ? explode( ',', \IPS\Request::i()->scopes ) : NULL ) )
{
@@ -184,16 +212,21 @@ abstract class _OAuth2 extends \IPS\Login\Handler
*
* @param \IPS\Login $login The login object
* @param string $usernameOrEmail The username or email address provided by the user
* @param string $password The plaintext password provided by the user
* @param object $password The plaintext password provided by the user, wrapped in an object that can be cast to a string so it doesn't show in any logs
* @return \IPS\Member
* @throws \IPS\Login\Exception
*/
public function authenticateUsernamePassword( \IPS\Login $login, $usernameOrEmail, $password )
{
if( !$usernameOrEmail )
{
throw new \IPS\Login\Exception( 'login_bad_username_or_password', \IPS\Login\Exception::NO_ACCOUNT );
}
$data = array(
'grant_type' => 'password',
'grant_type' => 'password',
'username' => $usernameOrEmail,
'password' => $password,
'password' => (string) $password,
);
if ( $scopes = $this->scopesToRequest() )
{
@@ -229,8 +262,8 @@ abstract class _OAuth2 extends \IPS\Login\Handler
/**
* Authenticate
*
* @param \IPS\Member $member The member
* @param string $password The plaintext password provided by the user
* @param \IPS\Member $member The member
* @param object $password The plaintext password provided by the user, wrapped in an object that can be cast to a string so it doesn't show in any logs
* @return bool
*/
public function authenticatePasswordForMember( \IPS\Member $member, $password )
@@ -242,7 +275,7 @@ abstract class _OAuth2 extends \IPS\Login\Handler
$response = $this->_authenticatedRequest( $this->tokenEndpoint(), array(
'grant_type' => 'password',
'username' => $member->name,
'password' => $password,
'password' => (string) $password,
) )->decodeJson();
if ( isset( $response['access_token'] ) )
{
@@ -259,7 +292,7 @@ abstract class _OAuth2 extends \IPS\Login\Handler
$response = $this->_authenticatedRequest( $this->tokenEndpoint(), array(
'grant_type' => 'password',
'username' => $member->email,
'password' => $password,
'password' => (string) $password,
) )->decodeJson();
if ( isset( $response['access_token'] ) )
{
@@ -299,12 +332,12 @@ abstract class _OAuth2 extends \IPS\Login\Handler
if ( $this->_authenticationType() === static::AUTHENTICATE_HEADER )
{
$request = $request->login( $this->settings['client_id'], $this->settings['client_secret'] );
$request = $request->login( $this->settings['client_id'], $this->clientSecret() );
}
else
{
$data['client_id'] = $this->settings['client_id'];
$data['client_secret'] = $this->settings['client_secret'];
$data['client_secret'] = $this->clientSecret();
}
return $request->post( $data );
@@ -398,13 +431,23 @@ abstract class _OAuth2 extends \IPS\Login\Handler
throw new \UnderflowException;
}
/* Otherwise, update our token... */
\IPS\Db::i()->update( 'core_login_links', array(
/* Otherwise, update our token without replacing values already set but not reset in this request... */
$update = array(
'token_access_token' => $accessToken['access_token'],
'token_expires' => isset( $accessToken['expires_in'] ) ? ( time() + intval( $accessToken['expires_in'] ) ) : NULL,
'token_refresh_token' => isset( $accessToken['refresh_token'] ) ? $accessToken['refresh_token'] : NULL,
'token_scope' => $scope ? json_encode( $scope ) : NULL,
), array( 'token_login_method=? AND token_member=?', $this->id, $oauthAccess['token_member'] ) );
'token_expires' => ( isset( $accessToken['expires_in'] ) ) ? ( time() + \intval( $accessToken['expires_in'] ) ) : NULL
);
if( isset( $accessToken['refresh_token'] ) )
{
$update['token_refresh_token'] = $accessToken['refresh_token'];
}
if( $scope )
{
$update['token_scope'] = json_encode( $scope );
}
\IPS\Db::i()->update( 'core_login_links', $update, array( 'token_login_method=? AND token_member=?', $this->id, $oauthAccess['token_member'] ) );
/* ... and return the member object */
return $member;
@@ -445,7 +488,7 @@ abstract class _OAuth2 extends \IPS\Login\Handler
'token_identifier' => $userId,
'token_linked' => 1,
'token_access_token' => $accessToken['access_token'],
'token_expires' => isset( $accessToken['expires_in'] ) ? ( time() + intval( $accessToken['expires_in'] ) ) : NULL,
'token_expires' => isset( $accessToken['expires_in'] ) ? ( time() + \intval( $accessToken['expires_in'] ) ) : NULL,
'token_refresh_token' => isset( $accessToken['refresh_token'] ) ? $accessToken['refresh_token'] : NULL,
'token_scope' => $scope ? json_encode( $scope ) : NULL,
) );
@@ -476,16 +519,29 @@ abstract class _OAuth2 extends \IPS\Login\Handler
{
if ( $exception->getCode() === \IPS\Login\Exception::MERGE_SOCIAL_ACCOUNT )
{
\IPS\Db::i()->replace( 'core_login_links', array(
'token_login_method' => $this->id,
'token_member' => $exception->member->member_id,
'token_identifier' => $userId,
'token_linked' => 0,
'token_access_token' => $accessToken['access_token'],
'token_expires' => isset( $accessToken['expires_in'] ) ? ( time() + intval( $accessToken['expires_in'] ) ) : NULL,
'token_refresh_token' => isset( $accessToken['refresh_token'] ) ? $accessToken['refresh_token'] : NULL,
'token_scope' => $scope ? json_encode( $scope ) : NULL,
) );
try
{
$identifier = \IPS\Db::i()->select( 'token_identifier', 'core_login_links', array( 'token_login_method=? AND token_member=?', $this->id, $exception->member->member_id ) )->first();
if( $identifier != $userId )
{
$exception->setCode( \IPS\Login\Exception::LOCAL_ACCOUNT_ALREADY_MERGED );
throw $exception;
}
}
catch( \UnderflowException $e )
{
\IPS\Db::i()->replace( 'core_login_links', array(
'token_login_method' => $this->id,
'token_member' => $exception->member->member_id,
'token_identifier' => $userId,
'token_linked' => 0,
'token_access_token' => $accessToken['access_token'],
'token_expires' => isset( $accessToken['expires_in'] ) ? ( time() + \intval( $accessToken['expires_in'] ) ) : NULL,
'token_refresh_token' => isset( $accessToken['refresh_token'] ) ? $accessToken['refresh_token'] : NULL,
'token_scope' => $scope ? json_encode( $scope ) : NULL,
) );
}
}
throw $exception;
@@ -507,12 +563,21 @@ abstract class _OAuth2 extends \IPS\Login\Handler
$response = array();
try
{
$data = $this->_authenticatedRequest( $this->tokenEndpoint(), array(
$post = array(
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => (string) $this->redirectionEndpoint(),
) );
);
if( $this->pkceSupported === TRUE )
{
$post['code_verifier'] = $_SESSION['codeVerifier'];
}
$data = $this->_authenticatedRequest( $this->tokenEndpoint(), $post );
$response = $data->decodeJson();
unset( $_SESSION['codeVerifier'] );
}
catch( \RuntimeException $e )
{
@@ -551,7 +616,11 @@ abstract class _OAuth2 extends \IPS\Login\Handler
if ( isset( $newAccessToken['error'] ) or !isset( $newAccessToken['access_token'] ) or ( isset( $newAccessToken['token_type'] ) and mb_strtolower( $newAccessToken['token_type'] ) !== 'bearer' ) )
{
\IPS\Log::log( print_r( $newAccessToken, TRUE ), 'oauth' );
if( !isset( $newAccessToken['error'] ) OR $newAccessToken['error'] != 'invalid_grant' )
{
\IPS\Log::log( print_r( $newAccessToken, TRUE ), 'oauth' );
}
\IPS\Db::i()->update( 'core_login_links', array( 'token_refresh_token' => NULL ), array( 'token_login_method=? AND token_member=?', $this->id, $member->member_id ) );
return $link;
}
@@ -701,7 +770,7 @@ abstract class _OAuth2 extends \IPS\Login\Handler
*/
public function userId( \IPS\Member $member )
{
if ( !( $link = $this->_link( $member ) ) )
if ( !( $link = $this->_link( $member ) ) or ( $link['token_expires'] and $link['token_expires'] < time() ) )
{
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
}
@@ -721,7 +790,7 @@ abstract class _OAuth2 extends \IPS\Login\Handler
*/
public function userProfileName( \IPS\Member $member )
{
if ( !( $link = $this->_link( $member ) ) )
if ( !( $link = $this->_link( $member ) ) or ( $link['token_expires'] and $link['token_expires'] < time() ) )
{
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
}
@@ -741,7 +810,7 @@ abstract class _OAuth2 extends \IPS\Login\Handler
*/
public function userEmail( \IPS\Member $member )
{
if ( !( $link = $this->_link( $member ) ) )
if ( !( $link = $this->_link( $member ) ) or ( $link['token_expires'] and $link['token_expires'] < time() ) )
{
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
}
@@ -765,4 +834,25 @@ abstract class _OAuth2 extends \IPS\Login\Handler
}
return parent::showInUcp( $member );
}
/**
* Has any sync options
*
* @return bool
*/
public function hasSyncOptions()
{
return TRUE;
}
/**
* Client Secret
*
* @return string | NULL
*/
public function clientSecret()
{
return isset( $this->settings['client_secret'] ) ? $this->settings['client_secret'] : NULL;
}
}