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

apiauthaction.php 14.2 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 88 89 90 91 92 93 94 95
        // NOTE: $this->scoped and $this->auth_user has to get set in
        // prepare(), not handle(), as subclasses use them in prepares.

        // Allow regular login session
        if (common_logged_in()) {
            $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.'));
            }
96 97
            // 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));
98 99 100
            $this->access = self::READ_WRITE;
        } else {
            $oauthReq = $this->getOAuthRequest();
101

102
            if ($oauthReq instanceof OAuthRequest) {
103
                $this->checkOAuthRequest($oauthReq);
104 105 106 107
            } 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());
108
            }
109

110 111 112 113 114 115
            // 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;
            }
116
        }
117

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

122 123 124 125
        // Reject API calls with the wrong access level

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

        return true;
    }

136 137 138 139 140 141 142
    /**
     * 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()
143
    {
144
        ApiOAuthAction::cleanRequest();
145 146 147 148 149 150 151 152 153 154 155 156 157 158

        $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;
159 160
    }

161 162 163 164 165 166 167 168 169
    /**
     * 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)
170
    {
mattl's avatar
mattl committed
171
        $datastore   = new ApiGNUsocialOAuthDataStore();
Zach Copley's avatar
Zach Copley committed
172 173
        $server      = new OAuthServer($datastore);
        $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
174

Zach Copley's avatar
Zach Copley committed
175
        $server->add_signature_method($hmac_method);
176

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

180 181
            $consumer     = $request->get_parameter('oauth_consumer_key');
            $access_token = $request->get_parameter('oauth_token');
182

183
            $app = Oauth_application::getByConsumerKey($consumer);
184

185
            if (empty($app)) {
Zach Copley's avatar
Zach Copley committed
186 187 188 189 190
                common_log(
                    LOG_WARNING,
                    'API OAuth - Couldn\'t find the OAuth app for consumer key: ' .
                    $consumer
                );
191 192
                // 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
193
            }
194

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


201
            $appUser = Oauth_application_user::getKV('token', $access_token);
202

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

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

212
                    // Set the auth user
213
                    if (Event::handle('StartSetApiUser', array(&$user))) {
214
                        $user = User::getKV('id', $appUser->profile_id);
215 216 217 218 219
                    }
                    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.'));
220 221
                        }
                        $this->auth_user = $user;
222 223 224 225
                        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;
226
                    }
227

228 229 230 231 232 233
                    // 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
234
                    $msg = "API OAuth authentication for user '%s' (id: %d) on behalf of " .
Zach Copley's avatar
Zach Copley committed
235 236 237 238 239 240 241 242 243 244 245 246 247
                        "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
248
                } else {
249 250
                    // 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
251 252
                }
            } else {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
253
                // Also should not happen.
254 255
                // 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.'));
256
            }
257

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

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

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

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

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

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

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

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

301 302 303
            $user = common_check_user($this->auth_user_nickname,
                                      $this->auth_user_password);

304 305 306 307 308
            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.'));
309
                }
310
                $this->auth_user = $user;
311

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

317 318 319 320
            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
321 322 323 324 325
                $msg = sprintf(
                    "basic auth nickname = %s",
                    $this->auth_user_nickname
                );
                $this->logAuthFailure($msg);
326 327 328

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

338 339 340 341 342 343
    /**
     * 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
     */
344 345
    function basicAuthProcessHeader()
    {
346 347 348 349 350 351 352 353 354
        $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;
            }
355 356 357
        }

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

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

369
            // Set all to null on a empty basic auth request
370

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

    /**
379
     * Log an API authentication failure. Collect the proxy and IP
Zach Copley's avatar
Zach Copley committed
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
     * 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);
     }
396
}