GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Issues (724)

app/Http/Controllers/ProfileController.php (1 issue)

1
<?php
2
/**
3
 * ProfileController.php
4
 * Copyright (c) 2019 [email protected]
5
 *
6
 * This file is part of Firefly III (https://github.com/firefly-iii).
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License as
10
 * published by the Free Software Foundation, either version 3 of the
11
 * License, or (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License
19
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20
 */
21
declare(strict_types=1);
22
23
namespace FireflyIII\Http\Controllers;
24
25
use Auth;
26
use DB;
27
use FireflyIII\Events\UserChangedEmail;
28
use FireflyIII\Exceptions\FireflyException;
29
use FireflyIII\Exceptions\ValidationException;
30
use FireflyIII\Http\Middleware\IsDemoUser;
31
use FireflyIII\Http\Middleware\IsSandStormUser;
32
use FireflyIII\Http\Requests\DeleteAccountFormRequest;
33
use FireflyIII\Http\Requests\EmailFormRequest;
34
use FireflyIII\Http\Requests\ProfileFormRequest;
35
use FireflyIII\Http\Requests\TokenFormRequest;
36
use FireflyIII\Models\Preference;
37
use FireflyIII\Repositories\User\UserRepositoryInterface;
38
use FireflyIII\Support\Http\Controllers\CreateStuff;
39
use FireflyIII\Support\Http\Controllers\RequestInformation;
40
use FireflyIII\User;
41
use Google2FA;
42
use Hash;
43
use Illuminate\Contracts\Auth\Guard;
44
use Illuminate\Contracts\View\Factory;
45
use Illuminate\Http\RedirectResponse;
46
use Illuminate\Http\Request;
47
use Illuminate\Routing\Redirector;
48
use Illuminate\Support\Collection;
49
use Illuminate\View\View;
50
use Laravel\Passport\ClientRepository;
51
use Log;
52
use PragmaRX\Recovery\Recovery;
53
54
/**
55
 * Class ProfileController.
56
 *
57
 * @method Guard guard()
58
 *
59
 */
