Page MenuHomePhabricator

oauthserver: get client ID/secret from HTTP auth
ClosedPublic

Authored by wrl on Oct 28 2016, 5:29 AM.
Tags
None
Referenced Files
Unknown Object (File)
Thu, Dec 12, 1:17 AM
Unknown Object (File)
Sun, Dec 8, 9:40 PM
Unknown Object (File)
Sun, Dec 8, 9:39 PM
Unknown Object (File)
Sun, Dec 8, 9:39 PM
Unknown Object (File)
Sat, Dec 7, 10:23 PM
Unknown Object (File)
Thu, Dec 5, 11:18 AM
Unknown Object (File)
Wed, Dec 4, 11:25 PM
Unknown Object (File)
Wed, Nov 27, 1:01 AM
Subscribers

Details

Summary

This adds the ability for Phabricator's OAuth server implementation to use HTTP basic auth for the client ID and secret and brings it in line with the OAuth 2.0 specification in this respect.

Fixes T11794

Test Plan

Fixes my use case. Shouldn't impact other use-cases.

Diff Detail

Repository
rP Phabricator
Branch
master
Lint
Lint Passed
Unit
Tests Passed
Build Status
Buildable 14281
Build 18577: arc lint + arc unit

Event Timeline

wrl retitled this revision from to oauthserver: get client ID/secret from HTTP auth.
wrl updated this object.
wrl edited the test plan for this revision. (Show Details)
wrl added a reviewer: epriestley.
wrl edited edge metadata.
epriestley edited edge metadata.

This allows weird mixes of client_secret + AUTH_USER -- let's do something like this instead (provided the code actually works)?

$client_phid = $request->getStr('client_id');
$client_secret = $request->getStr('client_secret');
if (!strlen($client_phid) && !strlen($client_secret)) {
  $client_phid = idx($_SERVER, 'PHP_AUTH_USER');
  $client_secret = idx($_SERVER, 'PHP_AUTH_PW');
}

That is:

  • If either parameter is present, use the HTTP parameters only. This makes requests with an "obvious" client_secret or client_id always do the more obvious thing, so no one will ever report a bug where they run curl ...&client_secret=x and get some unrelated/bizarre failure because they have a weird ~/.curlrc or something.
  • Require both parameters to come from a single source, so callers can't mix things, which I think would just be weird/confusing/unhelpful in all cases.
This revision now requires changes to proceed.Oct 29 2016, 12:36 AM

Sure, works for me, but I'm going to invert your logic (prefer AUTH header, fallback to request parameters).

According to the spec (https://tools.ietf.org/html/rfc6749#section-3.2.1):

A client MAY use the "client_id" request parameter to identify itself when sending requests to the token endpoint. In the "authorization_code" "grant_type" request to the token endpoint, an unauthenticated client MUST send its "client_id" to prevent itself from inadvertently accepting a code intended for a client with a different "client_id".

So, client_id will always be present as a request param.

wrl edited edge metadata.

Use either HTTP auth or request parameters, not a potential mix of the two.

epriestley edited edge metadata.
epriestley added a subscriber: 0.

I explicitly want to prefer the parameters, not the auth header.

A scenario I can imagine where both are present might be something like this:

user: "I ran curl http://example.phabricator.com/api/whatever?client_id=x&client_secret=y but it says my client_id and client_secret are invalid! I'm sure they're right! Help! Your software is super broken!"
me: <I spend hours debugging>
me: Turns out you configured your environment to send an "Authorization" header and didn't tell me about it. Delete that config.
user: wow that worked
me: ...

This specific scenario may never occur, but I can't imagine any scenario where both are present and the "Authorization" header is explicit while the parameters are implicit. In contrast, I can easily imagine scenarios were the "Authorization" header is implicit (via a mechanism like ~/.netrc). Preferring parameters guarantees I'll never end up debugging this kind of config mess.


The use of strlen() is also important. In PHP, the string "0" is falsey:

$ php -r 'echo "0" ? "truthy\n" : "falsey\n";'
falsey

This means that ?client_id=0&client_secret=0 will be incorrectly ignored without the strlen() tests. Although these are probably not valid IDs/secrets, they can lead to a misleading error message (e.g., "client_secret not present", when the correct error is "client_secret present but not valid").

(Similar issues have occurred in the wild in the past with user @0, who is a real user, also has user account https://github.com/0, and has contributed to Phabricator.)


If the Go API sends client_id as both a request parameter and an "Authorization" header, we should require that they agree. Something in this vein:

$client_id_parameter = $request->getStr('client_id');
$client_id_header = idx($_SERVER, 'PHP_AUTH_USER');
if (strlen($client_id_parameter) && strlen($client_id_header)) {
  if ($client_id_parameter !== $client_id_header) {
    throw new Exception(
      pht(
        'Request included a client_id parameter and an "Authorization" header with '.
        'a username, but the values ("%s" and "%s") disagree. The values must match.',
        $client_id_parameter,
        $client_id_header));
  }
}

$client_secret_parameter = $request->getStr('client_secret');
$client_secret_header = idx($_SERVER, 'PHP_AUTH_PW');
if (strlen($client_secret_parameter)) {
  // If the `client_secret` parameter is present, prefer parameters.
  $client_id = $client_id_parameter;
  $client_secret = $client_secret_parameter;
} else {
  // Otherwise, read values from the "Authorization"  header.
  $client_id = $client_id_header;
  $client_secret = $client_secret_header;
}
This revision now requires changes to proceed.Oct 30 2016, 5:57 AM
wrl edited edge metadata.

Applied epriestley's recommendations

Done. Thanks for the strlen() tip as well, been a while since I've written any real amount of PHP.

This revision is now accepted and ready to land.Oct 31 2016, 3:21 PM
This revision was automatically updated to reflect the committed changes.

(I adjusted formatting slightly in the pull for project conventions, let me know if I broke anything by accident.)

Just pulled and tested and everything works fine.

Thanks!