We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

apiauthaction.php 14.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Base class for API actions that require authentication
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  API
 * @package   StatusNet
Zach Copley's avatar
Zach Copley committed
24 25 26 27 28 29 30
 * @author    Adrian Lang <mail@adrianlang.de>
 * @author    Brenda Wallace <shiny@cpan.org>
 * @author    Craig Andrews <candrews@integralblue.com>
 * @author    Dan Moore <dan@moore.cx>
 * @author    Evan Prodromou <evan@status.net>
 * @author    mEDI <medi@milaro.net>
 * @author    Sarven Capadisli <csarven@status.net>
31
 * @author    Zach Copley <zach@status.net>
32
 * @copyright 2009-2010 StatusNet, Inc.
33
 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
34 35 36 37
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

38 39 40 41
/* External API usage documentation. Please update when you change how this method works. */

/*! @page authentication Authentication

42
    GNU social supports HTTP Basic Authentication and OAuth for API calls.
43 44 45 46 47 48 49 50 51 52 53 54 55

    @warning Currently, users who have created accounts without setting a
    password via OpenID, Facebook Connect, etc., cannot use the API until
    they set a password with their account settings panel.

    @section HTTP Basic Auth



    @section OAuth

*/

56
if (!defined('GNUSOCIAL')) { exit(1); }
57