60
class ProfileController extends Controller
61
{
62
    use RequestInformation, CreateStuff;
63
64
    /**
65
     * ProfileController constructor.
66
     *
67
     * @codeCoverageIgnore
68
     */
69
    public function __construct()
70
    {
71
        parent::__construct();
72
73
        $this->middleware(
74
            static function ($request, $next) {
75
                app('view')->share('title', (string) trans('firefly.profile'));
76
                app('view')->share('mainTitleIcon', 'fa-user');
77
78
                return $next($request);
79
            }
80
        );
81
82
        $this->middleware(IsDemoUser::class)->except(['index']);
83
        $this->middleware(IsSandStormUser::class)->except('index');
84
    }
85
86
    /**
87
     * Change your email address.
88
     *
89
     * @param Request $request
90
     *
91
     * @return Factory|RedirectResponse|Redirector|View
92
     */
93
    public function changeEmail(Request $request)
94
    {
95
        $loginProvider = config('firefly.login_provider');
96
        if ('eloquent' !== $loginProvider) {
97
            // @codeCoverageIgnoreStart
98
            $request->session()->flash('error', trans('firefly.login_provider_local_only', ['login_provider' => e($loginProvider)]));
99
100
            return redirect(route('profile.index'));
101
            // @codeCoverageIgnoreEnd
102
        }
103
104
        $title        = auth()->user()->email;
105
        $email        = auth()->user()->email;
106
        $subTitle     = (string) trans('firefly.change_your_email');
107
        $subTitleIcon = 'fa-envelope';
108
109
        return view('profile.change-email', compact('title', 'subTitle', 'subTitleIcon', 'email'));
110
    }
111
112
    /**
113
     * Change your password.
114
     *
115
     * @param Request $request
116
     *
117
     * @return Factory|RedirectResponse|Redirector|View
118
     */
119
    public function changePassword(Request $request)
120
    {
121
        $loginProvider = config('firefly.login_provider');
122
        if ('eloquent' !== $loginProvider) {
123
            // @codeCoverageIgnoreStart
124
            $request->session()->flash('error', trans('firefly.login_provider_local_only', ['login_provider' => e($loginProvider)]));
125
126
            return redirect(route('profile.index'));
127
            // @codeCoverageIgnoreEnd
128
        }
129
130
        $title        = auth()->user()->email;
131
        $subTitle     = (string) trans('firefly.change_your_password');
132
        $subTitleIcon = 'fa-key';
133
134
        return view('profile.change-password', compact('title', 'subTitle', 'subTitleIcon'));
135
    }
136
137
    /**
138
     * View that generates a 2FA code for the user.
139
     *
140
     * @return Factory|View
141
     */
142
    public function code()
143
    {
144
        $domain = $this->getDomain();
145
        $secret = null;
146
147
        // generate secret if not in session
148
        if (!session()->has('temp-mfa-secret')) {
149
            // generate secret + store + flash
150
            $secret = Google2FA::generateSecretKey();
151
            session()->put('temp-mfa-secret', $secret);
152
            session()->flash('two-factor-secret', $secret);
153
        }
154
        // re-use secret if in session
155
        if (session()->has('temp-mfa-secret')) {
156
            // get secret from session and flash
157
            $secret = session()->get('temp-mfa-secret');
158
            session()->flash('two-factor-secret', $secret);
159
        }
160
161
        // generate codes if not in session:
162
        if (!session()->has('temp-mfa-codes')) {
163
            // generate codes + store + flash:
164
            $recovery      = app(Recovery::class);
165
            $recoveryCodes = $recovery->lowercase()->setCount(8)->setBlocks(2)->setChars(6)->toArray();
166
            session()->put('temp-mfa-codes', $recoveryCodes);
167
            session()->flash('two-factor-codes', $recoveryCodes);
168
        }
169
170
        // get codes from session if there already:
171
        if (session()->has('temp-mfa-codes')) {
172
            $recoveryCodes = session()->get('temp-mfa-codes');
173
            session()->flash('two-factor-codes', $recoveryCodes);
174
        }
175
176
        $codes = implode("\r\n", $recoveryCodes);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $recoveryCodes does not seem to be defined for all execution paths leading up to this point.
Loading history...
177
178
        $image = Google2FA::getQRCodeInline($domain, auth()->user()->email, $secret);
179
180
        return view('profile.code', compact('image', 'secret', 'codes'));
181
    }
182
183
    /**
184
     * Screen to confirm email change.
185
     *
186
     * @param UserRepositoryInterface $repository
187
     * @param string                  $token
188
     *
189
     * @throws FireflyException
190
     * @return RedirectResponse|Redirector
191
     *
192
     */
193
    public function confirmEmailChange(UserRepositoryInterface $repository, string $token)
194
    {
195
        $loginProvider = config('firefly.login_provider');
196
        if ('eloquent' !== $loginProvider) {
197
            // @codeCoverageIgnoreStart
198
            throw new FireflyException('Cannot confirm email change when authentication provider is not local.');
199
            // @codeCoverageIgnoreEnd
200
        }
201
        // find preference with this token value.
202
        /** @var Collection $set */
203
        $set  = app('preferences')->findByName('email_change_confirm_token');
204
        $user = null;
205
        //Log::debug(sprintf('Found %d preferences', $set->count()));
206
        /** @var Preference $preference */
207
        foreach ($set as $preference) {
208
            if ($preference->data === $token) {
209
                //Log::debug('Found user');
210
                $user = $preference->user;
211
            }
212
        }
213
        // update user to clear blocked and blocked_code.
214
        if (null === $user) {
215
            //Log::debug('Found no user');
216
            throw new FireflyException('Invalid token.');
217
        }
218
        //Log::debug('Will unblock user.');
219
        $repository->unblockUser($user);
220
221
        // return to login.
222
        session()->flash('success', (string) trans('firefly.login_with_new_email'));
223
224
        return redirect(route('login'));
225
    }
226
227
    /**
228
     * Delete your account view.
229
     *
230
     * @param Request $request
231
     *
232
     * @return Factory|View
233
     */
234
    public function deleteAccount(Request $request)
235
    {
236
        $loginProvider = config('firefly.login_provider');
237
        if ('eloquent' !== $loginProvider) {
238
            // @codeCoverageIgnoreStart
239
            $request->session()->flash('warning', trans('firefly.delete_local_info_only', ['login_provider' => e($loginProvider)]));
240
            // @codeCoverageIgnoreEnd
241
        }
242
        $title        = auth()->user()->email;
243
        $subTitle     = (string) trans('firefly.delete_account');
244
        $subTitleIcon = 'fa-trash';
245
246
        return view('profile.delete-account', compact('title', 'subTitle', 'subTitleIcon'));
247
    }
248
249
    /**
250
     * Delete 2FA routine.
251
     *
252
     * @return RedirectResponse|Redirector
253
     */
254
    public function deleteCode()
255
    {
256
        /** @var UserRepositoryInterface $repository */
257
        $repository = app(UserRepositoryInterface::class);
258
259
        /** @var User $user */
260
        $user = auth()->user();
261
262
        $repository->setMFACode($user, null);
263
        session()->flash('success', (string) trans('firefly.pref_two_factor_auth_disabled'));
264
        session()->flash('info', (string) trans('firefly.pref_two_factor_auth_remove_it'));
265
266
        return redirect(route('profile.index'));
267
    }
268
269
    /**
270
     * Enable 2FA screen.
271
     *
272
     * @return RedirectResponse|Redirector
273
     */
274
    public function enable2FA()
275
    {
276
        /** @var User $user */
277
        $user       = auth()->user();
278
        $enabledMFA = null !== $user->mfa_secret;
279
280
        // if we don't have a valid secret yet, redirect to the code page to get one.
281
        if (!$enabledMFA) {
282
            return redirect(route('profile.code'));
283
        }
284
285
        // If FF3 already has a secret, just set the two factor auth enabled to 1,
286
        // and let the user continue with the existing secret.
287
        session()->flash('info', (string) trans('firefly.2fa_already_enabled'));
288
289
        return redirect(route('profile.index'));
290
    }
291
292
    /**
293
     * Index for profile.
294
     *
295
     * @return Factory|View
296
     */
297
    public function index()
298
    {
299
        /** @var User $user */
300
        $user          = auth()->user();
301
        $loginProvider = config('firefly.login_provider');
302
        // check if client token thing exists (default one)
303
        $count = DB::table('oauth_clients')->where('personal_access_client', 1)->whereNull('user_id')->count();
304
305
        $this->createOAuthKeys();
306
307
        if (0 === $count) {
308
            /** @var ClientRepository $repository */
309
            $repository = app(ClientRepository::class);
310
            $repository->createPersonalAccessClient(null, config('app.name') . ' Personal Access Client', 'http://localhost');
311
        }
312
        $subTitle       = $user->email;
313
        $userId         = $user->id;
314
        $enabled2FA     = null !== $user->mfa_secret;
315
        $mfaBackupCount = count(app('preferences')->get('mfa_recovery', [])->data);
316
317
        // get access token or create one.
318
        $accessToken = app('preferences')->get('access_token', null);
319
        if (null === $accessToken) {
320
            $token       = $user->generateAccessToken();
321
            $accessToken = app('preferences')->set('access_token', $token);
322
        }
323
324
        return view('profile.index', compact('subTitle', 'mfaBackupCount', 'userId', 'accessToken', 'enabled2FA', 'loginProvider'));
325
    }
326
327
    /**
328
     * @return Factory|View
329
     */
330
    public function newBackupCodes()
331
    {
332
        // generate recovery codes:
333
        $recovery      = app(Recovery::class);
334
        $recoveryCodes = $recovery->lowercase()
335
                                  ->setCount(8)     // Generate 8 codes
336
                                  ->setBlocks(2)    // Every code must have 7 blocks
337
                                  ->setChars(6)    // Each block must have 16 chars
338
                                  ->toArray();
339
        $codes         = implode("\r\n", $recoveryCodes);
340
341
        app('preferences')->set('mfa_recovery', $recoveryCodes);
342
        app('preferences')->mark();
343
344
        return view('profile.new-backup-codes', compact('codes'));
345
    }
346
347
    /**
348
     * Submit the change email form.
349
     *
350
     * @param EmailFormRequest        $request
351
     * @param UserRepositoryInterface $repository
352
     *
353
     * @return $this|RedirectResponse|Redirector
354
     */
355
    public function postChangeEmail(EmailFormRequest $request, UserRepositoryInterface $repository)
356
    {
357
        $loginProvider = config('firefly.login_provider');
358
        if ('eloquent' !== $loginProvider) {
359
            // @codeCoverageIgnoreStart
360
            $request->session()->flash('error', trans('firefly.login_provider_local_only', ['login_provider' => e($loginProvider)]));
361
362
            return redirect(route('profile.index'));
363
            // @codeCoverageIgnoreEnd
364
        }
365
366
        /** @var User $user */
367
        $user     = auth()->user();
368
        $newEmail = $request->string('email');
369
        $oldEmail = $user->email;
370
        if ($newEmail === $user->email) {
371
            session()->flash('error', (string) trans('firefly.email_not_changed'));
372
373
            return redirect(route('profile.change-email'))->withInput();
374
        }
375
        $existing = $repository->findByEmail($newEmail);
376
        if (null !== $existing) {
377
            // force user logout.
378
            Auth::guard()->logout();
379
            $request->session()->invalidate();
380
381
            session()->flash('success', (string) trans('firefly.email_changed'));
382
383
            return redirect(route('index'));
384
        }
385
386
        // now actually update user:
387
        $repository->changeEmail($user, $newEmail);
388
389
        // call event.
390
        $ipAddress = $request->ip();
391
        event(new UserChangedEmail($user, $newEmail, $oldEmail, $ipAddress));
392
393
        // force user logout.
394
        Auth::guard()->logout();
395
        $request->session()->invalidate();
396
        session()->flash('success', (string) trans('firefly.email_changed'));
397
398
        return redirect(route('index'));
399
    }
400
401
    /**
402
     * Submit change password form.
403
     *
404
     * @param ProfileFormRequest      $request
405
     * @param UserRepositoryInterface $repository
406
     *
407
     * @return RedirectResponse|Redirector
408
     */
409
    public function postChangePassword(ProfileFormRequest $request, UserRepositoryInterface $repository)
410
    {
411
        $loginProvider = config('firefly.login_provider');
412
        if ('eloquent' !== $loginProvider) {
413
            // @codeCoverageIgnoreStart
414
            $request->session()->flash('error', trans('firefly.login_provider_local_only', ['login_provider' => e($loginProvider)]));
415
416
            return redirect(route('profile.index'));
417
            // @codeCoverageIgnoreEnd
418
        }
419
420
        // the request has already validated both new passwords must be equal.
421
        $current = $request->get('current_password');
422
        $new     = $request->get('new_password');
423
        /** @var User $user */
424
        $user = auth()->user();
425
        try {
426
            $this->validatePassword($user, $current, $new);
427
        } catch (ValidationException $e) {
428
            session()->flash('error', $e->getMessage());
429
430
            return redirect(route('profile.change-password'));
431
        }
432
433
        $repository->changePassword($user, $request->get('new_password'));
434
        session()->flash('success', (string) trans('firefly.password_changed'));
435
436
        return redirect(route('profile.index'));
437
    }
438
439
    /** @noinspection PhpUnusedParameterInspection */
440
    /**
441
     * Submit 2FA for the first time.
442
     *
443
     * @param TokenFormRequest $request
444
     *
445
     * @return RedirectResponse|Redirector
446
     */
447
    public function postCode(TokenFormRequest $request)
448
    {
449
        /** @var User $user */
450
        $user = auth()->user();
451
        /** @var UserRepositoryInterface $repository */
452
        $repository = app(UserRepositoryInterface::class);
453
        /** @var string $secret */
454
        $secret = session()->get('two-factor-secret');
455
        $repository->setMFACode($user, $secret);
456
457
        session()->flash('success', (string) trans('firefly.saved_preferences'));
458
        app('preferences')->mark();
459
460
        // also save the code so replay attack is prevented.
461
        $mfaCode = $request->get('code');
462
        $this->addToMFAHistory($mfaCode);
463
464
        // save backup codes in preferences:
465
        app('preferences')->set('mfa_recovery', session()->get('temp-mfa-codes'));
466
467
        // make sure MFA is logged out.
468
        if ('testing' !== config('app.env')) {
469
            Google2FA::logout();
470
        }
471
472
        // drop all info from session:
473
        session()->forget(['temp-mfa-secret', 'two-factor-secret', 'temp-mfa-codes', 'two-factor-codes']);
474
475
        return redirect(route('profile.index'));
476
    }
477
478
    /**
479
     * Submit delete account.
480
     *
481
     * @param UserRepositoryInterface  $repository
482
     * @param DeleteAccountFormRequest $request
483
     *
484
     * @return RedirectResponse|Redirector
485
     */
486
    public function postDeleteAccount(UserRepositoryInterface $repository, DeleteAccountFormRequest $request)
487
    {
488
        if (!Hash::check($request->get('password'), auth()->user()->password)) {
489
            session()->flash('error', (string) trans('firefly.invalid_password'));
490
491
            return redirect(route('profile.delete-account'));
492
        }
493
        /** @var User $user */
494
        $user = auth()->user();
495
        Log::info(sprintf('User #%d has opted to delete their account', auth()->user()->id));
496
        // make repository delete user:
497
        auth()->logout();
498
        session()->flush();
499
        $repository->destroy($user);
500
501
        return redirect(route('index'));
502
    }
503
504
    /**
505
     * Regenerate access token.
506
     *
507
     * @return RedirectResponse|Redirector
508
     */
509
    public function regenerate()
510
    {
511
        /** @var User $user */
512
        $user  = auth()->user();
513
        $token = $user->generateAccessToken();
514
        app('preferences')->set('access_token', $token);
515
        session()->flash('success', (string) trans('firefly.token_regenerated'));
516
517
        return redirect(route('profile.index'));
518
    }
519
520
    /**
521
     * Undo change of user email address.
522
     *
523
     * @param UserRepositoryInterface $repository
524
     * @param string                  $token
525
     * @param string                  $hash
526
     *
527
     * @throws FireflyException
528
     * @return RedirectResponse|Redirector
529
     *
530
     */
531
    public function undoEmailChange(UserRepositoryInterface $repository, string $token, string $hash)
532
    {
533
        $loginProvider = config('firefly.login_provider');
534
        if ('eloquent' !== $loginProvider) {
535
            // @codeCoverageIgnoreStart
536
            throw new FireflyException('Cannot confirm email change when authentication provider is not local.');
537
            // @codeCoverageIgnoreEnd
538
        }
539
540
        // find preference with this token value.
541
        $set  = app('preferences')->findByName('email_change_undo_token');
542
        $user = null;
543
        /** @var Preference $preference */
544
        foreach ($set as $preference) {
545
            if ($preference->data === $token) {
546
                $user = $preference->user;
547
            }
548
        }
549
        if (null === $user) {
550
            throw new FireflyException('Invalid token.');
551
        }
552
553
        // found user.which email address to return to?
554
        $set = app('preferences')->beginsWith($user, 'previous_email_');
555
        /** @var string $match */
556
        $match = null;
557
        foreach ($set as $entry) {
558
            $hashed = hash('sha256', sprintf('%s%s', (string) config('app.key'), $entry->data));
559
            if ($hashed === $hash) {
560
                $match = $entry->data;
561
                break;
562
            }
563
        }
564
        if (null === $match) {
565
            throw new FireflyException('Invalid token.');
566
        }
567
        // change user back
568
        // now actually update user:
569
        $repository->changeEmail($user, $match);
570
        $repository->unblockUser($user);
571
572
        // return to login.
573
        session()->flash('success', (string) trans('firefly.login_with_old_email'));
574
575
        return redirect(route('login'));
576
    }
577
578
    /**
579
     * TODO duplicate code.
580
     *
581
     * @param string $mfaCode
582
     */
583
    private function addToMFAHistory(string $mfaCode): void
584
    {
585
        /** @var array $mfaHistory */
586
        $mfaHistory   = app('preferences')->get('mfa_history', [])->data;
587
        $entry        = [
588
            'time' => time(),
589
            'code' => $mfaCode,
590
        ];
591
        $mfaHistory[] = $entry;
592
593
        app('preferences')->set('mfa_history', $mfaHistory);
594
        $this->filterMFAHistory();
595
    }
596
597
    /**
598
     * Remove old entries from the preferences array.
599
     */
600
    private function filterMFAHistory(): void
601
    {
602
        /** @var array $mfaHistory */
603
        $mfaHistory = app('preferences')->get('mfa_history', [])->data;
604
        $newHistory = [];
605
        $now        = time();
606
        foreach ($mfaHistory as $entry) {
607
            $time = $entry['time'];
608
            $code = $entry['code'];
609
            if ($now - $time <= 300) {
610
                $newHistory[] = [
611
                    'time' => $time,
612
                    'code' => $code,
613
                ];
614
            }
615
        }
616
        app('preferences')->set('mfa_history', $newHistory);
617
    }
618
}
619