Twitch login expiring really quickly

We have login via Twitch on our website which someone smarter than me was kind enough to set up May 2020, apart from one Twitch glitch several months ago it has been working without issue until today. Suddenly as soon as I try to switch to a different page or submit a form, the login has expired and the data is lost. The most recent alteration I made to the callback script was a few months ago to update the scopes, otherwise I have not touched it so I’m confused and concerned as to how I have managed to break this incredibly fundamental part of our setup!

Have there been any very recent changes to how the Oauth stuff is supposed to be set up? I have re-read the documentation again but that hasn’t helped :frowning:

I’m using next-auth for Twitch auth and had no issues recently, so it can be something in your end, if you can share more details about how you are making the requests, if using cookies to save session, refresh token… hard to help w/o details

This sounds like a problem with your session manager and not a twitch issue.

Somethings up with that most likely cookie settings or something similar

Whatever it was stopped happening about 8 or so hours after I posted but has just started up again in the the last 30 minutes. The only session data we set is the data we receive back from Twitch, i.e. the person’s Twitch ID and their display name, no cookies are set at all either for admins or users. I will have to dig into the scripts I guess and try and figure out what must be the point of failure and why it’s only happening some of the time.

Web server sessions generally need to set a cookie on the users computer so on reload it can recover the session…

So somethings odd somewhere but it’s not clear what/how you are operating to know where the issue is

I was wrong, there is a mention of session cookies in the callback script:
This is how it’s setup - I’ve removed our database access details but they are definitely correct in the actual script because the token are being stored once fetched:

<?php
// Provides $clientid and $secret (plus other configuration) for this script
require_once('secret.php');

// util functions for getting URL contents
require_once('utils.php');

$some_name = session_name("some_name");
session_set_cookie_params(0, '/', '.shamblingincompetence.com');
session_start();

