Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
25.00% covered (danger)
25.00%
5 / 20
CRAP
19.92% covered (danger)
19.92%
50 / 251
oauth
0.00% covered (danger)
0.00%
0 / 1
25.00% covered (danger)
25.00%
5 / 20
3450.28
19.92% covered (danger)
19.92%
50 / 251
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
15 / 15
 init
0.00% covered (danger)
0.00%
0 / 1
42.00
0.00% covered (danger)
0.00%
0 / 5
 login
0.00% covered (danger)
0.00%
0 / 1
210.00
0.00% covered (danger)
0.00%
0 / 63
 get_login_data
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 10
 acp
0.00% covered (danger)
0.00%
0 / 1
6.00
0.00% covered (danger)
0.00%
0 / 7
 get_acp_template
0.00% covered (danger)
0.00%
0 / 1
6.00
0.00% covered (danger)
0.00%
0 / 9
 login_link_has_necessary_data
0.00% covered (danger)
0.00%
0 / 1
42.00
0.00% covered (danger)
0.00%
0 / 6
 link_account
0.00% covered (danger)
0.00%
0 / 1
90.00
0.00% covered (danger)
0.00%
0 / 16
 logout
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 3
 get_auth_link_data
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
20 / 20
 unlink_account
0.00% covered (danger)
0.00%
0 / 1
4.01
90.91% covered (success)
90.91%
10 / 11
 link_account_login_link
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 17
 link_account_auth_link
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 16
 link_account_perform_link
0.00% covered (danger)
0.00%
0 / 1
6.00
0.00% covered (danger)
0.00%
0 / 13
 get_service
0.00% covered (danger)
0.00%
0 / 1
30.00
0.00% covered (danger)
0.00%
0 / 19
 get_service_name
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 get_provider
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 get_provider_title
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 is_set_code
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 6
 set_redirect
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 10
<?php
/**
 *
 * This file is part of the phpBB Forum Software package.
 *
 * @copyright (c) phpBB Limited <https://www.phpbb.com>
 * @license GNU General Public License, version 2 (GPL-2.0)
 *
 * For full copyright and license information, please see
 * the docs/CREDITS.txt file.
 *
 */
namespace phpbb\auth\provider\oauth;
use OAuth\Common\Http\Exception\TokenResponseException;
use OAuth\ServiceFactory;
use OAuth\Common\Consumer\Credentials;
use OAuth\Common\Service\ServiceInterface;
use OAuth\OAuth1\Service\AbstractService as OAuth1Service;
use OAuth\OAuth2\Service\AbstractService as OAuth2Service;
use phpbb\auth\provider\base;
use phpbb\auth\provider\db;
use phpbb\auth\provider\oauth\service\exception;
use phpbb\config\config;
use phpbb\db\driver\driver_interface;
use phpbb\di\service_collection;
use phpbb\event\dispatcher;
use phpbb\language\language;
use phpbb\request\request_interface;
use phpbb\user;
/**
 * OAuth authentication provider for phpBB3
 */
