Repository: gratipay/gratipay.com

Issue:
use uuid instead of username for Bitbucket
**Update:** Bitbucket recently rolled out `uuid`s for their user API, so we can switch to that instead of dropping them. See [below](https://github.com/gratipay/gratipay.com/issues/1945#issuecomment-62242306).

---

In the long run we're considering dropping all authentication via OAuth and only allowing password authentication (#1052). In the short run we need to drop Bitbucket as an auth method, because Bitbucket gives us no immutable user id, so if someone deletes their Bitbucket account, an attacker could create a Bitbucket account with their username and take over their Gittip account. @bruceadams demonstrated this here: https://github.com/gittip/www.gittip.com/issues/1807#issuecomment-33314863.

<bountysource-plugin>

---

Want to back this issue? **[Place a bounty on it!](https://www.bountysource.com/issues/1387030-drop-bitbucket-as-auth-method?utm_campaign=plugin&utm_content=tracker%2F85909&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F85909&utm_medium=issues&utm_source=github).
</bountysource-plugin>



Discussion:
Agreed. I will work on collecting data about how many Gittip user's will be impacted and in what ways.

Any idea about how to approach this? We have in general no way of
contacting the affected users. Disabling bitbucket just for new users will
leave our UI in a strange state because we would have to show the UI to
sign in to all users and detect and differentiate opting in and say "sorry,
not for new users". It would also leave the current users in a potentially
vulnerable state. Disabling it for everyone would leave some users unable
to sign in.

Bitbucket asks users for their email addresses. I don't know if we can get those addresses. I want data on how bad the problem is. How many users are only setup with Bitbucket? Of those, how many have tips? how many have a balance? If the list of people is small, we can try to track those people down one at a time. If the list is long, we may have to take a batch approach, possibly kicking out users :frowning:.

Some basic Gittip numbers for BitBucket OAuth.

| Count | Description |
| --: | --- |
| 3031 | Total Gittip accounts with BitBucket OAuth setup |
| 1181 | BitBucket only |
| 33 | the related BitBucket user does not exist |

What about active users? (either giving or receiving)

I haven't gotten to active users. Of the 33 with OAuth tied to a nonexistent BitBucket user, 11 have no other OAuth set and 22 do have another OAuth setup. I'm going just blow away those 22 ties, since that appears to be a safe thing to do. (Once I've cleared the links, I'll publish that list here.)

I just disconnected these 22 ties to BitBucket accounts that do not exist:

| elsewhere id | BitBucket user | Gittip user |
| --: | --- | --- |
| 129618 | aeloi | joshschmelzle |
| 117714 | Apathetic | Apathetic012 |
| 71195 | beresovskiy | beresovskiy |
| 198619 | davidhurley | dbhurley |
| 112860 | duke_ | dukex |
| 102587 | iekechukwu | kwuchu |
| 216689 | jarobins | jarobins |
| 84431 | jksk | jespr |
| 289655 | jolaq | djolaq |
| 315458 | jonahoffline | jonahoffline |
| 126040 | kylescottmcgill | kylescottmcgill |
| 268658 | MaddinXx | MaddinXx |
| 125868 | montyman100 | montyjanderson |
| 325438 | mortred | kerimdzhanov |
| 172766 | nkistner | wyze |
| 126198 | pooh110andco | ale110 |
| 175176 | rnh16 | rhamzeh |
| 126932 | ronnyspringer | ronny-springer |
| 71780 | sikaondrej | sikaondrej |
| 277196 | theylive | theylive |
| 121852 | tunixman | computionist |
| 110380 | wanbokchoi | wanbok |

1181 does not seem like a number we can contact by hand :frowning:.
@bruceadams Were you able to find out if we have an access to the email on
Bitbucket?

The Bitbucket API does not give out an email, but then, it doesn't give out account creation date either, when their web site does. I haven't dug around on their web site to see if I might be able to find email addresses there. Git commits will have email addresses, but it may not always be clear which commit (and email addresses) are associated with an account owner and some accounts may not have any commits.

The eleven Gittip users which only have OAuth to a nonexistent Bitbucket user have never had incoming tips nor outgoing tips and no Stripe or Balanced financial tie setup. So, these users present no risk, they are only messy.

There is a possibility to "Send message" once you are logged in the
bitbucket website. No email to be seen anywhere. It seems it should be
possible to script this using requests http://docs.python-requests.org/.
Any better ideas? We can also show depreciation message to all users with
bitbucket only while on the gittip website.

I also am wondering if we just should drop the whole bitbucket
implementation. What would it mean when an bitbucket account is associated
to a gittip account but the bitbucket account is deleted? Or when someone
creates a new account with the same name? We are not in the "stealing
money" situation anymore but still, we have "this is confusing, not
trustful" situation which is also bad.

On 28 January 2014 11:48, Bruce Adams notifications@github.com wrote:

> The eleven Gittip users which only have OAuth to a nonexistent Bitbucket
> user have never had incoming tips nor outgoing tips and no Stripe or
> Balanced financial tie setup. So, these users present no risk, they are
> only messy.
> 
> —
> Reply to this email directly or view it on GitHubhttps://github.com/gittip/www.gittip.com/issues/1945#issuecomment-33467936
> .

@bruceadams What are your thoughts on the way forward with this? I am willing to try the "send message" thing to notify bitbucket users (to attach another account). The next thing would be #900.

BTW: Bitbucket allows renames (nkistner is now most likely wyze but we have now way of confirming).

Everyone's periodic reminder that Bitbucket auth is unfixably broken, and that we need to remove it.

Top.

I've added the `security` label.

This has been open long enough that we need to go through the exercise again.

Some basic Gratipay numbers for Bitbucket OAuth.

| Count | Description | SQL |
| --: | --- | --- |
| 4319 | Total Gratipay accounts with Bitbucket OAuth setup | `select count(*) from elsewhere where platform='bitbucket';` |
| 2799 | Not Bitbucket only | `select count(*) from (select e1.user_name from elsewhere e1 join elsewhere e2 on e1.participant=e2.participant where e1.platform='bitbucket' and e2.platform in ('twitter', 'github', 'facebook', 'google', 'openstreetmap') group by e1.user_name) as foo;` |
| 1520 | Bitbucket only | `select 4319 - 2799;` |
| ? | the related Bitbucket user does not exist |  |

We're going to have to drop some users entirely. Anyone with Bitbucket as their only signin method will no longer have a Gratipay account.

We will drop some people who are giving through Gratipay.

We will drop some people who are receiving through Gratipay.

The real hard part will be dropping people who have a balance on Gratipay.

We should try our best to notify people who are giving, receiving, and/or have a balance on Gratipay. We can do this by trying to find emails for people, and then beyond that we'll just have to depend on a blog post about it.

Question: Are we going to keep Bitbucket as a non-sign-in auth method? I say no. With no immutable ID there's no question but that those connections are going to degrade over time as people change their Bitbucket usernames. I say we drop Bitbucket entirely.

So how many people in each category are we going to have to drop?

| Category | Number |
| --- | --- |
| only giving | 14 |
| only receiving | 1 |
| giving and receiving | 0 |
| has a balance | 28 |

We have 39 users that are either giving or receiving or have a balance, and can only sign in using Bitbucket.

``` sql
     select e1.platform, e1.user_name, p.username, p.balance, p.receiving, p.giving                     
       from elsewhere e1                                                                                
  left join elsewhere e2                                                                                
         on e1.participant = e2.participant                                                             
        and e1.platform = 'bitbucket'                                                                   
        and e2.platform in ('twitter', 'github', 'facebook', 'google', 'openstreetmap')                 
       join participants p                                                                              
         on e1.participant = p.username                                                                 

      where e1.platform = 'bitbucket'                                                                   
        and e2.platform is null                                                                         
        and (p.balance + p.receiving + p.giving) > 0                                                    

   order by p.balance desc                                                                              
           ;
```

36 if we exclude suspicious accounts.

``` sql
drop table if exists only_bitbucket;
create temp table only_bitbucket as (

     select e1.platform, e1.user_name, p.username, p.balance, p.receiving, p.giving
       from elsewhere e1
  left join elsewhere e2
         on e1.participant = e2.participant
        and e1.platform = 'bitbucket'
        and e2.platform in ('twitter', 'github', 'facebook', 'google', 'openstreetmap')
       join participants p
         on e1.participant = p.username
        and p.is_suspicious is not true

      where e1.platform = 'bitbucket'
        and e2.platform is null
        and (p.balance + p.receiving + p.giving) > 0

   order by p.balance desc
);
select * from only_bitbucket;
```

30 if we exclude unclaimed accounts:

``` sql
drop table if exists only_bitbucket;
create temp table only_bitbucket as (
     select e1.platform, e1.user_name, p.username, p.balance, p.receiving, p.giving
       from elsewhere e1
  left join elsewhere e2
         on e1.participant = e2.participant
        and e1.platform = 'bitbucket'
        and e2.platform in ('twitter', 'github', 'facebook', 'google', 'openstreetmap')
       join participants p
         on e1.participant = p.username
        and p.is_suspicious is not true
        and p.claimed_time is not null

      where e1.platform = 'bitbucket'
        and e2.platform is null
        and (p.balance + p.receiving + p.giving) > 0

   order by p.balance desc
);
select * from only_bitbucket;
```

```
┌─────────┐
│ balance │
├─────────┤
│   30.00 │
│    9.16 │
│    9.05 │
│    8.82 │
│    8.64 │
│    7.87 │
│    7.23 │
│    5.73 │
│    4.82 │
│    4.55 │
│    4.44 │
│    4.05 │
│    3.60 │
│    2.45 │
│    1.81 │
│    1.71 │
│    1.64 │
│    1.64 │
│    1.51 │
│    1.46 │
│    0.82 │
│    0.82 │
│    0.41 │
│    0.32 │
│    0.23 │
│    0.21 │
│    0.20 │
│    0.16 │
└─────────┘
┌─────────┐
│   sum   │
├─────────┤
│  123.35 │
└─────────┘
```

So how do we clear balances for these 28? That's the hardest question.

Twelve users with a balance also have tips set up. For them we can distribute their balance as a final gift before closing their account.

The user with the $30 balance appears to have a second Gratipay account that he was using to test out the system, I guess? I think there we can one-off the tips back upstream to his other account. I'll reach out to him to confirm.

That leaves 15 users who have a balance and no tips set up. Can we refund to their credit cards? Of these, seven have a balance less than $1.

BTW, I did google around and check Bitbucket's [API docs](https://confluence.atlassian.com/display/BITBUCKET/users+Endpoint). It appears that they still don't have a mutable ID.

Let's see if any have a bank account and/or credit card and/or PayPal attached:

``` sql
drop table if exists only_bitbucket;
create temp table only_bitbucket as (
     select p.username, p.balance, p.receiving, p.giving, p.last_ach_result, p.last_bill_result, p.paypal_email
       from elsewhere e1
  left join elsewhere e2
         on e1.participant = e2.participant
        and e1.platform = 'bitbucket'
        and e2.platform in ('twitter', 'github', 'facebook', 'google', 'openstreetmap')
       join participants p
         on e1.participant = p.username
        and p.is_suspicious is not true
        and p.claimed_time is not null

      where e1.platform = 'bitbucket'
        and e2.platform is null
        and (p.balance + p.receiving + p.giving) > 0

   order by p.balance desc
);
select * from only_bitbucket;
```

No bank accounts, no PayPal. Some credit cards.

Four affected users who have a balance and no tips set up do have a working credit card on file.

That leaves 12 users with a balance, no tips, and no funding instrument attached.

```
┌─────────┐
│ balance │
├─────────┤
│    8.82 │
│    4.55 │
│    3.60 │
│    2.45 │
│    1.46 │
│    0.82 │
│    0.41 │
│    0.32 │
│    0.23 │
│    0.21 │
│    0.20 │
│    0.16 │
└─────────┘
┌─────────┐
│    sum  │
├─────────┤
│   23.23 │
└─────────┘
```

I guess we can also try upstreaming balances.

The general problem here is, how do we close user accounts? We have DIY account closing, but there are situations where _we_ want to forcibly close an account: reticketed as #2909.

All 30 affected users with a balance do have a Bitbucket account by the `user_name` we have on file for them: whether or not it's truly them is another matter. :-/

If reaching out fails:
- Do a blog post, and wait a few weeks.
- Failing that, send the money back to givers (to their credit cards).
- Failing that, take the remaining cash to team Gratipay.

It does feel like a lot of effort for little money though.

Just wondering instead of forcibly deleting the accounts can we leave them and do a sort of "force password change" the next time that the user logs into the account with the Bitbucket auth. Thats is to say that they have to provide another method of authentication (twitter, google, etc) once they try to access the account with their BitBucket credential. Just a suggestion, cause I find it hard to delete someone's account (not matter how small their balance) for something that is not their fault.  

@kaguillera Quoting @zwn [above](https://github.com/gratipay/gratipay.com/issues/1945#issuecomment-33422958):

> Disabling bitbucket just for new users will leave our UI in a strange state because we would have to show the UI to sign in to all users and detect and differentiate opting in and say "sorry, not for new users". It would also leave the current users in a potentially vulnerable state.

Remember that this is actually a security issue. The longer we leave this open, the more likely it is that an account will be compromised.

There are 599 Gratipay accounts with Bitbucket attached that either have a balance or are receiving. Those are all vulnerable.

Hey @erikvanzijst, wanted to give you a heads-up on this: We're planning to remove Bitbucket support from Gratipay (née Gittip), because of the lack of an immutable user id in the Bitbucket API. Right now we're using username as the key to a Bitbucket account. But if a Gratipay user with Bitbucket attached changes their Bitbucket username, it's possible to make a Bitbucket account using their old username and thereby take over their Gratipay account. If you have any workarounds to suggest, please speak up soon, as the gears are in motion to remove Bitbucket support entirely (#2912).

API 2.0 `created_on` may be the immutable user that we need https://confluence.atlassian.com/display/BITBUCKET/users+Endpoint

`created_on` is just a timestamp: not to be trusted as a unique identifier (despite the docs). However, it looks like there is an undocumented `uuid` key that presumably is what we want:

![screen shot 2014-11-08 at 8 23 01 am](https://cloud.githubusercontent.com/assets/134455/4964243/9beab88a-6720-11e4-8adc-4ab2a5a769ce.png)

Can you confirm, @erikvanzijst?

I searched for `uuid` on https://confluence.atlassian.com/ using both Google and site search, and found no results.

Indeed, we added that a few weeks ago, but our doco is lagging behind.

That is a public field that you should use. It's on repos too (though you wouldn't care as much about that I'd think).

You can substitute usernames and uuids in URLs. These URLs are equivalent:

https://api.bitbucket.org/2.0/users/evzijst
https://api.bitbucket.org/2.0/users/{a288a0ab-e13b-43f0-a689-c4ef0a249875}

The curly braces let us distinguish between a UUID string and a username that looks like one. Curly braces are illegal in usernames (you will have to urlencode them, which I omitted in my previous comment for clarity).

Awesome, thanks @erikvanzijst! Congrats on rolling out `uuid`s, we'll pursue that route. :-)

Here's a little script to take a list of usernames on stdin and spit out `{200,404} username` on stdout. It's useful to get an idea of how often people change their Bitbucket usernames, as @bruceadams did [above](https://github.com/gratipay/gratipay.com/issues/1945#issuecomment-33442546).

``` python
import requests, sys

for line in sys.stdin:
    username = line.strip()
    response = requests.get('https://bitbucket.org/api/2.0/users/' + username)
    print response.status_code, username
    sys.stdout.flush()
```

We have 4,278 non-team Bitbucket accounts on file. Of these 4,211 are `200` and 67 (1.6%) are `404`, indicating a changed username. We can't tell (of course) how many of the `200`s are accounts that have changed `uuid`s since we filed them.

Okay! Here's how I suggest we proceed:
1. Update our Bitbucket integration to key off of `uuid` instead of `username` for new Bitbucket elsewheres (natch).
2. Drop Bitbucket for the 2,808 accounts that also have another elsewhere attached. We have no guarantee that the Bitbucket account is indeed theirs. The risk of a false positive is high (there's $21,749.45 in 569 affected accounts), and the risk of a false negative is low (they can easily reconnect Bitbucket). [Note: make sure the other account elsewhere is not `is_team`!]
3. For the 1,521 accounts that are only linked to Bitbucket, I propose that we link it to whatever Bitbucket `uuid` is currently associated with the Bitbucket `username` we have on file. If a given such account doesn't have a balance/giving/receiving, then it doesn't matter whether someone has compromised their account, because their Gratipay account is now functionally equivalent to the account that any attacker could have created using the Bitbucket username that had been let go. There are only 36 non-suspicious accounts that _do_ have a balance/giving/receiving (6 are unclaimed). The chance of a false positive is low: 1.6% of 36 is roughly 0. The _risk_ associated with a false positive is also low: there's only $123.35 in balance and $3.40/wk in receiving across these accounts. On the other hand, forcibly closing these accounts is both difficult (#2909), and heavy-handed. We should make a good-faith effort beyond just a blog post to notify the 36 affected users.

> then it doesn't matter whether someone has compromised their account

We should also look at whether a Bitbucket-connected-account has a funding instrument attached (credit card, bank account, PayPal, bitcoin).

Of the 1,521 Bitbucket-only Gratipay accounts, there are five with bitcoin addresses on file, one with PayPal, two with bank accounts (both working), and 34 with credit cards (24 working). That's 42 accounts total (there's no overlap amongst those four categories).

Nine of the 1,521 have already been closed by the account owner.

Should we put some sort of notice under "Connected Accounts" with a link to our blog post about this? It would only be relevant for people who had another account connected.


Resolving patch:
diff --git a/gratipay/elsewhere/__init__.py b/gratipay/elsewhere/__init__.py
--- a/gratipay/elsewhere/__init__.py
+++ b/gratipay/elsewhere/__init__.py
@@ -117,7 +117,7 @@ def api_get(self, path, sess=None, **kw):
         # Check status
         status = response.status_code
         if status == 404:
-            raise Response(404)
+            raise Response(404, response.text)
         elif status != 200:
             log('{} api responded with {}:\n{}'.format(self.name, status, response.text)
                , level=logging.ERROR)
diff --git a/gratipay/elsewhere/bitbucket.py b/gratipay/elsewhere/bitbucket.py
--- a/gratipay/elsewhere/bitbucket.py
+++ b/gratipay/elsewhere/bitbucket.py
@@ -1,5 +1,6 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+from aspen import Response
 from gratipay.elsewhere import PlatformOAuth1
 from gratipay.elsewhere._extractors import any_key, key, not_available
 from gratipay.elsewhere._paginators import keys_paginator
@@ -20,15 +21,31 @@ class Bitbucket(PlatformOAuth1):
     api_format = 'json'
     api_paginator = keys_paginator(prev='previous')
     api_url = 'https://bitbucket.org/api'
-    api_user_info_path = '/1.0/users/{user_name}'
-    api_user_self_info_path = '/1.0/user'
+    api_user_info_path = '/2.0/users/{user_name}'
+    api_user_self_info_path = '/2.0/user'
     api_team_members_path = '/2.0/teams/{user_name}/members'
 
     # User info extractors
     x_user_info = key('user')
-    x_user_id = not_available  # No immutable id. :-/
+    x_user_id = key('uuid')
     x_user_name = key('username')
     x_display_name = key('display_name')
     x_email = not_available
     x_avatar_url = any_key('avatar', ('links', 'avatar', 'href'))
-    x_is_team = key('is_team')
+    x_is_team = key('type', lambda v: v == 'team')
+
+    def api_get(self, path, sess=None, **kw):
+        """Extend to manually retry /users/pypy as /teams/pypy.
+
+        Bitbucket gives us a 404 where a 30x would be more helpful.
+
+        """
+        try:
+            return PlatformOAuth1.api_get(self, path, sess, **kw)
+        except Response, response:
+            if response.code == 404 and ' is a team account' in response.body:
+                assert path.startswith('/2.0/users/')
+                path = '/2.0/teams/' + path[11:]
+                return PlatformOAuth1.api_get(self, path, sess, **kw)
+            else:
+                raise


Test patch:
diff --git a/gratipay/testing/elsewhere.py b/gratipay/testing/elsewhere.py
--- a/gratipay/testing/elsewhere.py
+++ b/gratipay/testing/elsewhere.py
@@ -10,101 +10,33 @@
 import xml.etree.ElementTree as ET
 
 bitbucket = lambda: {
-    "repositories": [
-        {
-            "scm": "hg",
-            "has_wiki": True,
-            "last_updated": "2012-03-16T23:36:38.019",
-            "no_forks": None,
-            "created_on": "2012-03-16T23:34:46.740",
-            "owner": "whit537",
-            "logo": "https://d3oaxc4q5k2d6q.cloudfront.net/m/6fac1fb24100/img/language-avatars/default_16.png",
-            "email_mailinglist": "",
-            "is_mq": False,
-            "size": 142818,
-            "read_only": False,
-            "fork_of": {
-                "scm": "hg",
-                "has_wiki": True,
-                "last_updated": "2014-02-01T03:41:46.920",
-                "no_forks": None,
-                "created_on": "2010-07-17T16:12:34.381",
-                "owner": "jaraco",
-                "logo": "https://d3oaxc4q5k2d6q.cloudfront.net/m/6fac1fb24100/img/language-avatars/python_16.png",
-                "email_mailinglist": "",
-                "is_mq": False,
-                "size": 316601,
-                "read_only": False,
-                "creator": None,
-                "state": "available",
-                "utc_created_on": "2010-07-17 14:12:34+00:00",
-                "website": "",
-                "description": "Inspried by jezdez.setuptools_hg, and building on that work, hgtools provides tools for developing with mercurial and setuptools/distribute (specifically a file-finder plugin and automatic repo tag versioning).\r\n\r\nThe underlying library is designed to be extensible for other applications to build other functionality that depends on mercurial, whether using the 'hg' command or the mercurial libraries directly.",
-                "has_issues": True,
-                "is_fork": True,
-                "slug": "hgtools",
-                "is_private": False,
-                "name": "hgtools",
-                "language": "python",
-                "utc_last_updated": "2014-02-01 02:41:46+00:00",
-                "email_writers": True,
-                "no_public_forks": False,
-                "resource_uri": "/1.0/repositories/jaraco/hgtools"
-            },
-            "mq_of": {
-                "scm": "hg",
-                "has_wiki": True,
-                "last_updated": "2014-02-01T03:41:46.920",
-                "no_forks": None,
-                "created_on": "2010-07-17T16:12:34.381",
-                "owner": "jaraco",
-                "logo": "https://d3oaxc4q5k2d6q.cloudfront.net/m/6fac1fb24100/img/language-avatars/python_16.png",
-                "email_mailinglist": "",
-                "is_mq": False,
-                "size": 316601,
-                "read_only": False,
-                "creator": None,
-                "state": "available",
-                "utc_created_on": "2010-07-17 14:12:34+00:00",
-                "website": "",
-                "description": "Inspried by jezdez.setuptools_hg, and building on that work, hgtools provides tools for developing with mercurial and setuptools/distribute (specifically a file-finder plugin and automatic repo tag versioning).\r\n\r\nThe underlying library is designed to be extensible for other applications to build other functionality that depends on mercurial, whether using the 'hg' command or the mercurial libraries directly.",
-                "has_issues": True,
-                "is_fork": True,
-                "slug": "hgtools",
-                "is_private": False,
-                "name": "hgtools",
-                "language": "python",
-                "utc_last_updated": "2014-02-01 02:41:46+00:00",
-                "email_writers": True,
-                "no_public_forks": False,
-                "resource_uri": "/1.0/repositories/jaraco/hgtools"
-            },
-            "state": "available",
-            "utc_created_on": "2012-03-16 22:34:46+00:00",
-            "website": None,
-            "description": "I'm forking to fix another bug case in issue #7.",
-            "has_issues": True,
-            "is_fork": True,
-            "slug": "hgtools",
-            "is_private": False,
-            "name": "hgtools",
-            "language": "",
-            "utc_last_updated": "2012-03-16 22:36:38+00:00",
-            "email_writers": True,
-            "no_public_forks": False,
-            "creator": None,
-            "resource_uri": "/1.0/repositories/whit537/hgtools"
+    "username": "whit537",
+    "website": "https://www.gittip.com/whit537/",
+    "display_name": "Chad Whitacre",
+    "uuid": "{59efeb39-29dc-415e-959e-3cb1ea7f579b}",
+    "links": {
+        "self": {
+            "href": "https://bitbucket.org/api/2.0/users/whit537"
+        },
+        "repositories": {
+            "href": "https://bitbucket.org/api/2.0/repositories/whit537"
+        },
+        "html": {
+            "href": "https://bitbucket.org/whit537"
+        },
+        "followers": {
+            "href": "https://bitbucket.org/api/2.0/users/whit537/followers"
+        },
+        "avatar": {
+            "href": "https://secure.gravatar.com/avatar/5698bc43665106a28833ef61c8a9f67f?d=https%3A%2F%2Fd3oaxc4q5k2d6q.cloudfront.net%2Fm%2F5fe8c0346b2d%2Fimg%2Fdefault_avatar%2F32%2Fuser_blue.png&s=32"
+        },
+        "following": {
+            "href": "https://bitbucket.org/api/2.0/users/whit537/following"
         }
-    ],
-    "user": {
-        "username": "whit537",
-        "first_name": "Chad",
-        "last_name": "Whitacre",
-        "display_name": "Chad Whitacre",
-        "is_team": False,
-        "avatar": "https://secure.gravatar.com/avatar/5698bc43665106a28833ef61c8a9f67f?d=https%3A%2F%2Fd3oaxc4q5k2d6q.cloudfront.net%2Fm%2F6fac1fb24100%2Fimg%2Fdefault_avatar%2F32%2Fuser_blue.png&s=32",
-        "resource_uri": "/1.0/users/whit537"
-    }
+    },
+    "created_on": "2012-01-23T20:11:10.736097+00:00",
+    "location": "Pittsburgh, PA USA",
+    "type": "user"
 }
 
 bountysource = lambda: {