}
public function getAllowedParams() {
- return ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_CREATE,
+ $ret = ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_CREATE,
'requests', 'messageformat', 'mergerequestfields', 'preservestate', 'returnurl', 'continue'
);
+ $ret['preservestate'][ApiBase::PARAM_HELP_MSG_APPEND][] =
+ 'apihelp-createaccount-param-preservestate';
+ return $ret;
}
public function dynamicParameterDocumentation() {
use MediaWiki\Auth\AuthManager;
use MediaWiki\Auth\AuthenticationRequest;
use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\CreateFromLoginAuthenticationRequest;
/**
* Log in to the wiki with AuthManager
$res = $manager->beginAuthentication( $reqs, $params['returnurl'] );
}
+ // Remove CreateFromLoginAuthenticationRequest from $res->neededRequests.
+ // It's there so a RESTART treated as UI will work right, but showing
+ // it to the API client is just confusing.
+ $res->neededRequests = ApiAuthManagerHelper::blacklistAuthenticationRequests(
+ $res->neededRequests, [ CreateFromLoginAuthenticationRequest::class ]
+ );
+
$this->getResult()->addValue( null, 'clientlogin',
$helper->formatAuthenticationResponse( $res ) );
}
'canauthenticatenow' => $manager->canAuthenticateNow(),
'cancreateaccounts' => $manager->canCreateAccounts(),
'canlinkaccounts' => $manager->canLinkAccounts(),
- 'haspreservedstate' => $helper->getPreservedRequest() !== null,
];
if ( $params['securitysensitiveoperation'] !== null ) {
}
if ( $params['requestsfor'] ) {
- $reqs = $manager->getAuthenticationRequests( $params['requestsfor'], $this->getUser() );
+ $action = $params['requestsfor'];
+
+ $preservedReq = $helper->getPreservedRequest();
+ if ( $preservedReq ) {
+ $ret += [
+ 'haspreservedstate' => $preservedReq->hasStateForAction( $action ),
+ 'hasprimarypreservedstate' => $preservedReq->hasPrimaryStateForAction( $action ),
+ 'preservedusername' => (string)$preservedReq->username,
+ ];
+ } else {
+ $ret += [
+ 'haspreservedstate' => false,
+ 'hasprimarypreservedstate' => false,
+ 'preservedusername' => '',
+ ];
+ }
+
+ $reqs = $manager->getAuthenticationRequests( $action, $this->getUser() );
// Filter out blacklisted requests, depending on the action
- switch ( $params['requestsfor'] ) {
+ switch ( $action ) {
case AuthManager::ACTION_CHANGE:
$reqs = ApiAuthManagerHelper::blacklistAuthenticationRequests(
$reqs, $this->getConfig()->get( 'ChangeCredentialsBlacklist' )
"apihelp-compare-example-1": "Create a diff between revision 1 and 2.",
"apihelp-createaccount-description": "Create a new user account.",
+ "apihelp-createaccount-param-preservestate": "If <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> returned true for <samp>hasprimarypreservedstate</samp>, requests marked as <samp>primary-required</samp> should be omitted. If it returned a non-empty value for <samp>preservedusername</samp>, that username must be used for the <var>username</var> parameter.",
"apihelp-createaccount-example-create": "Start the process of creating user <kbd>Example</kbd> with password <kbd>ExamplePassword</kbd>.",
"apihelp-createaccount-param-name": "Username.",
"apihelp-createaccount-param-password": "Password (ignored if <var>$1mailpassword</var> is set).",
"apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}",
"apihelp-compare-example-1": "{{doc-apihelp-example|compare}}",
"apihelp-createaccount-description": "{{doc-apihelp-description|createaccount}}",
+ "apihelp-createaccount-param-preservestate": "{{doc-apihelp-param|createaccount|preservestate|info=This message is displayed in addition to {{msg-mw|api-help-authmanagerhelper-preservestate}}.}}",
"apihelp-createaccount-example-create": "{{doc-apihelp-example|createaccount}}",
"apihelp-createaccount-param-name": "{{doc-apihelp-param|createaccount|name}}\n{{Identical|Username}}",
"apihelp-createaccount-param-password": "{{doc-apihelp-param|createaccount|password}}",
/**
* Start an authentication flow
+ *
+ * In addition to the AuthenticationRequests returned by
+ * $this->getAuthenticationRequests(), a client might include a
+ * CreateFromLoginAuthenticationRequest from a previous login attempt to
+ * preserve state.
+ *
+ * Instead of the AuthenticationRequests returned by
+ * $this->getAuthenticationRequests(), a client might pass a
+ * CreatedAccountAuthenticationRequest from an account creation that just
+ * succeeded to log in to the just-created account.
+ *
* @param AuthenticationRequest[] $reqs
* @param string $returnToUrl Url that REDIRECT responses should eventually
* return to.
* Return values are interpreted as follows:
* - status FAIL: Authentication failed. If $response->createRequest is
* set, that may be passed to self::beginAuthentication() or to
- * self::beginAccountCreation() (after adding a username, if necessary)
- * to preserve state.
+ * self::beginAccountCreation() to preserve state.
* - status REDIRECT: The client should be redirected to the contained URL,
* new AuthenticationRequests should be made (if any), then
* AuthManager::continueAuthentication() should be called.
/**
* Start an account creation flow
+ *
+ * In addition to the AuthenticationRequests returned by
+ * $this->getAuthenticationRequests(), a client might include a
+ * CreateFromLoginAuthenticationRequest from a previous login attempt. If
+ * <code>
+ * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
+ * </code>
+ * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
+ * should be omitted. If the CreateFromLoginAuthenticationRequest has a
+ * username set, that username must be used for all other requests.
+ *
* @param User $creator User doing the account creation
* @param AuthenticationRequest[] $reqs
* @param string $returnToUrl Url that REDIRECT responses should eventually
if ( $req ) {
$state['maybeLink'] = $req->maybeLink;
- // If we get here, the user didn't submit a form with any of the
- // usual AuthenticationRequests that are needed for an account
- // creation. So we need to determine if there are any and return a
- // UI response if so.
if ( $req->createRequest ) {
- // We have a createRequest from a
- // PrimaryAuthenticationProvider, so don't ask.
- $providers = $this->getPreAuthenticationProviders() +
- $this->getSecondaryAuthenticationProviders();
- } else {
- // We're only preserving maybeLink, so ask for primary fields
- // too.
- $providers = $this->getPreAuthenticationProviders() +
- $this->getPrimaryAuthenticationProviders() +
- $this->getSecondaryAuthenticationProviders();
- }
- $reqs = $this->getAuthenticationRequestsInternal(
- self::ACTION_CREATE,
- [],
- $providers
- );
- // See if we need any requests to begin
- foreach ( (array)$reqs as $r ) {
- if ( !$r instanceof UsernameAuthenticationRequest &&
- !$r instanceof UserDataAuthenticationRequest &&
- !$r instanceof CreationReasonAuthenticationRequest
- ) {
- // Needs some reqs, so request them
- $reqs[] = new CreateFromLoginAuthenticationRequest( $req->createRequest, [] );
- $state['continueRequests'] = $reqs;
- $session->setSecret( 'AuthManager::accountCreationState', $state );
- $session->persist();
- return AuthenticationResponse::newUI( $reqs, wfMessage( 'authmanager-create-from-login' ) );
- }
+ $reqs[] = $req->createRequest;
+ $state['reqs'][] = $req->createRequest;
}
- // No reqs needed, so we can just continue.
- $req->createRequest->returnToUrl = $returnToUrl;
- $reqs = [ $req->createRequest ];
}
$session->setSecret( 'AuthManager::accountCreationState', $state );
$req->username = $state['username'];
}
- // If we're coming in from a create-from-login UI response, we need
- // to extract the createRequest (if any).
- $req = AuthenticationRequest::getRequestByClass(
- $reqs, CreateFromLoginAuthenticationRequest::class
- );
- if ( $req && $req->createRequest ) {
- $reqs[] = $req->createRequest;
- }
-
// Run pre-creation tests, if we haven't already
if ( !$state['ranPreTests'] ) {
$providers = $this->getPreAuthenticationProviders() +
/**
* @var AuthenticationRequest|null
*
- * Returned with a PrimaryAuthenticationProvider login FAIL, this holds a
- * request that should result in a PASS when passed to that provider's
- * PrimaryAuthenticationProvider::beginPrimaryAccountCreation().
+ * Returned with a PrimaryAuthenticationProvider login FAIL or a PASS with
+ * no username, this holds a request that should result in a PASS when
+ * passed to that provider's PrimaryAuthenticationProvider::beginPrimaryAccountCreation().
*
- * Returned with an AuthManager login FAIL or RESTART, this holds a request
- * that may be passed to AuthManager::beginCreateAccount() after setting
- * its ->returnToUrl property. It may also be passed to
+ * Returned with an AuthManager login FAIL or RESTART, this holds a
+ * CreateFromLoginAuthenticationRequest that may be passed to
+ * AuthManager::beginCreateAccount(), possibly in place of any
+ * "primary-required" requests. It may also be passed to
* AuthManager::beginAuthentication() to preserve state.
*/
public $createRequest = null;
if ( !is_array( $state ) ) {
return AuthenticationResponse::newAbstain();
}
- $maybeLink = $state['maybeLink'];
+
+ $maybeLink = array_filter( $state['maybeLink'], function ( $req ) {
+ return $this->manager->allowsAuthenticationDataChange( $req )->isGood();
+ } );
if ( !$maybeLink ) {
return AuthenticationResponse::newAbstain();
}
* This transfers state between the login and account creation flows.
*
* AuthManager::getAuthenticationRequests() won't return this type, but it
- * may be passed to AuthManager::beginAccountCreation() anyway.
+ * may be passed to AuthManager::beginAuthentication() or
+ * AuthManager::beginAccountCreation() anyway.
*
* @ingroup Auth
* @since 1.27
) {
$this->createRequest = $createRequest;
$this->maybeLink = $maybeLink;
+ $this->username = $createRequest ? $createRequest->username : null;
}
public function getFieldInfo() {
public function loadFromSubmission( array $data ) {
return true;
}
+
+ /**
+ * Indicate whether this request contains any state for the specified
+ * action.
+ * @param string $action One of the AuthManager::ACTION_* constants
+ * @return boolean
+ */
+ public function hasStateForAction( $action ) {
+ switch ( $action ) {
+ case AuthManager::ACTION_LOGIN:
+ return (bool)$this->maybeLink;
+ case AuthManager::ACTION_CREATE:
+ return $this->maybeLink || $this->createRequest;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Indicate whether this request contains state for the specified
+ * action sufficient to replace other primary-required requests.
+ * @param string $action One of the AuthManager::ACTION_* constants
+ * @return boolean
+ */
+ public function hasPrimaryStateForAction( $action ) {
+ switch ( $action ) {
+ case AuthManager::ACTION_CREATE:
+ return (bool)$this->createRequest;
+ default:
+ return false;
+ }
+ }
}
$userReq = new UsernameAuthenticationRequest;
$userReq->username = 'UTDummy';
+ $req1->returnToUrl = 'http://localhost/';
+ $req2->returnToUrl = 'http://localhost/';
+ $req3->returnToUrl = 'http://localhost/';
+ $req3->username = 'UTDummy';
+ $userReq->returnToUrl = 'http://localhost/';
+
// Passing one into beginAuthentication(), and an immediate FAIL
$primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
$this->primaryauthMocks = [ $primary ];
$this->assertSame( $req2, $ret->createRequest->createRequest );
$this->assertEquals( [], $ret->createRequest->maybeLink );
- // Pass into beginAccountCreation(), no createRequest, primary needs reqs
- $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
- ->setMethods( [ 'testForAccountCreation' ] )
- ->getMockForAbstractClass();
+ // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
+ $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
$this->primaryauthMocks = [ $primary ];
$this->initializeManager( true );
+ $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
+ $createReq->returnToUrl = 'http://localhost/';
+ $createReq->username = 'UTDummy';
+ $res = AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) );
+ $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
+ ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
+ ->will( $this->returnValue( $res ) );
$primary->expects( $this->any() )->method( 'accountCreationType' )
->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
- $primary->expects( $this->any() )->method( 'getAuthenticationRequests' )
- ->will( $this->returnValue( [ $req1 ] ) );
- $primary->expects( $this->any() )->method( 'testForAccountCreation' )
- ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
- $createReq = new CreateFromLoginAuthenticationRequest(
- null, [ $req2->getUniqueId() => $req2 ]
- );
$this->logger->setCollect( true );
$ret = $this->manager->beginAccountCreation(
$user, [ $userReq, $createReq ], 'http://localhost/'
);
$this->logger->setCollect( false );
$this->assertSame( AuthenticationResponse::UI, $ret->status );
- $this->assertCount( 4, $ret->neededRequests );
- $this->assertSame( $req1, $ret->neededRequests[0] );
- $this->assertInstanceOf( UsernameAuthenticationRequest::class, $ret->neededRequests[1] );
- $this->assertInstanceOf( UserDataAuthenticationRequest::class, $ret->neededRequests[2] );
- $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->neededRequests[3] );
- $this->assertSame( null, $ret->neededRequests[3]->createRequest );
- $this->assertEquals( [], $ret->neededRequests[3]->maybeLink );
-
- // Pass into beginAccountCreation(), with createRequest, primary needs reqs
- $createReq = new CreateFromLoginAuthenticationRequest( $req2, [] );
- $this->logger->setCollect( true );
- $ret = $this->manager->beginAccountCreation(
- $user, [ $userReq, $createReq ], 'http://localhost/'
- );
- $this->logger->setCollect( false );
- $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
- $this->assertSame( 'fail', $ret->message->getKey() );
-
- // Again, with a secondary needing reqs too
- $secondary = $this->getMockBuilder( AbstractSecondaryAuthenticationProvider::class )
- ->getMockForAbstractClass();
- $this->secondaryauthMocks = [ $secondary ];
- $this->initializeManager( true );
- $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
- ->will( $this->returnValue( [ $req3 ] ) );
- $createReq = new CreateFromLoginAuthenticationRequest( $req2, [] );
- $this->logger->setCollect( true );
- $ret = $this->manager->beginAccountCreation(
- $user, [ $userReq, $createReq ], 'http://localhost/'
- );
- $this->logger->setCollect( false );
- $this->assertSame( AuthenticationResponse::UI, $ret->status );
- $this->assertCount( 4, $ret->neededRequests );
- $this->assertSame( $req3, $ret->neededRequests[0] );
- $this->assertInstanceOf( UsernameAuthenticationRequest::class, $ret->neededRequests[1] );
- $this->assertInstanceOf( UserDataAuthenticationRequest::class, $ret->neededRequests[2] );
- $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->neededRequests[3] );
- $this->assertSame( $req2, $ret->neededRequests[3]->createRequest );
- $this->assertEquals( [], $ret->neededRequests[3]->maybeLink );
- $this->logger->setCollect( true );
- $ret = $this->manager->continueAccountCreation( $ret->neededRequests );
- $this->logger->setCollect( false );
- $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
- $this->assertSame( 'fail', $ret->message->getKey() );
+ $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
+ $this->assertNotNull( $state );
+ $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
+ $this->assertEquals( [ $req2 ], $state['maybeLink'] );
}
/**
}
public function testBeginLinkAttempt() {
+ $badReq = $this->getMockBuilder( AuthenticationRequest::class )
+ ->setMethods( [ 'getUniqueId' ] )
+ ->getMockForAbstractClass();
+ $badReq->expects( $this->any() )->method( 'getUniqueId' )
+ ->will( $this->returnValue( "BadReq" ) );
+
$user = \User::newFromName( 'UTSysop' );
$provider = \TestingAccessWrapper::newFromObject(
new ConfirmLinkSecondaryAuthenticationProvider
);
$request = new \FauxRequest();
- $manager = new AuthManager( $request, \RequestContext::getMain()->getConfig() );
+ $manager = $this->getMockBuilder( AuthManager::class )
+ ->setMethods( [ 'allowsAuthenticationDataChange' ] )
+ ->setConstructorArgs( [ $request, \RequestContext::getMain()->getConfig() ] )
+ ->getMock();
+ $manager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' )
+ ->will( $this->returnCallback( function ( $req ) {
+ return $req->getUniqueId() !== 'BadReq'
+ ? \StatusValue::newGood()
+ : \StatusValue::newFatal( 'no' );
+ } ) );
$provider->setManager( $manager );
$this->assertEquals(
$reqs = $this->getLinkRequests();
$request->getSession()->setSecret( 'state', [
- 'maybeLink' => $reqs
+ 'maybeLink' => $reqs + [ 'BadReq' => $badReq ]
] );
$res = $provider->beginLinkAttempt( $user, 'state' );
$this->assertInstanceOf( AuthenticationResponse::class, $res );
],
];
}
+
+ /**
+ * @dataProvider provideState
+ */
+ public function testState(
+ $createReq, $maybeLink, $username, $loginState, $createState, $createPrimaryState
+ ) {
+ $req = new CreateFromLoginAuthenticationRequest( $createReq, $maybeLink );
+ $this->assertSame( $username, $req->username );
+ $this->assertSame( $loginState, $req->hasStateForAction( AuthManager::ACTION_LOGIN ) );
+ $this->assertSame( $createState, $req->hasStateForAction( AuthManager::ACTION_CREATE ) );
+ $this->assertFalse( $req->hasStateForAction( AuthManager::ACTION_LINK ) );
+ $this->assertFalse( $req->hasPrimaryStateForAction( AuthManager::ACTION_LOGIN ) );
+ $this->assertSame( $createPrimaryState,
+ $req->hasPrimaryStateForAction( AuthManager::ACTION_CREATE ) );
+ }
+
+ public static function provideState() {
+ $req1 = new UsernameAuthenticationRequest;
+ $req2 = new UsernameAuthenticationRequest;
+ $req2->username = 'Bob';
+
+ return [
+ 'Nothing' => [ null, [], null, false, false, false ],
+ 'Link, no create' => [ null, [ $req2 ], null, true, true, false ],
+ 'No link, create but no name' => [ $req1, [], null, false, true, true ],
+ 'Link and create but no name' => [ $req1, [ $req2 ], null, true, true, true ],
+ 'No link, create with name' => [ $req2, [], 'Bob', false, true, true ],
+ 'Link and create with name' => [ $req2, [ $req2 ], 'Bob', true, true, true ],
+ ];
+ }
}