class oauth extends base
{
    /** @var config */
    protected $config;
    /** @var driver_interface */
    protected $db;
    /** @var db */
    protected $db_auth;
    /** @var dispatcher */
    protected $dispatcher;
    /** @var language */
    protected $language;
    /** @var request_interface */
    protected $request;
    /** @var service_collection */
    protected $service_providers;
    /** @var user */
    protected $user;
    /** @var string OAuth table: token storage */
    protected $oauth_token_table;
    /** @var string OAuth table: state */
    protected $oauth_state_table;
    /** @var string OAuth table: account association */
    protected $oauth_account_table;
    /** @var string Users table */
    protected $users_table;
    /** @var string phpBB root path */
    protected $root_path;
    /** @var string php File extension */
    protected $php_ext;
    /**
     * Constructor.
     *
     * @param config                $config                    Config object
     * @param driver_interface    $db                        Database object
     * @param db            $db_auth                DB auth provider
     * @param dispatcher            $dispatcher                Event dispatcher object
     * @param language            $language                Language object
     * @param request_interface    $request                Request object
     * @param service_collection        $service_providers        OAuth providers service collection
     * @param user                        $user                    User object
     * @param string                            $oauth_token_table        OAuth table: token storage
     * @param string                            $oauth_state_table        OAuth table: state
     * @param string                            $oauth_account_table    OAuth table: account association
     * @param string                            $users_table            User table
     * @param string                            $root_path                phpBB root path
     * @param string                            $php_ext                php File extension
     */
    public function __construct(
        config $config,
        driver_interface $db,
        db $db_auth,
        dispatcher $dispatcher,
        language $language,
        request_interface $request,
        service_collection $service_providers,
        user $user,
        $oauth_token_table,
        $oauth_state_table,
        $oauth_account_table,
        $users_table,
        $root_path,
        $php_ext
    )
    {
        $this->config                = $config;
        $this->db                    = $db;
        $this->db_auth                = $db_auth;
        $this->dispatcher            = $dispatcher;
        $this->language                = $language;
        $this->service_providers    = $service_providers;
        $this->request                = $request;
        $this->user                    = $user;
        $this->oauth_token_table    = $oauth_token_table;
        $this->oauth_state_table    = $oauth_state_table;
        $this->oauth_account_table    = $oauth_account_table;
        $this->users_table            = $users_table;
        $this->root_path            = $root_path;
        $this->php_ext                = $php_ext;
    }
    /**
     * {@inheritdoc}
     */
    public function init()
    {
        // This does not test whether or not the key and secret provided are valid.
        foreach ($this->service_providers as $service_provider)
        {
            $credentials = $service_provider->get_service_credentials();
            if (($credentials['key'] && !$credentials['secret']) || (!$credentials['key'] && $credentials['secret']))
            {
                return $this->language->lang('AUTH_PROVIDER_OAUTH_ERROR_ELEMENT_MISSING');
            }
        }
        return false;
    }
    /**
     * {@inheritdoc}
     */
    public function login($username, $password)
    {
        // Temporary workaround for only having one authentication provider available
        if (!$this->request->is_set('oauth_service'))
        {
            return $this->db_auth->login($username, $password);
        }
        // Request the name of the OAuth service
        $provider = $this->request->variable('oauth_service', '', false);
        $service_name = $this->get_service_name($provider);
        if ($provider === '' || !$this->service_providers->offsetExists($service_name))
        {
            return [
                'status'        => LOGIN_ERROR_EXTERNAL_AUTH,
                'error_msg'        => 'LOGIN_ERROR_OAUTH_SERVICE_DOES_NOT_EXIST',
                'user_row'        => ['user_id' => ANONYMOUS],
            ];
        }
        // Get the service credentials for the given service
        $storage = new token_storage($this->db, $this->user, $this->oauth_token_table, $this->oauth_state_table);
        $query = 'mode=login&login=external&oauth_service=' . $provider;
        try
        {
            /** @var OAuth1Service|OAuth2Service $service */
            $service = $this->get_service($provider, $storage, $query);
        }
        catch (\Exception $e)
        {
            return [
                'status'        => LOGIN_ERROR_EXTERNAL_AUTH,
                'error_msg'        => $e->getMessage(),
                'user_row'        => ['user_id' => ANONYMOUS],
            ];
        }
        if ($this->is_set_code($service))
        {
            $this->service_providers[$service_name]->set_external_service_provider($service);
            try
            {
                $unique_id = $this->service_providers[$service_name]->perform_auth_login();
            }
            catch (exception $e)
            {
                return [
                    'status'        => LOGIN_ERROR_EXTERNAL_AUTH,
                    'error_msg'        => $e->getMessage(),
                    'user_row'        => ['user_id' => ANONYMOUS],
                ];
            }
            /**
             * Check to see if this provider is already associated with an account.
             *
             * Enforcing a data type to make sure it are strings and not integers,
             * so values are quoted in the SQL WHERE statement.
             */
            $data = [
                'provider'            => (string) utf8_strtolower($provider),
                'oauth_provider_id'    => (string) $unique_id
            ];
            $sql = 'SELECT user_id 
                FROM ' . $this->oauth_account_table . '
                WHERE ' . $this->db->sql_build_array('SELECT', $data);
            $result = $this->db->sql_query($sql);
            $row = $this->db->sql_fetchrow($result);
            $this->db->sql_freeresult($result);
            $redirect_data = array(
                'auth_provider'                => 'oauth',
                'login_link_oauth_service'    => $provider,
            );
            /**
             * Event is triggered before check if provider is already associated with an account
             *
             * @event core.oauth_login_after_check_if_provider_id_has_match
             * @var array                row                User row
             * @var array                data            Provider data
             * @var    array                redirect_data    Data to be appended to the redirect url
             * @var ServiceInterface    service            OAuth service
             * @since 3.2.3-RC1
             * @changed 3.2.6-RC1                        Added redirect_data
             */
            $vars = [
                'row',
                'data',
                'redirect_data',
                'service',
            ];
            extract($this->dispatcher->trigger_event('core.oauth_login_after_check_if_provider_id_has_match', compact($vars)));
            if (!$row)
            {
                // The user does not yet exist, ask to link or create profile
                return [
                    'status'        => LOGIN_SUCCESS_LINK_PROFILE,
                    'error_msg'        => 'LOGIN_OAUTH_ACCOUNT_NOT_LINKED',
                    'user_row'        => [],
                    'redirect_data'    => $redirect_data,
                ];
            }
            // Retrieve the user's account
            $sql = 'SELECT user_id, username, user_password, user_passchg, user_email, user_ip, user_type, user_login_attempts
                FROM ' . $this->users_table . '
                WHERE user_id = ' . (int) $row['user_id'];
            $result = $this->db->sql_query($sql);
            $row = $this->db->sql_fetchrow($result);
            $this->db->sql_freeresult($result);
            if (!$row)
            {
                return [
                    'status'        => LOGIN_ERROR_EXTERNAL_AUTH,
                    'error_msg'        => 'AUTH_PROVIDER_OAUTH_ERROR_INVALID_ENTRY',
                    'user_row'        => ['user_id' => ANONYMOUS],
                ];
            }
            /**
             * Check if the user is banned.
             * The fourth parameter (return) has to be true, otherwise the OAuth login is still called and
             * an uncaught exception is thrown as there is no token stored in the database.
             */
            $ban = $this->user->check_ban($row['user_id'], $row['user_ip'], $row['user_email'], true);
            if (!empty($ban))
            {
                $till_date = !empty($ban['ban_end']) ? $this->user->format_date($ban['ban_end']) : '';
                $message = !empty($ban['ban_end']) ? 'BOARD_BAN_TIME' : 'BOARD_BAN_PERM';
                $contact_link = phpbb_get_board_contact_link($this->config, $this->root_path, $this->php_ext);
                $message = $this->language->lang($message, $till_date, '<a href="' . $contact_link . '">', '</a>');
                $message .= !empty($ban['ban_give_reason']) ? '<br /><br />' . $this->language->lang('BOARD_BAN_REASON', $ban['ban_give_reason']) : '';
                $message .= !empty($ban['ban_triggered_by']) ? '<br /><br /><em>' . $this->language->lang('BAN_TRIGGERED_BY_' . utf8_strtoupper($ban['ban_triggered_by'])) . '</em>' : '';
                return [
                    'status'    => LOGIN_BREAK,
                    'error_msg'    => $message,
                    'user_row'    => $row,
                ];
            }
            // Update token storage to store the user_id
            $storage->set_user_id($row['user_id']);
            /**
             * Event is triggered after user is successfully logged in via OAuth.
             *
             * @event core.auth_oauth_login_after
             * @var array    row        User row
             * @since 3.1.11-RC1
             */
            $vars = [
                'row',
            ];
            extract($this->dispatcher->trigger_event('core.auth_oauth_login_after', compact($vars)));
            // The user is now authenticated and can be logged in
            return [
                'status'        => LOGIN_SUCCESS,
                'error_msg'        => false,
                'user_row'        => $row,
            ];
        }
        else
        {
            return $this->set_redirect($service);
        }
    }
    /**
     * {@inheritdoc}
     */
    public function get_login_data()
    {
        $login_data = [
            'TEMPLATE_FILE'        => 'login_body_oauth.html',
            'BLOCK_VAR_NAME'    => 'oauth',
            'BLOCK_VARS'        => [],
        ];
        foreach ($this->service_providers as $service_name => $service_provider)
        {
            // Only include data if the credentials are set
            $credentials = $service_provider->get_service_credentials();
            if ($credentials['key'] && $credentials['secret'])
            {
                $provider = $this->get_provider($service_name);
                $redirect_url = generate_board_url() . '/ucp.' . $this->php_ext . '?mode=login&login=external&oauth_service=' . $provider;
                $login_data['BLOCK_VARS'][$service_name] = [
                    'REDIRECT_URL'    => redirect($redirect_url, true),
                    'SERVICE_NAME'    => $this->get_provider_title($provider),
                ];
            }
        }
        return $login_data;
    }
    /**
     * {@inheritdoc}
     */
    public function acp()
    {
        $ret = [];
        foreach ($this->service_providers as $service_name => $service_provider)
        {
            $provider = $this->get_provider($service_name);
            $provider = utf8_strtolower($provider);
            $ret[] = 'auth_oauth_' . $provider . '_key';
            $ret[] = 'auth_oauth_' . $provider . '_secret';
        }
        return $ret;
    }
    /**
     * {@inheritdoc}
     */
    public function get_acp_template($new_config)
    {
        $ret = [
            'BLOCK_VAR_NAME'    => 'oauth_services',
            'BLOCK_VARS'        => [],
            'TEMPLATE_FILE'        => 'auth_provider_oauth.html',
            'TEMPLATE_VARS'        => [],
        ];
        foreach ($this->service_providers as $service_name => $service_provider)
        {
            $provider = $this->get_provider($service_name);
            $ret['BLOCK_VARS'][$provider] = [
                'NAME'            => $provider,
                'ACTUAL_NAME'    => $this->get_provider_title($provider),
                'KEY'            => $new_config['auth_oauth_' . utf8_strtolower($provider) . '_key'],
                'SECRET'        => $new_config['auth_oauth_' . utf8_strtolower($provider) . '_secret'],
            ];
        }
        return $ret;
    }
    /**
     * {@inheritdoc}
     */
    public function login_link_has_necessary_data(array $login_link_data)
    {
        if (empty($login_link_data))
        {
            return 'LOGIN_LINK_NO_DATA_PROVIDED';
        }
        if (!array_key_exists('oauth_service', $login_link_data) || !$login_link_data['oauth_service'] ||
            !array_key_exists('link_method', $login_link_data) || !$login_link_data['link_method'])
        {
            return 'LOGIN_LINK_MISSING_DATA';
        }
        return null;
    }
    /**
     * {@inheritdoc}
     */
    public function link_account(array $link_data)
    {
        // Check for a valid link method (auth_link or login_link)
        if (!array_key_exists('link_method', $link_data) ||
            !in_array($link_data['link_method'], ['auth_link', 'login_link']))
        {
            return 'LOGIN_LINK_MISSING_DATA';
        }
        // We must have an oauth_service listed, check for it two ways
        if (!array_key_exists('oauth_service', $link_data) || !$link_data['oauth_service'])
        {
            $link_data['oauth_service'] = $this->request->variable('oauth_service', '');
            if (!$link_data['oauth_service'])
            {
                return 'LOGIN_LINK_MISSING_DATA';
            }
        }
        $service_name = $this->get_service_name($link_data['oauth_service']);
        if (!$this->service_providers->offsetExists($service_name))
        {
            return 'LOGIN_ERROR_OAUTH_SERVICE_DOES_NOT_EXIST';
        }
        switch ($link_data['link_method'])
        {
            case 'auth_link':
                return $this->link_account_auth_link($link_data, $service_name);
            case 'login_link':
                return $this->link_account_login_link($link_data, $service_name);
            default:
                return 'LOGIN_LINK_MISSING_DATA';
        }
    }
    /**
     * {@inheritdoc}
     */
    public function logout($data, $new_session)
    {
        // Clear all tokens belonging to the user
        $storage = new token_storage($this->db, $this->user, $this->oauth_token_table, $this->oauth_state_table);
        $storage->clearAllTokens();
        return;
    }
    /**
     * {@inheritdoc}
     */
    public function get_auth_link_data($user_id = 0)
    {
        $user_ids    = [];
        $block_vars    = [];
        $sql = 'SELECT oauth_provider_id, provider
             FROM ' . $this->oauth_account_table . '
            WHERE user_id = ' . ($user_id > 0 ? (int) $user_id : (int) $this->user->data['user_id']);
        $result = $this->db->sql_query($sql);
        while ($row = $this->db->sql_fetchrow($result))
        {
            $user_ids[$row['provider']] = $row['oauth_provider_id'];
        }
        $this->db->sql_freeresult($result);
        foreach ($this->service_providers as $service_name => $service_provider)
        {
            // Only include data if the credentials are set
            $credentials = $service_provider->get_service_credentials();
            if ($credentials['key'] && $credentials['secret'])
            {
                $provider = $this->get_provider($service_name);
                $block_vars[$service_name] = [
                    'SERVICE_NAME'    => $this->get_provider_title($provider),
                    'UNIQUE_ID'        => isset($user_ids[$provider]) ? $user_ids[$provider] : null,
                    'HIDDEN_FIELDS'    => [
                        'link'            => !isset($user_ids[$provider]),
                        'oauth_service' => $provider,
                    ],
                ];
            }
        }
        return [
            'BLOCK_VAR_NAME'    => 'oauth',
            'BLOCK_VARS'        => $block_vars,
            'TEMPLATE_FILE'        => 'ucp_auth_link_oauth.html',
        ];
    }
    /**
     * {@inheritdoc}
     */
    public function unlink_account(array $link_data)
    {
        if (!array_key_exists('oauth_service', $link_data) || !$link_data['oauth_service'])
        {
            return 'LOGIN_LINK_MISSING_DATA';
        }
        // Remove user specified in $link_data if possible
        $user_id = isset($link_data['user_id']) ? $link_data['user_id'] : $this->user->data['user_id'];
        // Remove the link
        $sql = 'DELETE FROM ' . $this->oauth_account_table . "
            WHERE provider = '" . $this->db->sql_escape($link_data['oauth_service']) . "'
                AND user_id = " . (int) $user_id;
        $this->db->sql_query($sql);
        $service_name = $this->get_service_name($link_data['oauth_service']);
        // Clear all tokens belonging to the user on this service
        $storage = new token_storage($this->db, $this->user, $this->oauth_token_table, $this->oauth_state_table);
        $storage->clearToken($service_name);
        return false;
    }
    /**
     * Performs the account linking for login_link.
     *
     * @param array        $link_data        The same variable given to
     *                                     {@see \phpbb\auth\provider\provider_interface::link_account}
     * @param string    $service_name    The name of the service being used in linking.
     * @return string|false                Returns a language key (string) if an error is encountered,
     *                                     or false on success.
     */
    protected function link_account_login_link(array $link_data, $service_name)
    {
        $storage = new token_storage($this->db, $this->user, $this->oauth_token_table, $this->oauth_state_table);
        // Check for an access token, they should have one
        if (!$storage->has_access_token_by_session($service_name))
        {
            return 'LOGIN_LINK_ERROR_OAUTH_NO_ACCESS_TOKEN';
        }
        // Prepare for an authentication request
        $query = 'mode=login_link&login_link_oauth_service=' . $link_data['oauth_service'];
        try
        {
            $service = $this->get_service($link_data['oauth_service'], $storage, $query);
        }
        catch (\Exception $e)
        {
            return $e->getMessage();
        }
        $this->service_providers[$service_name]->set_external_service_provider($service);
        try
        {
            // The user has already authenticated successfully, request to authenticate again
            $unique_id = $this->service_providers[$service_name]->perform_token_auth();
        }
        catch (exception $e)
        {
            return $e->getMessage();
        }
        // Insert into table, they will be able to log in after this
        $data = [
            'user_id'            => $link_data['user_id'],
            'provider'            => utf8_strtolower($link_data['oauth_service']),
            'oauth_provider_id'    => $unique_id,
        ];
        $this->link_account_perform_link($data);
        // Update token storage to store the user_id
        $storage->set_user_id($link_data['user_id']);
        return false;
    }
    /**
     * Performs the account linking for auth_link.
     *
     * @param array        $link_data        The same variable given to
     *                                     {@see \phpbb\auth\provider\provider_interface::link_account}
     * @param string    $service_name    The name of the service being used in linking.
     * @return string|false                Returns a language constant (string) if an error is encountered,
     *                                     or false on success.
     */
    protected function link_account_auth_link(array $link_data, $service_name)
    {
        $storage = new token_storage($this->db, $this->user, $this->oauth_token_table, $this->oauth_state_table);
        $query = 'i=ucp_auth_link&mode=auth_link&link=1&oauth_service=' . $link_data['oauth_service'];
        try
        {
            /** @var OAuth1Service|OAuth2Service $service */
            $service = $this->get_service($link_data['oauth_service'], $storage, $query);
        }
        catch (\Exception $e)
        {
            return $e->getMessage();
        }
        if ($this->is_set_code($service))
        {
            $this->service_providers[$service_name]->set_external_service_provider($service);
            try
            {
                $unique_id = $this->service_providers[$service_name]->perform_auth_login();
            }
            catch (exception $e)
            {
                return $e->getMessage();
            }
            // Insert into table, they will be able to log in after this
            $data = [
                'user_id'            => $this->user->data['user_id'],
                'provider'            => utf8_strtolower($link_data['oauth_service']),
                'oauth_provider_id'    => $unique_id,
            ];
            $this->link_account_perform_link($data);
            return false;
        }
        else
        {
            return $this->set_redirect($service);
        }
    }
    /**
     * Performs the query that inserts an account link
     *
     * @param    array    $data    This array is passed to db->sql_build_array
     */
    protected function link_account_perform_link(array $data)
    {
        // Check if the external account is already associated with other user
        $sql = 'SELECT user_id
            FROM ' . $this->oauth_account_table . "
            WHERE provider = '" . $this->db->sql_escape($data['provider']) . "'
                AND oauth_provider_id = '" . $this->db->sql_escape($data['oauth_provider_id']) . "'";
        $result = $this->db->sql_query($sql);
        $row = $this->db->sql_fetchrow($result);
        $this->db->sql_freeresult($result);
        if ($row)
        {
            trigger_error('AUTH_PROVIDER_OAUTH_ERROR_ALREADY_LINKED');
        }
        // Link account
        $sql = 'INSERT INTO ' . $this->oauth_account_table . ' ' . $this->db->sql_build_array('INSERT', $data);
        $this->db->sql_query($sql);
        /**
         * Event is triggered after user links account.
         *
         * @event core.auth_oauth_link_after
         * @var array    data    User row
         * @since 3.1.11-RC1
         */
        $vars = [
            'data',
        ];
        extract($this->dispatcher->trigger_event('core.auth_oauth_link_after', compact($vars)));
    }
    /**
     * Returns a new service object.
     *
     * @param string            $provider        The name of the provider
     * @param token_storage        $storage        Token storage object
     * @param string            $query            The query string used for the redirect uri
     * @return ServiceInterface
     * @throws exception                        When OAuth service was not created
     */
    protected function get_service($provider, token_storage $storage, $query)
    {
        $service_name = $this->get_service_name($provider);
        /** @see \phpbb\auth\provider\oauth\service\service_interface::get_service_credentials */
        $service_credentials = $this->service_providers[$service_name]->get_service_credentials();
        /** @see \phpbb\auth\provider\oauth\service\service_interface::get_auth_scope */
        $scopes = $this->service_providers[$service_name]->get_auth_scope();
        $callback = generate_board_url() . "/ucp.{$this->php_ext}?{$query}";
        // Setup the credentials for the requests
        $credentials = new Credentials(
            $service_credentials['key'],
            $service_credentials['secret'],
            $callback
        );
        $service_factory = new ServiceFactory;
        // Allow providers to register a custom class or override the provider name
        if ($class = $this->service_providers[$service_name]->get_external_service_class())
        {
            if (class_exists($class))
            {
                try
                {
                    $service_factory->registerService($provider, $class);
                }
                catch (\OAuth\Common\Exception\Exception $e)
                {
                    throw new exception('AUTH_PROVIDER_OAUTH_ERROR_INVALID_SERVICE_TYPE');
                }
            }
            else
            {
                $provider = $class;
            }
        }
        $service = $service_factory->createService($provider, $credentials, $storage, $scopes);
        if (!$service)
        {
            throw new exception('AUTH_PROVIDER_OAUTH_ERROR_SERVICE_NOT_CREATED');
        }
        return $service;
    }
    /**
     * Returns the service name for an OAuth provider name.
     *
     * @param string    $provider        The OAuth provider name
     * @return string                    The service name
     */
    protected function get_service_name($provider)
    {
        if (strpos($provider, 'auth.provider.oauth.service.') !== 0)
        {
            $provider = 'auth.provider.oauth.service.' . utf8_strtolower($provider);
        }
        return $provider;
    }
    /**
     * Returns the OAuth provider name from a service name.
     *
     * @param string    $service_name    The service name
     * @return string                    The OAuth provider name
     */
    protected function get_provider($service_name)
    {
        return str_replace('auth.provider.oauth.service.', '', $service_name);
    }
    /**
     * Returns the localized title for the OAuth provider.
     *
     * @param string    $provider        The OAuth provider name
     * @return string                    The OAuth provider title
     */
    protected function get_provider_title($provider)
    {
        return $this->language->lang('AUTH_PROVIDER_OAUTH_SERVICE_' . utf8_strtoupper($provider));
    }
    /**
     * Returns whether or not the authorization code is set.
     *
     * @param OAuth1Service|OAuth2Service    $service    The external OAuth service
     * @return bool                                        Whether or not the authorization code is set in the URL
     *                                                   for the respective OAuth service's version
     */
    protected function is_set_code($service)
    {
        switch ($service::OAUTH_VERSION)
        {
            case 1:
                return $this->request->is_set('oauth_token', request_interface::GET);
            case 2:
                return $this->request->is_set('code', request_interface::GET);
            default:
                return false;
        }
    }
    /**
     * Sets a redirect to the authorization uri.
     *
     * @param OAuth1Service|OAuth2Service $service        The external OAuth service
     * @return array|false                                Array if an error occurred,
     *                                                    false on success
     */
    protected function set_redirect($service)
    {
        $parameters = [];
        if ($service::OAUTH_VERSION === 1)
        {
            try
            {
                $token        = $service->requestRequestToken();
                $parameters    = ['oauth_token' => $token->getRequestToken()];
            }
            catch (TokenResponseException $e)
            {
                return [
                    'status'        => LOGIN_ERROR_EXTERNAL_AUTH,
                    'error_msg'        => $e->getMessage(),
                    'user_row'        => ['user_id' => ANONYMOUS],
                ];
            }
        }
        redirect($service->getAuthorizationUri($parameters), false, true);
        return false;
    }
}