Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 33 additions & 22 deletions app/Console/Commands/UpdateTwitchAuthUsers.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use Carbon\Carbon;
use Crypt;
use GuzzleHttp\Client;
use Exception;

use App\Http\Resources\Twitch\AuthToken as TwitchAuthToken;

class UpdateTwitchAuthUsers extends Command
{
Expand All @@ -26,11 +29,11 @@ class UpdateTwitchAuthUsers extends Command
protected $description = 'Updates the users table to make sure all tokens are valid, removes if invalid.';

/**
* Twitch API base URL.
* Twitch API token URL.
*
* @var string
*/
protected $baseUrl = 'https://api.twitch.tv/kraken';
protected $tokenUrl = 'https://id.twitch.tv/oauth2/token';

/**
* Create a new command instance.
Expand All @@ -49,45 +52,53 @@ public function __construct()
*/
public function handle()
{
$timeDiff = Carbon::now()->subHours(24);
$users = User::where('updated_at', '<', $timeDiff)
->get();
$timeDiff = Carbon::now();
$users = User::where('expires', '<', $timeDiff)
->get();

if ($users->isEmpty()) {
return $this->info('No authenticated Twitch users to check.');
}

$settings = [
'headers' => [
'Accept' => 'application/vnd.twitchtv.v5+json',
'Client-ID' => env('TWITCH_CLIENT_ID'),
],
'http_errors' => false,
'form_params' => [
'grant_type' => 'refresh_token',
'client_id' => env('TWITCH_CLIENT_ID', null),
'client_secret' => env('TWITCH_CLIENT_SECRET', null),
],
];

$client = new Client;

foreach ($users as $user) {
$token = Crypt::decrypt($user->access_token);
$settings['headers']['Authorization'] = 'OAuth ' . $token;
$request = $client->request('GET', $this->baseUrl, $settings);
$token = Crypt::decrypt($user->refresh_token);
$settings['form_params']['refresh_token'] = $token;

$request = $client->request('POST', $this->tokenUrl, $settings);

$body = json_decode($request->getBody(), true);

if (empty($body['token']) || $body['token']['valid'] === false) {
if (isset($body['status']) && $body['status'] === 400) {
$user->delete();

if (empty($user->twitch)) {
$this->info(sprintf('Removed user: %s', $user->id));
continue;
}

$this->info(sprintf('Removed user: %s (%s)', $user->twitch->username, $user->id));
$this->info('Deleting user because of invalid refresh token: ' . $user->id);
continue;
}

$user->updated_at = Carbon::now();
$user->save();
try {
$newToken = TwitchAuthToken::make($body)
->resolve();

$user->access_token = Crypt::encrypt($newToken['access_token']);
$user->refresh_token = Crypt::encrypt($newToken['refresh_token']);
$user->expires = $newToken['expires'];
$user->save();
$this->info('Refreshed token for ID: ' . $user->id);
}
catch (Exception $ex)
{
$this->error('Error occurred refreshing token for ID: ' . $user->id);
}
}
}
}
10 changes: 10 additions & 0 deletions app/Exceptions/TwitchApiException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Exceptions;

use Exception;

class TwitchApiException extends Exception
{
//
}
10 changes: 10 additions & 0 deletions app/Exceptions/TwitchFormatException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Exceptions;

use Exception;

class TwitchFormatException extends Exception
{
//
}
106 changes: 86 additions & 20 deletions app/Http/Controllers/TwitchAuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
use Crypt;
use Log;

use App\Repositories\TwitchApiRepository;
use GuzzleHttp\Client as HttpClient;
use App\Http\Resources\Twitch\AuthToken as TwitchAuthToken;

class TwitchAuthController extends Controller
{
use ThrottlesLogins;
Expand Down Expand Up @@ -73,6 +77,41 @@ class TwitchAuthController extends Controller
'user:read:email',
];

/**
* Helix scopes that should be included when Kraken scopes are requested.
*
* @var array
*/
private $krakenToHelixScopes = [
'channel_subscriptions' => 'channel:read:subscriptions',
'channel_check_subscription' => 'channel:read:subscriptions',
'user_read' => 'user:read:email',
];

/**
* OAuth URLs for Twitch.
*
* @var string
*/
private $authUrl = 'https://id.twitch.tv/oauth2/authorize';
private $tokenUrl = 'https://id.twitch.tv/oauth2/token';

/**
* @var GuzzleHttp\Client
*/
private $httpClient;

/**
* @var TwitchApiRepository
*/
private $api;

public function __construct(HttpClient $client, TwitchApiRepository $repository)
{
$this->httpClient = $client;
$this->api = $repository;
}

/**
* Redirect the user to the Twitch authentication page.
*
Expand All @@ -84,20 +123,6 @@ public function redirect(Request $request)
$scopes = $request->input('scopes', null);
$redirect = $request->input('redirect', 'home');

/**
* This block is a bit of a dirty hotfix, to fix an issue with users using beta.decapi.me instead of decapi.me
* Normally (according to various internet sources), changing the session cookie domain should've worked to fix this, but apparently not.
*
* Hopefully this doesn't have to linger around for months before I am able to look at this issue again.
*
* TODO: See the catch block for InvalidStateException inside callback().
*/
$requestUrl = parse_url($request->url());
$authUrl = parse_url(env('TWITCH_REDIRECT_URI', 'https://example.com/auth/twitch/callback'));
if ($authUrl['host'] !== $requestUrl['host']) {
return redirect($authUrl['scheme'] . '://' . $authUrl['host'] . '/auth/twitch?scopes=' . $scopes . '&redirect=' . $redirect);
}

if (empty($scopes)) {
return Helper::message('missing_scopes');
}
Expand All @@ -108,6 +133,18 @@ public function redirect(Request $request)
if (!in_array($scope, $this->scopes)) {
return Helper::message('invalid_scope');
}

/**
* Make sure the Helix scope is included (for a smoother transition).
*/
if (!array_key_exists($scope, $this->krakenToHelixScopes)) {
continue;
}

$helixScope = $this->krakenToHelixScopes[$scope];
if (!in_array($helixScope, $scopes)) {
$scopes[] = $helixScope;
}
}
}