/////////////////////////////////////////////////////////////////////////////
// Start of OAUTH processing to get the user token
//
// Make sure that the session state returned matches the one saved
if (isset($_SESSION['auth_state']) and isset($_GET['state']) and $_SESSION['auth_state']==$_GET['state']) {

	// Check whether verbose authentication process was enabled
	$verbose=isset($_GET['verbose']) || isset($_SESSION['verbose']);

	if($verbose) {
		echo "<pre>\nNote: Redirecting in 10 seconds to: " . $_SESSION['auth_redirect'] . "\n\n"; 
		echo "\$_GET:\n";
		var_dump($_GET);
		echo "\n \$_SESSION:\n";
		var_dump($_SESSION);
		echo "\n\n";
	}

	// Use authorization code to get the user's access token.
    $fields=array(
		'client_id' => $clientid,
		'client_secret' => $secret,
		'code' => $_GET['code'],
		'grant_type' => 'authorization_code',
		'redirect_uri' => urlencode($redirect_uri)
	);
	$result = curl_post($tokenurl, $fields);

    $response=json_decode($result);

	if($verbose) { var_dump($response); echo "\n";}
	
	if (!isset($response->access_token)) auth_error('No Access Token returned');	
        $auth_token=$response->access_token;
	$_SESSION['user_token'] = $auth_token;
        $_SESSION['client_id'] = $clientid;
	
	$date = new DateTime(NULL, new DateTimeZone( "UTC" ));
	$date->modify('+'.$response->expires_in.' seconds');
	$_SESSION['refresh_token'] = $response->refresh_token;
	$_SESSION['token_expires'] = $date->getTimeStamp();
	$_SESSION['token_expires_readable'] = $date->format('Y-m-d H:i:s T');
	
	//////////////////////////////////////////////////////////////////
	// OAUTH process completed, token now available for further usage
	//////////////////////////////////////////////////////////////////

	// Get the User ID and Display Name from API
	// https://dev.twitch.tv/docs/api/reference#get-users
	// GET https://api.twitch.tv/helix/users
        $header=array(
            'Authorization: Bearer '.$auth_token, 
            'Client-ID: '.$clientid
        );
        $result = curl_header($api_url.'users', $header);
        $response=json_decode($result);

	if($verbose) { var_dump($response); echo "\n";}

    if (!isset($response->data[0]->id)) auth_error('No User ID returned');
    if (!isset($response->data[0]->display_name)) auth_error('No User display name returned');

	// Are they the streamer?
	if($response->data[0]->id == $streamerID) {
		if($_GET['scope'] == $scopes_req) {
			// Let's re-auth with a request to access subscribers
			
			// Clear out session data
			unset($_SESSION['user_token']);
			unset($_SESSION['refresh_token']);
			unset($_SESSION['token_expires']);
			unset($_SESSION['token_expires_readable']);
			
			$state=uniqid();
			$_SESSION['auth_state']=$state;
			session_write_close();
			header(
				'Location:'.$oauthurl
				.'?response_type=code&redirect_uri='.urlencode($redirect_uri)
				.'&client_id='.$clientid.'&scope='.urlencode('channel:read:subscriptions moderator:read:followers bits:read channel:read:redemptions channel:moderate whispers:read channel:manage:redemptions channel:manage:broadcast user:read:email user:edit:broadcast clips:edit channel:manage:schedule moderator:manage:chat_settings moderator:manage:announcements moderator:read:chatters moderator:manage:shoutouts channel:read:hype_train channel:manage:vips channel:manage:moderators moderation:read').'&state='.$state
			);
			exit();
		}
	}
	
	$_SESSION['user_id']=$response->data[0]->id;
	$_SESSION['user_name']=$response->data[0]->display_name;
	
	// Prepare MYSQL connection:
	$servername = "XXXXXX";
	$username = "XXXXXX";
	$password = "XXXXXX";
	$dbname = "XXXXXX";
	$sqltblname = "TWITCH_Auth";
	
	$conn = new mysqli($servername, $username, $password, $dbname);
	if ($conn->connect_error) {
	  die("Connection failed: " . $conn->connect_error);
	}
	
	if($_SESSION['user_id'] == $streamerID) {
		// Store auth token in database
		$sql = "INSERT INTO ".$sqltblname." (token, refresh_token, expires) VALUES ('".$_SESSION['user_token']."', '".$_SESSION['refresh_token']."', '".$_SESSION['token_expires']."')";
		if($verbose) {echo $sql."\n";}
		$result = $conn->query($sql);
		if($verbose) {var_dump($result); echo "\n";}
		
		$_SESSION['subscriber'] = true;
		$_SESSION['subscriber_tier'] = 3000;
		$_SESSION['follower'] = true;
			
	} else {
		// Are they a follower?
		// https://dev.twitch.tv/docs/api/reference#get-users-follows
		// GET https://api.twitch.tv/helix/users/follows?from_id=<user ID>	
		$result = curl_header($api_url.'users/follows?from_id='.$_SESSION['user_id'].'&to_id='.$streamerID, $header);
                $response=json_decode($result);
	
		if($verbose) { var_dump($response); echo "\n"; }
	
		$_SESSION['follower'] = (isset($response->total) && ($response->total == 1));
		
		// Are they a subscriber?
		$sql = "SELECT * FROM ".$sqltblname." ORDER BY expires DESC LIMIT 1";
		if($verbose) {echo $sql."\n";}
		$result = $conn->query($sql);
		if($verbose) {var_dump($result); echo "\n";}

		$date = new DateTime(NULL, new DateTimeZone( "UTC" ));
		$subtok = "";
		$subref = "";
		$subexp = "";
		if ($result->num_rows > 0) {
			while($row = $result->fetch_assoc()) {
                            	if($verbose) {echo "Row data \n"; var_dump($row); echo "\n";}
				$subtok = $row['token'];
				$subref = $row['refresh_token'];
				$subexp = $row['expires'];
			}
		} else {
			auth_error('Streamer Auth Data not in the database');
		}
		
		if($subexp < $date->getTimeStamp()) {
			// Need to refresh token
			// Use authorization code to get the user's access token.
			$fields=array(
				'client_id' => $clientid,
				'client_secret' => $secret,
				'refresh_token' => $subref,
				'grant_type' => 'refresh_token',
				'redirect_uri' => urlencode($redirect_uri)
			);
			$result = curl_post($tokenurl, $fields);
			
			$response=json_decode($result);
			
			if($verbose) { var_dump($response); echo "\n";}
			
			if (!isset($response->access_token)) auth_error('Failed to refresh streamer token');
			$oldtok = $subtok;
			$subtok = $response->access_token;
			$subref = $response->refresh_token;
			$date->modify('+'.$response->expires_in.' seconds');
			$subexp = $date->getTimeStamp();
			
			$sql = "UPDATE ".$sqltblname." SET token = '".$subtok."', refresh_token = '".$subref."', expires = '".$subexp."' WHERE token = '".$oldtok."'";
			if($verbose) {echo $sql."\n";}
			$result = $conn->query($sql);
			if($verbose) {var_dump($result); echo "\n";}
		}
		
		// Check whether current user is subscribed to the streamer
		// https://dev.twitch.tv/docs/api/reference#get-broadcaster-subscriptions
		// GET https://api.twitch.tv/helix/subscriptions?broadcaster_id=<StreamerID>&user_id=<user ID>
		$streamerheader = array(
				'Authorization: Bearer '.$subtok, 
				'Client-ID: '.$clientid
			);
		
		$result = curl_header($api_url.'subscriptions?user_id='.$_SESSION['user_id'].'&broadcaster_id='.$streamerID, $streamerheader);
	    $response=json_decode($result);
	
		if($verbose) { var_dump($response); echo "\n"; }
	
		if(isset($response->data) && (count($response->data) == 1)) {
			$_SESSION['subscriber'] = true;
			$_SESSION['subscriber_tier'] = $response->data[0]->tier;
		} else {
			$_SESSION['subscriber'] = false;
			$_SESSION['subscriber_tier'] = 0;
		}
	}
	
	// Close session and redirect to original page	
	if (!isset($_SESSION['auth_redirect'])) auth_error('No return URL');
	$return_page = $_SESSION['auth_redirect'];
	
	// Clear out no longer needed items from the session
	unset($_SESSION['auth_redirect']);
	unset($_SESSION['auth_state']);
	session_write_close();

	if($verbose) {
		echo "\n";
		var_dump($_SESSION);
		header('Refresh: 10; URL='. $return_page);
	} else {
		header('Location:'. $return_page);
	}
} else {
    echo "<pre>\nState is wrong. Did you make sure to actually hit the login url first?\nYou will be redirected to <a href=" . $mainsite . ">" . $mainsite . "</a> shortly\n\n";
	var_dump($_GET);
	echo "\n";
	var_dump($_SESSION);
	header('Refresh: 30; URL='. $mainsite);
}
exit();