58 59 60 61 62 63 64 65 66
/**
 * Actions extending this class will require auth
 *
 * @category API
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */
67
class ApiAuthAction extends ApiAction
68
{
69 70
    var $auth_user_nickname = null;
    var $auth_user_password = null;
71

72
    /**
73 74
     * Take arguments for running, looks for an OAuth request,
     * and outputs basic auth header if needed
75 76 77 78 79 80
     *
     * @param array $args $_REQUEST args
     *
     * @return boolean success flag
     *
     */
81
    protected function prepare(array $args=array())
82 83 84
    {
        parent::prepare($args);

85 86 87
        // NOTE: $this->scoped and $this->auth_user has to get set in
        // prepare(), not handle(), as subclasses use them in prepares.

88 89 90 91
        // Allow regular login session, but we have to double-check the
        // HTTP_REFERER value to avoid cross domain POSTing since the API
        // doesn't use the "token" form field.
        if (common_logged_in() && common_local_referer()) {
92 93 94 95 96 97
            $this->scoped = Profile::current();
            $this->auth_user = $this->scoped->getUser();
            if (!$this->auth_user->hasRight(Right::API)) {
                // TRANS: Authorization exception thrown when a user without API access tries to access the API.
                throw new AuthorizationException(_('Not allowed to use API.'));
            }
98 99
            // Let's run this in the same way as if we've just authenticated the user (basic/oauth auth)
            Event::handle('EndSetApiUser', array($this->auth_user));
100 101 102
            $this->access = self::READ_WRITE;
        } else {
            $oauthReq = $this->getOAuthRequest();
103

104
            if ($oauthReq instanceof OAuthRequest) {
105
                $this->checkOAuthRequest($oauthReq);
106 107 108 109
            } else {
                // If not using OAuth, check if there is a basic auth
                // and require it if the current action requires it.
                $this->checkBasicAuthUser($this->requiresAuth());
110
            }
111

112 113 114 115 116 117
            // NOTE: Make sure we're scoped properly based on the auths!
            if (isset($this->auth_user) && $this->auth_user instanceof User) {
                $this->scoped = $this->auth_user->getProfile();
            } else {
                $this->scoped = null;
            }
118
        }
119

120 121 122 123
        // legacy user transferral
        // TODO: remove when sure no extended classes need it
        $this->user = $this->auth_user;

124 125 126 127
        // Reject API calls with the wrong access level

        if ($this->isReadOnly($args) == false) {
            if ($this->access != self::READ_WRITE) {
128
                // TRANS: Client error 401.
129 130
                $msg = _('API resource requires read-write access, ' .
                         'but you only have read access.');
131
                $this->clientError($msg, 401);
Zach Copley's avatar
Zach Copley committed
132
            }
133 134 135 136 137
        }

        return true;
    }

138 139 140 141 142 143 144
    /**
     * Determine whether the request is an OAuth request.
     * This is to avoid doign any unnecessary DB lookups.
     *
     * @return mixed the OAuthRequest or false
     */
    function getOAuthRequest()
145
    {
146
        ApiOAuthAction::cleanRequest();
147 148 149 150 151 152 153 154 155 156 157 158 159 160

        $req  = OAuthRequest::from_request();

        $consumer    = $req->get_parameter('oauth_consumer_key');
        $accessToken = $req->get_parameter('oauth_token');

        // XXX: Is it good enough to assume it's not meant to be an
        // OAuth request if there is no consumer or token? --Z

        if (empty($consumer) || empty($accessToken)) {
            return false;
        }

        return $req;
161 162
    }

163 164 165 166 167 168 169 170 171
    /**
     * Verifies the OAuth request signature, sets the auth user
     * and access type (read-only or read-write)
     *
     * @param OAuthRequest $request the OAuth Request
     *
     * @return nothing
     */
    function checkOAuthRequest($request)
172
    {
mattl's avatar
mattl committed
173
        $datastore   = new ApiGNUsocialOAuthDataStore();
Zach Copley's avatar
Zach Copley committed
174 175
        $server      = new OAuthServer($datastore);
        $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
176

Zach Copley's avatar
Zach Copley committed
177
        $server->add_signature_method($hmac_method);
178

Zach Copley's avatar
Zach Copley committed
179
        try {
180
            $server->verify_request($request);
181

182 183
            $consumer     = $request->get_parameter('oauth_consumer_key');
            $access_token = $request->get_parameter('oauth_token');
184

185
            $app = Oauth_application::getByConsumerKey($consumer);
186

187
            if (empty($app)) {
Zach Copley's avatar
Zach Copley committed
188 189 190 191 192
                common_log(
                    LOG_WARNING,
                    'API OAuth - Couldn\'t find the OAuth app for consumer key: ' .
                    $consumer
                );
193 194
                // TRANS: OAuth exception thrown when no application is found for a given consumer key.
                throw new OAuthException(_('No application for that consumer key.'));
Zach Copley's avatar
Zach Copley committed
195
            }
196

197
            // set the source attr
198 199 200
            if ($app->name != 'anonymous') {
                $this->source = $app->name;
            }
201 202


203
            $appUser = Oauth_application_user::getKV('token', $access_token);
204

Zach Copley's avatar
Zach Copley committed
205 206 207
            if (!empty($appUser)) {
                // If access_type == 0 we have either a request token
                // or a bad / revoked access token
208

209 210
                if ($appUser->access_type != 0) {
                    // Set the access level for the api call
211 212 213
                    $this->access = ($appUser->access_type & Oauth_application::$writeAccess)
                      ? self::READ_WRITE : self::READ_ONLY;

214
                    // Set the auth user
215
                    if (Event::handle('StartSetApiUser', array(&$user))) {
216
                        $user = User::getKV('id', $appUser->profile_id);
217 218 219 220 221
                    }
                    if ($user instanceof User) {
                        if (!$user->hasRight(Right::API)) {
                            // TRANS: Authorization exception thrown when a user without API access tries to access the API.
                            throw new AuthorizationException(_('Not allowed to use API.'));
222 223
                        }
                        $this->auth_user = $user;
224 225 226 227
                        Event::handle('EndSetApiUser', array($this->auth_user));
                    } else {
                        // If $user is not a real User, let's force it to null.
                        $this->auth_user = null;
228
                    }
229

230 231 232 233 234 235
                    // FIXME: setting the value returned by common_current_user()
                    // There should probably be a better method for this. common_set_user()
                    // does lots of session stuff.
                    global $_cur;
                    $_cur = $this->auth_user;

Zach Copley's avatar
Zach Copley committed
236
                    $msg = "API OAuth authentication for user '%s' (id: %d) on behalf of " .
Zach Copley's avatar
Zach Copley committed
237 238 239 240 241 242 243 244 245 246 247 248 249
                        "application '%s' (id: %d) with %s access.";

                    common_log(
                        LOG_INFO,
                        sprintf(
                            $msg,
                            $this->auth_user->nickname,
                            $this->auth_user->id,
                            $app->name,
                            $app->id,
                            ($this->access = self::READ_WRITE) ? 'read-write' : 'read-only'
                        )
                    );
Zach Copley's avatar
Zach Copley committed
250
                } else {
251 252
                    // TRANS: OAuth exception given when an incorrect access token was given for a user.
                    throw new OAuthException(_('Bad access token.'));
Zach Copley's avatar
Zach Copley committed
253 254
                }
            } else {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
255
                // Also should not happen.
256 257
                // TRANS: OAuth exception given when no user was found for a given token (no token was found).
                throw new OAuthException(_('No user for that token.'));
258
            }
259

Zach Copley's avatar
Zach Copley committed
260
        } catch (OAuthException $e) {
Zach Copley's avatar
Zach Copley committed
261
            $this->logAuthFailure($e->getMessage());
262
            common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage());
263
            $this->clientError($e->getMessage(), 401);
Zach Copley's avatar
Zach Copley committed
264
        }
265 266
    }

267 268 269 270 271
    /**
     * Does this API resource require authentication?
     *
     * @return boolean true
     */
272
    public function requiresAuth()
273 274 275 276
    {
        return true;
    }

