private $remembered = false;
private $forceHTTPS = false;
private $idIsSafe = false;
+ private $forceUse = false;
/** @var array|null */
private $providerMetadata = null;
* - idIsSafe: (bool) Set true if the 'id' did not come from the user.
* Generally you'll use this from SessionProvider::newEmptySession(),
* and not from any other method.
+ * - forceUse: (bool) Set true if the 'id' is from
+ * SessionProvider::hashToSessionId() to delete conflicting session
+ * store data instead of discarding this SessionInfo. Ignored unless
+ * both 'provider' and 'id' are given.
* - copyFrom: (SessionInfo) SessionInfo to copy other data items from.
*/
public function __construct( $priority, array $data ) {
'forceHTTPS' => $from->forceHTTPS,
'metadata' => $from->providerMetadata,
'idIsSafe' => $from->idIsSafe,
+ 'forceUse' => $from->forceUse,
// @codeCoverageIgnoreStart
];
// @codeCoverageIgnoreEnd
'forceHTTPS' => false,
'metadata' => null,
'idIsSafe' => false,
+ 'forceUse' => false,
// @codeCoverageIgnoreStart
];
// @codeCoverageIgnoreEnd
if ( $data['id'] !== null ) {
$this->id = $data['id'];
$this->idIsSafe = $data['idIsSafe'];
+ $this->forceUse = $data['forceUse'] && $this->provider;
} else {
$this->id = $this->provider->getManager()->generateSessionId();
$this->idIsSafe = true;
+ $this->forceUse = false;
}
$this->priority = (int)$priority;
$this->userInfo = $data['userInfo'];
return $this->idIsSafe;
}
+ /**
+ * Force use of this SessionInfo if validation fails
+ *
+ * The normal behavior is to discard the SessionInfo if validation against
+ * the data stored in the session store fails. If this returns true,
+ * SessionManager will instead delete the session store data so this
+ * SessionInfo may still be used.
+ *
+ * @return bool
+ */
+ final public function forceUse() {
+ return $this->forceUse;
+ }
+
/**
* Return the priority
* @return int
$key = wfMemcKey( 'MWSession', $info->getId() );
$blob = $this->store->get( $key );
+ // If we got data from the store and the SessionInfo says to force use,
+ // "fail" means to delete the data from the store and retry. Otherwise,
+ // "fail" is just return false.
+ if ( $info->forceUse() && $blob !== false ) {
+ $failHandler = function () use ( $key, &$info, $request ) {
+ $this->store->delete( $key );
+ return $this->loadSessionInfoFromStore( $info, $request );
+ };
+ } else {
+ $failHandler = function () {
+ return false;
+ };
+ }
+
$newParams = [];
if ( $blob !== false ) {
'session' => $info,
] );
$this->store->delete( $key );
- return false;
+ return $failHandler();
}
// Sanity check: blob has data and metadata arrays
'session' => $info,
] );
$this->store->delete( $key );
- return false;
+ return $failHandler();
}
$data = $blob['data'];
'session' => $info,
] );
$this->store->delete( $key );
- return false;
+ return $failHandler();
}
// First, load the provider from metadata, or validate it against the metadata.
]
);
$this->store->delete( $key );
- return false;
+ return $failHandler();
}
} elseif ( $metadata['provider'] !== (string)$provider ) {
$this->logger->warning( 'Session "{session}": Wrong provider ' .
[
'session' => $info,
] );
- return false;
+ return $failHandler();
}
// Load provider metadata from metadata, or validate it against the metadata
'exception' => $ex,
] + $ex->getContext()
);
- return false;
+ return $failHandler();
}
}
}
'session' => $info,
'exception' => $ex,
] );
- return false;
+ return $failHandler();
}
$newParams['userInfo'] = $userInfo;
} else {
'uid_a' => $metadata['userId'],
'uid_b' => $userInfo->getId(),
] );
- return false;
+ return $failHandler();
}
// If the user was renamed, probably best to fail here.
'uname_a' => $metadata['userName'],
'uname_b' => $userInfo->getName(),
] );
- return false;
+ return $failHandler();
}
} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
'uname_a' => $metadata['userName'],
'uname_b' => $userInfo->getName(),
] );
- return false;
+ return $failHandler();
}
} elseif ( !$userInfo->isAnon() ) {
// Metadata specifies an anonymous user, but the passed-in
[
'session' => $info,
] );
- return false;
+ return $failHandler();
}
}
$this->logger->warning( 'Session "{session}": User token mismatch', [
'session' => $info,
] );
- return false;
+ return $failHandler();
}
if ( !$userInfo->isVerified() ) {
$newParams['userInfo'] = $userInfo->verified();
[
'session' => $info,
] );
- return false;
+ return $failHandler();
}
// If no user was provided and no metadata, it must be anon.
[
'session' => $info,
] );
- return false;
+ return $failHandler();
}
} elseif ( !$info->getUserInfo()->isVerified() ) {
$this->logger->warning(
[
'session' => $info,
] );
- return false;
+ return $failHandler();
}
$data = false;
// Allow the provider to check the loaded SessionInfo
$providerMetadata = $info->getProviderMetadata();
if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
- return false;
+ return $failHandler();
}
if ( $providerMetadata !== $info->getProviderMetadata() ) {
$info = new SessionInfo( $info->getPriority(), [
$this->logger->warning( 'Session "{session}": ' . $reason, [
'session' => $info,
] );
- return false;
+ return $failHandler();
}
return true;
*
* Generally this will only be used when self::persistsSessionId() is false and
* the provider has to base the session ID on the verified user's identity
- * or other static data.
+ * or other static data. The SessionInfo should then typically have the
+ * 'forceUse' flag set to avoid persistent session failure if validation of
+ * the stored data fails.
*
* @param string $data
* @param string|null $key Defaults to $this->config->get( 'SecretKey' )
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $anonInfo, $info->getUserInfo() );
$this->assertTrue( $info->isIdSafe() );
+ $this->assertFalse( $info->forceUse() );
$this->assertFalse( $info->wasPersisted() );
$this->assertFalse( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
$this->assertTrue( $info->isIdSafe() );
+ $this->assertFalse( $info->forceUse() );
$this->assertFalse( $info->wasPersisted() );
$this->assertFalse( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $userInfo, $info->getUserInfo() );
$this->assertTrue( $info->isIdSafe() );
+ $this->assertFalse( $info->forceUse() );
$this->assertFalse( $info->wasPersisted() );
$this->assertTrue( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $anonInfo, $info->getUserInfo() );
$this->assertFalse( $info->isIdSafe() );
+ $this->assertFalse( $info->forceUse() );
$this->assertTrue( $info->wasPersisted() );
$this->assertFalse( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $userInfo, $info->getUserInfo() );
$this->assertFalse( $info->isIdSafe() );
+ $this->assertFalse( $info->forceUse() );
$this->assertFalse( $info->wasPersisted() );
$this->assertTrue( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $userInfo, $info->getUserInfo() );
$this->assertFalse( $info->isIdSafe() );
+ $this->assertFalse( $info->forceUse() );
$this->assertTrue( $info->wasPersisted() );
$this->assertFalse( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertTrue( $info->isIdSafe() );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+ 'id' => $id,
+ 'forceUse' => true,
+ ] );
+ $this->assertFalse( $info->forceUse(), 'no provider' );
+
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+ 'provider' => $provider,
+ 'forceUse' => true,
+ ] );
+ $this->assertFalse( $info->forceUse(), 'no id' );
+
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+ 'provider' => $provider,
+ 'id' => $id,
+ 'forceUse' => true,
+ ] );
+ $this->assertTrue( $info->forceUse(), 'correct use' );
+
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
'id' => $id,
'forceHTTPS' => 1,
'provider' => $provider,
'userInfo' => $userInfo,
'idIsSafe' => true,
+ 'forceUse' => true,
'persisted' => true,
'remembered' => true,
'forceHTTPS' => true,
$this->assertSame( $provider, $info->getProvider() );
$this->assertSame( $userInfo, $info->getUserInfo() );
$this->assertTrue( $info->isIdSafe() );
+ $this->assertTrue( $info->forceUse() );
$this->assertTrue( $info->wasPersisted() );
$this->assertTrue( $info->wasRemembered() );
$this->assertTrue( $info->forceHTTPS() );
'provider' => $provider2,
'userInfo' => $unverifiedUserInfo,
'idIsSafe' => false,
+ 'forceUse' => false,
'persisted' => false,
'remembered' => false,
'forceHTTPS' => false,
$this->assertSame( $provider2, $info->getProvider() );
$this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
$this->assertFalse( $info->isIdSafe() );
+ $this->assertFalse( $info->forceUse() );
$this->assertFalse( $info->wasPersisted() );
$this->assertFalse( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
[ LogLevel::WARNING, 'Session "{session}": Hook aborted' ],
], $logger->getBuffer() );
$logger->clearBuffer();
+ $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionCheckInfo' => [] ] );
+
+ // forceUse deletes bad backend data
+ $this->store->setSessionMeta( $id, [ 'userToken' => 'Bad' ] + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo,
+ 'forceUse' => true,
+ ] );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertFalse( $this->store->getSession( $id ) );
+ $this->assertSame( [
+ [ LogLevel::WARNING, 'Session "{session}": User token mismatch' ],
+ ], $logger->getBuffer() );
+ $logger->clearBuffer();
}
}