/////////////////////////////////////////////////////////////////////////////
// Support functions below this point

// Error function
function auth_error($error_message)
{
	print "There's been an error - " . $error_message;
	error_log($error_message);
	exit();
}
?>

It looks like it should be outputting errors if there were any, so I’m guessing I need to be looking somewhere else. All our pages share a common header which I haven’t changed in 2 years.

This looks like your token exchange that will start a session.

So this will send the user to twitch if they have no session and no state.
and if they do have a auth_state and state in session do the code exchange.

So this isn’t your “I’m on another page doing a session check for the user of my website” logic.

The other thing that could be at fault is that you have run out of disk space on the server for where PHP is configured to store session information.

And when the server does “trash cleanup” if then has space again.

So here I’m intrepreting your question to say that $_SESSION is empty
Not that the contents/the access token you have stored in $_SESSION is expired.
And the refresh token stored within $_SESSION is also dead. (which would be weird)

So if your session is dumping out (rather than the token is dead and you try the refresh token). then it’s PHP session storage would be my shout.

Check the server/php error logs and see if that throws anything up
And/or check your server for Disk space usage on where PHP is storing sessions (check server php.ini and see what it says is the location for storage, default config is usually using file based sessions and off hand I forget what the defaults are (and can vary by distro/packaging))

We’re using 51Gb of 250Gb, I will see what I can find in the way of logs/errors and I guess get it to spit out what session data is stored to see if that offers up any explanation. Thanks for the input, hopefully this will turn up something! It seems to be happening intermittently and then goes away again so I guess that’s better than happening all the time, but it would be nice to make it go away so people can use everything properly.

Depending on your fstab (assuming linux) /tmp (the usual spot for session) but not be in the “main” section. My usually the space my tmp lives tends to be running about 5gb free but I did hae a fault on another server recently where a monster log file killed sessions due to a misconfiguration

Yeah thats why I think it’s something odd outside the normal realms!

Good luck tracking it down!

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.