277
    /**
278
     * Check for a user specified via HTTP basic auth. If there isn't
279 280 281 282
     * one, try to get one by outputting the basic auth header.
     *
     * @return boolean true or false
     */
283
    function checkBasicAuthUser($required = true)
284 285 286
    {
        $this->basicAuthProcessHeader();

Evan Prodromou's avatar
Evan Prodromou committed
287 288 289 290 291
        $realm = common_config('api', 'realm');

        if (empty($realm)) {
            $realm = common_config('site', 'name') . ' API';
        }
292

293
        if (empty($this->auth_user_nickname) && $required) {
294
            header('WWW-Authenticate: Basic realm="' . $realm . '"');
295 296

            // show error if the user clicks 'cancel'
297
            // TRANS: Client error thrown when authentication fails because a user clicked "Cancel".
298
            $this->clientError(_('Could not authenticate you.'), 401);
299

300
        } else {
301
            // $this->auth_user_nickname - i.e. PHP_AUTH_USER - will have a value since it was not empty
302

303 304 305
            $user = common_check_user($this->auth_user_nickname,
                                      $this->auth_user_password);

306 307 308 309 310
            Event::handle('StartSetApiUser', array(&$user));
            if ($user instanceof User) {
                if (!$user->hasRight(Right::API)) {
                    // TRANS: Authorization exception thrown when a user without API access tries to access the API.
                    throw new AuthorizationException(_('Not allowed to use API.'));
311
                }
312
                $this->auth_user = $user;
313

314 315 316
                Event::handle('EndSetApiUser', array($this->auth_user));
            } else {
                $this->auth_user = null;
Craig Andrews's avatar
Craig Andrews committed
317
            }
318

319 320 321 322
            if ($required && $this->auth_user instanceof User) {
                // By default, basic auth users have rw access
                $this->access = self::READ_WRITE;
            } elseif ($required) {
Zach Copley's avatar
Zach Copley committed
323 324 325 326 327
                $msg = sprintf(
                    "basic auth nickname = %s",
                    $this->auth_user_nickname
                );
                $this->logAuthFailure($msg);
328 329 330

                // We must present WWW-Authenticate in accordance to HTTP status code 401
                header('WWW-Authenticate: Basic realm="' . $realm . '"');
331
                // TRANS: Client error thrown when authentication fails.
332
                $this->clientError(_('Could not authenticate you.'), 401);
333 334 335
            } else {
                // all get rw access for actions that don't require auth
                $this->access = self::READ_WRITE;
336 337 338 339
            }
        }
    }

340 341 342 343 344 345
    /**
     * Read the HTTP headers and set the auth user.  Decodes HTTP_AUTHORIZATION
     * param to support basic auth when PHP is running in CGI mode.
     *
     * @return void
     */
346 347
    function basicAuthProcessHeader()
    {
348 349 350 351 352 353 354 355 356
        $authHeaders = array('AUTHORIZATION',
                             'HTTP_AUTHORIZATION',
                             'REDIRECT_HTTP_AUTHORIZATION'); // rewrite for CGI
        $authorization_header = null;
        foreach ($authHeaders as $header) {
            if (isset($_SERVER[$header])) {
                $authorization_header = $_SERVER[$header];
                break;
            }
357 358 359
        }

        if (isset($_SERVER['PHP_AUTH_USER'])) {
360 361
            $this->auth_user_nickname = $_SERVER['PHP_AUTH_USER'];
            $this->auth_user_password = $_SERVER['PHP_AUTH_PW'];
362 363 364
        } elseif (isset($authorization_header)
            && strstr(substr($authorization_header, 0, 5), 'Basic')) {

365
            // Decode the HTTP_AUTHORIZATION header on php-cgi server self
366 367
            // on fcgid server the header name is AUTHORIZATION
            $auth_hash = base64_decode(substr($authorization_header, 6));
368 369
            list($this->auth_user_nickname,
                 $this->auth_user_password) = explode(':', $auth_hash);
370

371
            // Set all to null on a empty basic auth request
372

373 374 375
            if (empty($this->auth_user_nickname)) {
                $this->auth_user_nickname = null;
                $this->auth_password = null;
376 377 378
            }
        }
    }
Zach Copley's avatar
Zach Copley committed
379 380

    /**
381
     * Log an API authentication failure. Collect the proxy and IP
Zach Copley's avatar
Zach Copley committed
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
     * and log them
     *
     * @param string $logMsg additional log message
     */
     function logAuthFailure($logMsg)
     {
        list($proxy, $ip) = common_client_ip();

        $msg = sprintf(
            'API auth failure (proxy = %1$s, ip = %2$s) - ',
            $proxy,
            $ip
        );

        common_log(LOG_WARNING, $msg . $logMsg);
     }
398
}