Expand All @@ -119,7 +156,16 @@ public function redirect(Request $request)
session()->put('redirect', $redirect);
session()->put('scopes', implode('+', $scopes));

return Socialite::with('twitch')->scopes($scopes)->redirect();
$query = http_build_query([
'client_id' => env('TWITCH_CLIENT_ID', null),
'redirect_uri' => env('TWITCH_REDIRECT_URI', null),
'response_type' => 'code',
'scope' => implode(' ', $scopes),
'force_verify' => 'true',
]);

$url = sprintf('%s?%s', $this->authUrl, $query);
return redirect()->away($url);
}

/**
Expand All @@ -132,19 +178,33 @@ public function callback(Request $request)
{
$redirect = session()->get('redirect', 'home');
$scopes = session()->get('scopes');
$code = $request->input('code', null);

$authUrl = sprintf('%s?redirect=%s&scopes=%s', route('auth.twitch.base'), $redirect, $scopes);
$viewData = [
'authUrl' => $authUrl,
'error' => null,
];

if (empty($request->input('code', null))) {
if (empty($code)) {
$viewData['error'] = $request->input('error_description', null);
return view('auth.twitch', $viewData);
}

try {
$user = Socialite::with('twitch')->user();
$response = $this->httpClient->request('POST', $this->tokenUrl, [
'query' => [
'client_id' => env('TWITCH_CLIENT_ID', null),
'client_secret' => env('TWITCH_CLIENT_SECRET', null),
'redirect_uri' => env('TWITCH_REDIRECT_URI', null),
'grant_type' => 'authorization_code',
'code' => $code,
],
]);

$data = json_decode($response->getBody(), true);
$token = TwitchAuthToken::make($data)
->resolve();
} catch (Exception $ex) {
// TODO: Remove this once the problem is properly identified.
Log::error('Exception thrown on Twitch authentication: ' . $ex->getMessage());
Expand All @@ -153,11 +213,17 @@ public function callback(Request $request)
return view('auth.twitch', $viewData);
}

$this->api->setToken($token['access_token']);
$users = $this->api->users();
$user = $users[0];

$auth = User::firstOrCreate([
'id' => $user->id
'id' => $user['id'],
]);
$auth->access_token = Crypt::encrypt($user->token);
$auth->scopes = implode('+', $user->accessTokenResponseBody['scope']);
$auth->access_token = Crypt::encrypt($token['access_token']);
$auth->refresh_token = Crypt::encrypt($token['refresh_token']);
$auth->scopes = implode('+', $token['scope']);
$auth->expires = $token['expires'];
$auth->save();

Auth::login($auth, true);
Expand Down
Loading