Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.03% covered (danger)
18.03%
55 / 305
25.00% covered (danger)
25.00%
5 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
oauth
18.03% covered (danger)
18.03%
55 / 305
25.00% covered (danger)
25.00%
5 / 20
3876.82
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 init
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
42
 login
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 1
210
 get_login_data
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 acp
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 get_acp_template
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 login_link_has_necessary_data
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
42
 link_account
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
110
 logout
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_auth_link_data
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
7
 unlink_account
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 link_account_login_link
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 link_account_auth_link
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 link_account_perform_link
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 get_service
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 get_service_name
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_provider
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_provider_title
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_set_code
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 set_redirect
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14namespace phpbb\auth\provider\oauth;
15
16use OAuth\Common\Http\Exception\TokenResponseException;
17use OAuth\ServiceFactory;
18use OAuth\Common\Consumer\Credentials;
19use OAuth\Common\Service\ServiceInterface;
20use OAuth\OAuth1\Service\AbstractService as OAuth1Service;
21use OAuth\OAuth2\Service\AbstractService as OAuth2Service;
22use phpbb\auth\provider\base;
23use phpbb\auth\provider\db;
24use phpbb\auth\provider\oauth\service\exception;
25use phpbb\config\config;
26use phpbb\db\driver\driver_interface;
27use phpbb\di\service_collection;
28use phpbb\event\dispatcher;
29use phpbb\language\language;
30use phpbb\request\request_interface;
31use phpbb\user;
32
33/**
34 * OAuth authentication provider for phpBB3
35 */
36class oauth extends base
37{
38    /** @var config */
39    protected $config;
40
41    /** @var driver_interface */
42    protected $db;
43
44    /** @var db */
45    protected $db_auth;
46
47    /** @var dispatcher */
48    protected $dispatcher;
49
50    /** @var language */
51    protected $language;
52
53    /** @var request_interface */
54    protected $request;
55
56    /** @var service_collection */
57    protected $service_providers;
58
59    /** @var user */
60    protected $user;
61
62    /** @var string OAuth table: token storage */
63    protected $oauth_token_table;
64
65    /** @var string OAuth table: state */
66    protected $oauth_state_table;
67
68    /** @var string OAuth table: account association */
69    protected $oauth_account_table;
70
71    /** @var string Users table */
72    protected $users_table;
73
74    /** @var string phpBB root path */
75    protected $root_path;
76
77    /** @var string php File extension */
78    protected $php_ext;
79
80    /**
81     * Constructor.
82     *
83     * @param config                $config                    Config object
84     * @param driver_interface    $db                        Database object
85     * @param db            $db_auth                DB auth provider
86     * @param dispatcher            $dispatcher                Event dispatcher object
87     * @param language            $language                Language object
88     * @param request_interface    $request                Request object
89     * @param service_collection        $service_providers        OAuth providers service collection
90     * @param user                        $user                    User object
91     * @param string                            $oauth_token_table        OAuth table: token storage
92     * @param string                            $oauth_state_table        OAuth table: state
93     * @param string                            $oauth_account_table    OAuth table: account association
94     * @param string                            $users_table            User table
95     * @param string                            $root_path                phpBB root path
96     * @param string                            $php_ext                php File extension
97     */
98    public function __construct(
99        config $config,
100        driver_interface $db,
101        db $db_auth,
102        dispatcher $dispatcher,
103        language $language,
104        request_interface $request,
105        service_collection $service_providers,
106        user $user,
107        $oauth_token_table,
108        $oauth_state_table,
109        $oauth_account_table,
110        $users_table,
111        $root_path,
112        $php_ext
113    )
114    {
115        $this->config                = $config;
116        $this->db                    = $db;
117        $this->db_auth                = $db_auth;
118        $this->dispatcher            = $dispatcher;
119        $this->language                = $language;
120        $this->service_providers    = $service_providers;
121        $this->request                = $request;
122        $this->user                    = $user;
123
124        $this->oauth_token_table    = $oauth_token_table;
125        $this->oauth_state_table    = $oauth_state_table;
126        $this->oauth_account_table    = $oauth_account_table;
127        $this->users_table            = $users_table;
128        $this->root_path            = $root_path;
129        $this->php_ext                = $php_ext;
130    }
131
132    /**
133     * {@inheritdoc}
134     */
135    public function init()
136    {
137        // This does not test whether or not the key and secret provided are valid.
138        foreach ($this->service_providers as $service_provider)
139        {
140            $credentials = $service_provider->get_service_credentials();
141
142            if (($credentials['key'] && !$credentials['secret']) || (!$credentials['key'] && $credentials['secret']))
143            {
144                return $this->language->lang('AUTH_PROVIDER_OAUTH_ERROR_ELEMENT_MISSING');
145            }
146        }
147
148        return false;
149    }
150
151    /**
152     * {@inheritdoc}
153     */
154    public function login($username, $password)
155    {
156        // Temporary workaround for only having one authentication provider available
157        if (!$this->request->is_set('oauth_service'))
158        {
159            return $this->db_auth->login($username, $password);
160        }
161
162        // Request the name of the OAuth service
163        $provider = $this->request->variable('oauth_service', '', false);
164        $service_name = $this->get_service_name($provider);
165
166        if ($provider === '' || !$this->service_providers->offsetExists($service_name))
167        {
168            return [
169                'status'        => LOGIN_ERROR_EXTERNAL_AUTH,
170                'error_msg'        => 'LOGIN_ERROR_OAUTH_SERVICE_DOES_NOT_EXIST',
171                'user_row'        => ['user_id' => ANONYMOUS],
172            ];
173        }
174
175        // Get the service credentials for the given service
176        $storage = new token_storage($this->db, $this->user, $this->oauth_token_table, $this->oauth_state_table);
177        $query = 'mode=login&login=external&oauth_service=' . $provider;
178
179        try
180        {
181            /** @var OAuth1Service|OAuth2Service $service */
182            $service = $this->get_service($provider, $storage, $query);
183        }
184        catch (\Exception $e)
185        {
186            return [
187                'status'        => LOGIN_ERROR_EXTERNAL_AUTH,
188                'error_msg'        => $e->getMessage(),
189                'user_row'        => ['user_id' => ANONYMOUS],
190            ];
191        }
192
193        if ($this->is_set_code($service))
194        {
195            $this->service_providers[$service_name]->set_external_service_provider($service);
196
197            try
198            {
199                $unique_id = $this->service_providers[$service_name]->perform_auth_login();
200            }
201            catch (exception $e)
202            {
203                return [
204                    'status'        => LOGIN_ERROR_EXTERNAL_AUTH,
205                    'error_msg'        => $e->getMessage(),
206                    'user_row'        => ['user_id' => ANONYMOUS],
207                ];
208            }
209
210            /**
211             * Check to see if this provider is already associated with an account.
212             *
213             * Enforcing a data type to make sure it are strings and not integers,
214             * so values are quoted in the SQL WHERE statement.
215             */
216            $data = [
217                'provider'            => (string) utf8_strtolower($provider),
218                'oauth_provider_id'    => (string) $unique_id
219            ];
220
221            $sql = 'SELECT user_id
222                FROM ' . $this->oauth_account_table . '
223                WHERE ' . $this->db->sql_build_array('SELECT', $data);
224            $result = $this->db->sql_query($sql);
225            $row = $this->db->sql_fetchrow($result);
226            $this->db->sql_freeresult($result);
227
228            $redirect_data = array(
229                'auth_provider'                => 'oauth',
230                'login_link_oauth_service'    => $provider,
231            );
232
233            /**
234             * Event is triggered before check if provider is already associated with an account
235             *
236             * @event core.oauth_login_after_check_if_provider_id_has_match
237             * @var array                row                User row
238             * @var array                data            Provider data
239             * @var    array                redirect_data    Data to be appended to the redirect url
240             * @var ServiceInterface    service            OAuth service
241             * @since 3.2.3-RC1
242             * @changed 3.2.6-RC1                        Added redirect_data
243             * @psalm-var string[] $vars
244             */
245            $vars = [
246                'row',
247                'data',
248                'redirect_data',
249                'service',
250            ];
251            extract($this->dispatcher->trigger_event('core.oauth_login_after_check_if_provider_id_has_match', compact($vars)));
252
253            if (!$row)
254            {
255                // The user does not yet exist, ask to link or create profile
256                return [
257                    'status'        => LOGIN_SUCCESS_LINK_PROFILE,
258                    'error_msg'        => 'LOGIN_OAUTH_ACCOUNT_NOT_LINKED',
259                    'user_row'        => [],
260                    'redirect_data'    => $redirect_data,
261                ];
262            }
263
264            // Retrieve the user's account
265            $sql = 'SELECT user_id, username, user_password, user_passchg, user_email, user_ip, user_type, user_login_attempts
266                FROM ' . $this->users_table . '
267                WHERE user_id = ' . (int) $row['user_id'];
268            $result = $this->db->sql_query($sql);
269            $row = $this->db->sql_fetchrow($result);
270            $this->db->sql_freeresult($result);
271
272            if (!$row)
273            {
274                return [
275                    'status'        => LOGIN_ERROR_EXTERNAL_AUTH,
276                    'error_msg'        => 'AUTH_PROVIDER_OAUTH_ERROR_INVALID_ENTRY',
277                    'user_row'        => ['user_id' => ANONYMOUS],
278                ];
279            }
280
281            /**
282             * Check if the user is banned.
283             * The fourth parameter (return) has to be true, otherwise the OAuth login is still called and
284             * an uncaught exception is thrown as there is no token stored in the database.
285             */
286            $ban = $this->user->check_ban($row['user_id'], $row['user_ip'], $row['user_email'], true);
287
288            if (!empty($ban))
289            {
290                $till_date = !empty($ban['ban_end']) ? $this->user->format_date($ban['ban_end']) : '';
291                $message = !empty($ban['ban_end']) ? 'BOARD_BAN_TIME' : 'BOARD_BAN_PERM';
292
293                $contact_link = phpbb_get_board_contact_link($this->config, $this->root_path, $this->php_ext);
294
295                $message = $this->language->lang($message, $till_date, '<a href="' . $contact_link . '">', '</a>');
296                $message .= !empty($ban['ban_give_reason']) ? '<br /><br />' . $this->language->lang('BOARD_BAN_REASON', $ban['ban_give_reason']) : '';
297                $message .= !empty($ban['ban_triggered_by']) ? '<br /><br /><em>' . $this->language->lang('BAN_TRIGGERED_BY_' . utf8_strtoupper($ban['ban_triggered_by'])) . '</em>' : '';
298
299                return [
300                    'status'    => LOGIN_BREAK,
301                    'error_msg'    => $message,
302                    'user_row'    => $row,
303                ];
304            }
305
306            // Update token storage to store the user_id
307            $storage->set_user_id($row['user_id']);
308
309            /**
310             * Event is triggered after user is successfully logged in via OAuth.
311             *
312             * @event core.auth_oauth_login_after
313             * @var array    row        User row
314             * @since 3.1.11-RC1
315             */
316            $vars = [
317                'row',
318            ];
319            extract($this->dispatcher->trigger_event('core.auth_oauth_login_after', compact($vars)));
320
321            // The user is now authenticated and can be logged in
322            return [
323                'status'        => LOGIN_SUCCESS,
324                'error_msg'        => false,
325                'user_row'        => $row,
326            ];
327        }
328        else
329        {
330            return $this->set_redirect($service);
331        }
332    }
333
334    /**
335     * {@inheritdoc}
336     */
337    public function get_login_data()
338    {
339        $login_data = [
340            'TEMPLATE_FILE'        => 'login_body_oauth.html',
341            'BLOCK_VAR_NAME'    => 'oauth',
342            'BLOCK_VARS'        => [],
343        ];
344
345        foreach ($this->service_providers as $service_name => $service_provider)
346        {
347            // Only include data if the credentials are set
348            $credentials = $service_provider->get_service_credentials();
349
350            if ($credentials['key'] && $credentials['secret'])
351            {
352                $provider = $this->get_provider($service_name);
353                $redirect_url = generate_board_url() . '/ucp.' . $this->php_ext . '?mode=login&login=external&oauth_service=' . $provider;
354
355                $login_data['BLOCK_VARS'][$service_name] = [
356                    'REDIRECT_URL'    => redirect($redirect_url, true),
357                    'SERVICE_NAME'    => $this->get_provider_title($provider),
358                ];
359            }
360        }
361
362        return $login_data;
363    }
364
365    /**
366     * {@inheritdoc}
367     */
368    public function acp()
369    {
370        $ret = [];
371
372        foreach ($this->service_providers as $service_name => $service_provider)
373        {
374            $provider = $this->get_provider($service_name);
375
376            $provider = utf8_strtolower($provider);
377
378            $ret[] = 'auth_oauth_' . $provider . '_key';
379            $ret[] = 'auth_oauth_' . $provider . '_secret';
380        }
381
382        return $ret;
383    }
384
385    /**
386     * {@inheritdoc}
387     */
388    public function get_acp_template($new_config)
389    {
390        $ret = [
391            'BLOCK_VAR_NAME'    => 'oauth_services',
392            'BLOCK_VARS'        => [],
393            'TEMPLATE_FILE'        => 'auth_provider_oauth.html',
394            'TEMPLATE_VARS'        => [],
395        ];
396
397        foreach ($this->service_providers as $service_name => $service_provider)
398        {
399            $provider = $this->get_provider($service_name);
400
401            $ret['BLOCK_VARS'][$provider] = [
402                'NAME'            => $provider,
403                'ACTUAL_NAME'    => $this->get_provider_title($provider),
404                'KEY'            => $new_config['auth_oauth_' . utf8_strtolower($provider) . '_key'],
405                'SECRET'        => $new_config['auth_oauth_' . utf8_strtolower($provider) . '_secret'],
406            ];
407        }
408
409        return $ret;
410    }
411
412    /**
413     * {@inheritdoc}
414     */
415    public function login_link_has_necessary_data(array $login_link_data)
416    {
417        if (empty($login_link_data))
418        {
419            return 'LOGIN_LINK_NO_DATA_PROVIDED';
420        }
421
422        if (!array_key_exists('oauth_service', $login_link_data) || !$login_link_data['oauth_service'] ||
423            !array_key_exists('link_method', $login_link_data) || !$login_link_data['link_method'])
424        {
425            return 'LOGIN_LINK_MISSING_DATA';
426        }
427
428        return null;
429    }
430
431    /**
432     * {@inheritdoc}
433     */
434    public function link_account(array $link_data)
435    {
436        // Check for a valid link method (auth_link or login_link)
437        if (!array_key_exists('link_method', $link_data) ||
438            !in_array($link_data['link_method'], ['auth_link', 'login_link']))
439        {
440            return 'LOGIN_LINK_MISSING_DATA';
441        }
442
443        // We must have an oauth_service listed, check for it two ways
444        if (!array_key_exists('oauth_service', $link_data) || !$link_data['oauth_service'])
445        {
446            $link_data['oauth_service'] = $this->request->variable('oauth_service', '');
447
448            if (!$link_data['oauth_service'])
449            {
450                return 'LOGIN_LINK_MISSING_DATA';
451            }
452        }
453
454        $service_name = $this->get_service_name($link_data['oauth_service']);
455
456        if (!$this->service_providers->offsetExists($service_name))
457        {
458            return 'LOGIN_ERROR_OAUTH_SERVICE_DOES_NOT_EXIST';
459        }
460
461        switch ($link_data['link_method'])
462        {
463            case 'auth_link':
464                return $this->link_account_auth_link($link_data, $service_name);
465            case 'login_link':
466                return $this->link_account_login_link($link_data, $service_name);
467            default:
468                return 'LOGIN_LINK_MISSING_DATA';
469        }
470    }
471
472    /**
473     * {@inheritdoc}
474     */
475    public function logout($data, $new_session)
476    {
477        // Clear all tokens belonging to the user
478        $storage = new token_storage($this->db, $this->user, $this->oauth_token_table, $this->oauth_state_table);
479        $storage->clearAllTokens();
480    }
481
482    /**
483     * {@inheritdoc}
484     */
485    public function get_auth_link_data($user_id = 0)
486    {
487        $user_ids    = [];
488        $block_vars    = [];
489
490        $sql = 'SELECT oauth_provider_id, provider
491             FROM ' . $this->oauth_account_table . '
492            WHERE user_id = ' . ($user_id > 0 ? (int) $user_id : (int) $this->user->data['user_id']);
493        $result = $this->db->sql_query($sql);
494        while ($row = $this->db->sql_fetchrow($result))
495        {
496            $user_ids[$row['provider']] = $row['oauth_provider_id'];
497        }
498        $this->db->sql_freeresult($result);
499
500        foreach ($this->service_providers as $service_name => $service_provider)
501        {
502            // Only include data if the credentials are set
503            $credentials = $service_provider->get_service_credentials();
504
505            if ($credentials['key'] && $credentials['secret'])
506            {
507                $provider = $this->get_provider($service_name);
508
509                $block_vars[$service_name] = [
510                    'SERVICE_NAME'    => $this->get_provider_title($provider),
511                    'UNIQUE_ID'        => isset($user_ids[$provider]) ? $user_ids[$provider] : null,
512                    'HIDDEN_FIELDS'    => [
513                        'link'            => !isset($user_ids[$provider]),
514                        'oauth_service' => $provider,
515                    ],
516                ];
517            }
518        }
519
520        return [
521            'BLOCK_VAR_NAME'    => 'oauth',
522            'BLOCK_VARS'        => $block_vars,
523
524            'TEMPLATE_FILE'        => 'ucp_auth_link_oauth.html',
525        ];
526    }
527
528    /**
529     * {@inheritdoc}
530     */
531    public function unlink_account(array $link_data)
532    {
533        if (!array_key_exists('oauth_service', $link_data) || !$link_data['oauth_service'])
534        {
535            return 'LOGIN_LINK_MISSING_DATA';
536        }
537
538        // Remove user specified in $link_data if possible
539        $user_id = isset($link_data['user_id']) ? $link_data['user_id'] : $this->user->data['user_id'];
540
541        // Remove the link
542        $sql = 'DELETE FROM ' . $this->oauth_account_table . "
543            WHERE provider = '" . $this->db->sql_escape($link_data['oauth_service']) . "'
544                AND user_id = " . (int) $user_id;
545        $this->db->sql_query($sql);
546
547        $service_name = $this->get_service_name($link_data['oauth_service']);
548
549        // Clear all tokens belonging to the user on this service
550        $storage = new token_storage($this->db, $this->user, $this->oauth_token_table, $this->oauth_state_table);
551        $storage->clearToken($service_name);
552
553        return false;
554    }
555
556    /**
557     * Performs the account linking for login_link.
558     *
559     * @param array        $link_data        The same variable given to
560     *                                     {@see \phpbb\auth\provider\provider_interface::link_account}
561     * @param string    $service_name    The name of the service being used in linking.
562     * @return string|false                Returns a language key (string) if an error is encountered,
563     *                                     or false on success.
564     */
565    protected function link_account_login_link(array $link_data, $service_name)
566    {
567        $storage = new token_storage($this->db, $this->user, $this->oauth_token_table, $this->oauth_state_table);
568
569        // Check for an access token, they should have one
570        if (!$storage->has_access_token_by_session($service_name))
571        {
572            return 'LOGIN_LINK_ERROR_OAUTH_NO_ACCESS_TOKEN';
573        }
574
575        // Prepare for an authentication request
576        $query = 'mode=login_link&login_link_oauth_service=' . $link_data['oauth_service'];
577
578        try
579        {
580            $service = $this->get_service($link_data['oauth_service'], $storage, $query);
581        }
582        catch (\Exception $e)
583        {
584            return $e->getMessage();
585        }
586
587        $this->service_providers[$service_name]->set_external_service_provider($service);
588
589        try
590        {
591            // The user has already authenticated successfully, request to authenticate again
592            $unique_id = $this->service_providers[$service_name]->perform_token_auth();
593        }
594        catch (exception $e)
595        {
596            return $e->getMessage();
597        }
598
599        // Insert into table, they will be able to log in after this
600        $data = [
601            'user_id'            => $link_data['user_id'],
602            'provider'            => utf8_strtolower($link_data['oauth_service']),
603            'oauth_provider_id'    => $unique_id,
604        ];
605
606        $this->link_account_perform_link($data);
607
608        // Update token storage to store the user_id
609        $storage->set_user_id($link_data['user_id']);
610
611        return false;
612    }
613
614    /**
615     * Performs the account linking for auth_link.
616     *
617     * @param array        $link_data        The same variable given to
618     *                                     {@see \phpbb\auth\provider\provider_interface::link_account}
619     * @param string    $service_name    The name of the service being used in linking.
620     * @return string|false|never    Returns a language constant (string) if an error is encountered,
621     *                                     an array with error info or false on success.
622     */
623    protected function link_account_auth_link(array $link_data, $service_name)
624    {
625        $storage = new token_storage($this->db, $this->user, $this->oauth_token_table, $this->oauth_state_table);
626        $query = 'i=ucp_auth_link&mode=auth_link&link=1&oauth_service=' . $link_data['oauth_service'];
627
628        try
629        {
630            /** @var OAuth1Service|OAuth2Service $service */
631            $service = $this->get_service($link_data['oauth_service'], $storage, $query);
632        }
633        catch (\Exception $e)
634        {
635            return $e->getMessage();
636        }
637
638        if ($this->is_set_code($service))
639        {
640            $this->service_providers[$service_name]->set_external_service_provider($service);
641
642            try
643            {
644                $unique_id = $this->service_providers[$service_name]->perform_auth_login();
645            }
646            catch (exception $e)
647            {
648                return $e->getMessage();
649            }
650
651            // Insert into table, they will be able to log in after this
652            $data = [
653                'user_id'            => $this->user->data['user_id'],
654                'provider'            => utf8_strtolower($link_data['oauth_service']),
655                'oauth_provider_id'    => $unique_id,
656            ];
657
658            $this->link_account_perform_link($data);
659
660            return false;
661        }
662        else
663        {
664            $this->set_redirect($service);
665
666            return false; // Not reached
667        }
668    }
669
670    /**
671     * Performs the query that inserts an account link
672     *
673     * @param    array    $data    This array is passed to db->sql_build_array
674     * @return    void
675     */
676    protected function link_account_perform_link(array $data)
677    {
678        // Check if the external account is already associated with other user
679        $sql = 'SELECT user_id
680            FROM ' . $this->oauth_account_table . "
681            WHERE provider = '" . $this->db->sql_escape($data['provider']) . "'
682                AND oauth_provider_id = '" . $this->db->sql_escape($data['oauth_provider_id']) . "'";
683        $result = $this->db->sql_query($sql);
684        $row = $this->db->sql_fetchrow($result);
685        $this->db->sql_freeresult($result);
686
687        if ($row)
688        {
689            trigger_error('AUTH_PROVIDER_OAUTH_ERROR_ALREADY_LINKED');
690        }
691
692        // Link account
693        $sql = 'INSERT INTO ' . $this->oauth_account_table . ' ' . $this->db->sql_build_array('INSERT', $data);
694        $this->db->sql_query($sql);
695
696        /**
697         * Event is triggered after user links account.
698         *
699         * @event core.auth_oauth_link_after
700         * @var array    data    User row
701         * @since 3.1.11-RC1
702         */
703        $vars = [
704            'data',
705        ];
706        extract($this->dispatcher->trigger_event('core.auth_oauth_link_after', compact($vars)));
707    }
708
709    /**
710     * Returns a new service object.
711     *
712     * @param string            $provider        The name of the provider
713     * @param token_storage        $storage        Token storage object
714     * @param string            $query            The query string used for the redirect uri
715     * @return ServiceInterface
716     * @throws exception                        When OAuth service was not created
717     */
718    protected function get_service($provider, token_storage $storage, $query)
719    {
720        $service_name = $this->get_service_name($provider);
721
722        /** @see \phpbb\auth\provider\oauth\service\service_interface::get_service_credentials */
723        $service_credentials = $this->service_providers[$service_name]->get_service_credentials();
724
725        /** @see \phpbb\auth\provider\oauth\service\service_interface::get_auth_scope */
726        $scopes = $this->service_providers[$service_name]->get_auth_scope();
727
728        $callback = generate_board_url() . "/ucp.{$this->php_ext}?{$query}";
729
730        // Setup the credentials for the requests
731        $credentials = new Credentials(
732            $service_credentials['key'],
733            $service_credentials['secret'],
734            $callback
735        );
736
737        $service_factory = new ServiceFactory;
738
739        // Allow providers to register a custom class or override the provider name
740        if ($class = $this->service_providers[$service_name]->get_external_service_class())
741        {
742            if (class_exists($class))
743            {
744                try
745                {
746                    $service_factory->registerService($provider, $class);
747                }
748                catch (\OAuth\Common\Exception\Exception $e)
749                {
750                    throw new exception('AUTH_PROVIDER_OAUTH_ERROR_INVALID_SERVICE_TYPE');
751                }
752            }
753            else
754            {
755                $provider = $class;
756            }
757        }
758
759        $service = $service_factory->createService($provider, $credentials, $storage, $scopes);
760
761        if (!$service)
762        {
763            throw new exception('AUTH_PROVIDER_OAUTH_ERROR_SERVICE_NOT_CREATED');
764        }
765
766        return $service;
767    }
768
769    /**
770     * Returns the service name for an OAuth provider name.
771     *
772     * @param string    $provider        The OAuth provider name
773     * @return string                    The service name
774     */
775    protected function get_service_name($provider)
776    {
777        if (strpos($provider, 'auth.provider.oauth.service.') !== 0)
778        {
779            $provider = 'auth.provider.oauth.service.' . utf8_strtolower($provider);
780        }
781
782        return $provider;
783    }
784
785    /**
786     * Returns the OAuth provider name from a service name.
787     *
788     * @param string    $service_name    The service name
789     * @return string                    The OAuth provider name
790     */
791    protected function get_provider($service_name)
792    {
793        return str_replace('auth.provider.oauth.service.', '', $service_name);
794    }
795
796    /**
797     * Returns the localized title for the OAuth provider.
798     *
799     * @param string    $provider        The OAuth provider name
800     * @return string                    The OAuth provider title
801     */
802    protected function get_provider_title($provider)
803    {
804        return $this->language->lang('AUTH_PROVIDER_OAUTH_SERVICE_' . utf8_strtoupper($provider));
805    }
806
807    /**
808     * Returns whether or not the authorization code is set.
809     *
810     * @param OAuth1Service|OAuth2Service    $service    The external OAuth service
811     * @return bool                                        Whether or not the authorization code is set in the URL
812     *                                                   for the respective OAuth service's version
813     */
814    protected function is_set_code($service)
815    {
816        switch ($service::OAUTH_VERSION)
817        {
818            case 1:
819                return $this->request->is_set('oauth_token', request_interface::GET);
820
821            case 2:
822                return $this->request->is_set('code', request_interface::GET);
823
824            default:
825                return false;
826        }
827    }
828
829    /**
830     * Sets a redirect to the authorization uri.
831     *
832     * @param OAuth1Service|OAuth2Service $service        The external OAuth service
833     * @return array|never                                Array if an error occurred, won't return on success
834     */
835    protected function set_redirect($service)
836    {
837        $parameters = [];
838
839        if ($service::OAUTH_VERSION === 1)
840        {
841            try
842            {
843                $token        = $service->requestRequestToken();
844                $parameters    = ['oauth_token' => $token->getRequestToken()];
845            }
846            catch (TokenResponseException $e)
847            {
848                return [
849                    'status'        => LOGIN_ERROR_EXTERNAL_AUTH,
850                    'error_msg'        => $e->getMessage(),
851                    'user_row'        => ['user_id' => ANONYMOUS],
852                ];
853            }
854        }
855
856        redirect($service->getAuthorizationUri($parameters), false, true);
857
858        return []; // Never reached
859    }
860}