Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
PasskeyPrimaryAuthenticationProvider
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 8
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAuthenticationRequests
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 beginPrimaryAuthentication
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
56
 accountCreationType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 beginPrimaryAccountCreation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 testUserExists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 providerAllowsAuthenticationDataChange
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 providerChangeAuthenticationData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\OATHAuth\Auth;
4
5use BadMethodCallException;
6use MediaWiki\Auth\AbstractPrimaryAuthenticationProvider;
7use MediaWiki\Auth\AuthenticationRequest;
8use MediaWiki\Auth\AuthenticationResponse;
9use MediaWiki\Auth\AuthManager;
10use MediaWiki\Extension\OATHAuth\OATHAuthLogger;
11use MediaWiki\Extension\OATHAuth\WebAuthnAuthenticator;
12use MediaWiki\Status\Status;
13use MediaWiki\User\UserFactory;
14
15class PasskeyPrimaryAuthenticationProvider extends AbstractPrimaryAuthenticationProvider {
16
17    public function __construct(
18        private readonly WebAuthnAuthenticator $webAuthnAuthenticator,
19        private readonly OATHAuthLogger $oathLogger,
20        private readonly UserFactory $userFactory
21    ) {
22    }
23
24    public const SUCCESS_KEY = 'webauthn-passkey-successful';
25
26    /** @inheritDoc */
27    public function getAuthenticationRequests( $action, array $options ) {
28        if (
29            !$this->config->get( 'OATHPasswordlessLogin' ) ||
30            $action !== AuthManager::ACTION_LOGIN
31        ) {
32            return [];
33        }
34
35        $authStatus = $this->webAuthnAuthenticator->startPasswordlessAuthentication();
36        if ( !$authStatus->isGood() ) {
37            return [];
38        }
39
40        // TODO make AuthenticationRequest::getUsernameFromRequests() recognize the username
41        // from this, so that ThrottlePreAuthenticationProvider will apply the password throttle
42        return [ new WebAuthnAuthenticationRequest( $authStatus->getValue()['json'], false ) ];
43    }
44
45    /** @inheritDoc */
46    public function beginPrimaryAuthentication( array $reqs ) {
47        $req = AuthenticationRequest::getRequestByClass( $reqs, WebAuthnAuthenticationRequest::class );
48        if ( !$req || $req->credential === '' ) {
49            return AuthenticationResponse::newAbstain();
50        }
51
52        $oathUser = $this->webAuthnAuthenticator->determineUser( $req->credential );
53        if ( !$oathUser ) {
54            return AuthenticationResponse::newFail(
55                wfMessage( 'oathauth-webauthn-error-passkey-verification-failed' )
56            );
57        }
58        $user = $this->userFactory->newFromUserIdentity( $oathUser->getUser() );
59
60        // Don't increase pingLimiter, just check for limit exceeded.
61        if ( $user->pingLimiter( 'badoath', 0 ) ) {
62            return AuthenticationResponse::newFail(
63                wfMessage( 'oathauth-throttled' )
64            );
65        }
66
67        $authResult = $this->webAuthnAuthenticator->continueAuthentication( $oathUser, $req->credential );
68        if ( $authResult->isGood() ) {
69            $this->logger->info( 'OATHAuth user {user} completed passwordless login from {clientip}', [
70                'user'     => $user->getName(),
71                'clientip' => $user->getRequest()->getIP(),
72            ] );
73            $this->oathLogger->logSuccessfulVerification( $user );
74            // Record the fact that the user logged in with a passkey, so that
75            // our SecondaryAuthenticationProvider will skip the 2FA step
76            $this->manager->setAuthenticationSessionData( self::SUCCESS_KEY, true );
77            return AuthenticationResponse::newPass( $user->getName() );
78        }
79
80        // Increase rate limit counter for failed request
81        $user->pingLimiter( 'badoath' );
82
83        $this->logger->info( 'OATHAuth user {user} failed passwordless login from {clientip}', [
84            'user'     => $user->getName(),
85            'clientip' => $user->getRequest()->getIP(),
86        ] );
87
88        $this->oathLogger->logFailedVerification( $user );
89
90        $messages = $authResult->getMessages();
91        // Return the first error from the authenticator, if there is any
92        $failureMessage = $messages ? wfMessage( $messages[0] ) :
93            wfMessage( 'oathauth-webauthn-error-passkey-verification-failed' );
94
95        return AuthenticationResponse::newFail( $failureMessage );
96    }
97
98    /** @inheritDoc */
99    public function accountCreationType() {
100        return self::TYPE_NONE;
101    }
102
103    /** @inheritDoc */
104    public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
105        // @phan-suppress-previous-line PhanPluginNeverReturnMethod
106        throw new BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
107    }
108
109    /** @inheritDoc */
110    public function testUserExists( $username, $flags = \Wikimedia\Rdbms\IDBAccessObject::READ_NORMAL ) {
111        // We rely on other primary authentication providers to manage user existence
112        return false;
113    }
114
115    /** @inheritDoc */
116    public function providerAllowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
117        // Not supported
118        return Status::newGood( 'ignored' );
119    }
120
121    public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
122        // Do nothing; passkeys can't be managed this way
123    }
124}