incubator-allura-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From john...@apache.org
Subject git commit: [#6692] Added support for OAuth2 Bearer Tokens
Date Thu, 26 Sep 2013 14:50:39 GMT
Updated Branches:
  refs/heads/cj/6692 d5902b409 -> cee549119 (forced update)


[#6692] Added support for OAuth2 Bearer Tokens

Support for RFC-6750: OAuth 2.0: Bearer Token Usage.
See http://self-issued.info/docs/draft-ietf-oauth-v2-bearer.html
for more information.  Doesn't support the Authenticate header.

Bearer tokens can only be generated manually by the user;
tokens generated via the existing OAuth negotation cannot be
used as bearer tokens.

Also consolidated the access token revocation form into the
consumer token generation page, and made it its own entry in
the auth menu.

Signed-off-by: Cory Johns <cjohns@slashdotmedia.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/cee54911
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/cee54911
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/cee54911

Branch: refs/heads/cj/6692
Commit: cee549119c1597401aa9b2382657d0b34ccd6f34
Parents: 2d5a6ab
Author: Cory Johns <cjohns@slashdotmedia.com>
Authored: Wed Sep 25 23:23:47 2013 +0000
Committer: Cory Johns <cjohns@slashdotmedia.com>
Committed: Thu Sep 26 14:49:28 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py               |  68 +++++++++-
 Allura/allura/controllers/rest.py               |  19 ++-
 Allura/allura/lib/plugin.py                     |   6 +
 Allura/allura/model/oauth.py                    |   3 +-
 Allura/allura/templates/oauth_applications.html | 134 ++++++++++++++++---
 Allura/allura/templates/oauth_authorize.html    |  14 +-
 Allura/allura/templates/user_subs.html          |  11 --
 Allura/allura/tests/functional/test_rest.py     |  26 ++++
 8 files changed, 241 insertions(+), 40 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/cee54911/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index fa00208..9f00f18 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -732,7 +732,6 @@ class SubscriptionsController(BaseController):
         menu = provider.account_navigation()
         return dict(
             subscriptions=subscriptions,
-            authorized_applications=M.OAuthAccessToken.for_user(c.user),
             menu=menu)
 
     @h.vardec
@@ -756,26 +755,33 @@ class SubscriptionsController(BaseController):
 
 class OAuthController(BaseController):
 
+    def _check_security(self):
+        require_authenticated()
+
     @with_trailing_slash
     @expose('jinja:allura:templates/oauth_applications.html')
     def index(self, **kw):
-        require_authenticated()
         c.form = F.oauth_application_form
-        return dict(apps=M.OAuthConsumerToken.for_user(c.user))
+        consumer_tokens = M.OAuthConsumerToken.for_user(c.user)
+        access_tokens = M.OAuthAccessToken.for_user(c.user)
+        provider = plugin.AuthenticationProvider.get(request)
+        return dict(
+                menu=provider.account_navigation(),
+                consumer_tokens=consumer_tokens,
+                access_tokens=access_tokens,
+            )
 
     @expose()
     @require_post()
     @validate(F.oauth_application_form, error_handler=index)
     def register(self, application_name=None, application_description=None, **kw):
-        require_authenticated()
         M.OAuthConsumerToken(name=application_name, description=application_description)
         flash('OAuth Application registered')
         redirect('.')
 
     @expose()
     @require_post()
-    def delete(self, id=None):
-        require_authenticated()
+    def deregister(self, id=None):
         app = M.OAuthConsumerToken.query.get(_id=bson.ObjectId(id))
         if app is None:
             flash('Invalid app ID', 'error')
@@ -783,6 +789,56 @@ class OAuthController(BaseController):
         if app.user_id != c.user._id:
             flash('Invalid app ID', 'error')
             redirect('.')
+        M.OAuthRequestToken.query.remove({'consumer_token_id': app._id})
+        M.OAuthAccessToken.query.remove({'consumer_token_id': app._id})
         app.delete()
         flash('Application deleted')
         redirect('.')
+
+    @expose()
+    @require_post()
+    def generate_access_token(self, id, name=None):
+        """
+        Manually generate an OAuth access token for the given consumer.
+
+        NB: Manually generated access tokens are bearer tokens, which are
+        less secure (since they rely only on the token, which is transmitted
+        with each request, unlike the access token secret).
+        """
+        consumer_token = M.OAuthConsumerToken.query.get(_id=bson.ObjectId(id))
+        if consumer_token is None:
+            flash('Invalid app ID', 'error')
+            redirect('.')
+        if consumer_token.user_id != c.user._id:
+            flash('Invalid app ID', 'error')
+            redirect('.')
+        request_token = M.OAuthRequestToken(
+                consumer_token_id=consumer_token._id,
+                user_id=c.user._id,
+                callback='manual',
+                validation_pin=h.nonce(20),
+                name=name,
+                is_bearer=True,
+            )
+        access_token = M.OAuthAccessToken(
+                consumer_token_id=consumer_token._id,
+                request_token_id=c.user._id,
+                user_id=request_token.user_id,
+                name=name,
+                is_bearer=True,
+            )
+        redirect('.')
+
+    @expose()
+    @require_post()
+    def revoke_access_token(self, id):
+        access_token = M.OAuthAccessToken.query.get(_id=bson.ObjectId(id))
+        if access_token is None:
+            flash('Invalid token ID', 'error')
+            redirect('.')
+        if access_token.user_id != c.user._id:
+            flash('Invalid token ID', 'error')
+            redirect('.')
+        access_token.delete()
+        flash('Token revoked')
+        redirect('.')

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/cee54911/Allura/allura/controllers/rest.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/rest.py b/Allura/allura/controllers/rest.py
index d1b6cb1..e97bdac 100644
--- a/Allura/allura/controllers/rest.py
+++ b/Allura/allura/controllers/rest.py
@@ -45,7 +45,7 @@ class RestController(object):
 
     def _authenticate_request(self):
         'Based on request.params or oauth, authenticate the request'
-        if 'oauth_token' in request.params:
+        if 'oauth_token' in request.params or 'access_token' in request.params:
             return self.oauth._authenticate()
         elif 'api_key' in request.params:
             api_key = request.params.get('api_key')
@@ -115,6 +115,15 @@ class OAuthNegotiator(object):
         return result
 
     def _authenticate(self):
+        if 'access_token' in request.params:
+            # handle bearer tokens
+            if request.scheme != 'https':
+                raise exc.HTTPForbidden
+            access_token = M.OAuthAccessToken.query.get(
+                api_key=request.params['access_token'])
+            if not (access_token and access_token.is_bearer):
+                raise exc.HTTPForbidden
+            return access_token
         req = oauth.Request.from_request(
             request.method,
             request.url.split('?')[0],
@@ -233,9 +242,11 @@ class OAuthNegotiator(object):
             log.error('Invalid signature')
             return None
         acc_token = M.OAuthAccessToken(
-            consumer_token_id=consumer_token._id,
-            request_token_id=request_token._id,
-            user_id=request_token.user_id)
+                consumer_token_id=consumer_token._id,
+                request_token_id=request_token._id,
+                user_id=request_token.user_id,
+                name=request_token.name,
+            )
         return acc_token.to_string()
 
 class NeighborhoodRestController(object):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/cee54911/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 8e24cb2..e8bff17 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -172,6 +172,12 @@ class AuthenticationProvider(object):
                 'target': "/auth/subscriptions",
                 'alt': 'Manage Subscription Preferences',
             },
+            {
+                'tabid': 'account_oauth',
+                'title': 'OAuth',
+                'target': "/auth/oauth",
+                'alt': 'Manage OAuth Preferences',
+            },
         ]
 
     def user_project_shortname(self, user):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/cee54911/Allura/allura/model/oauth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/oauth.py b/Allura/allura/model/oauth.py
index c235984..5f9344c 100644
--- a/Allura/allura/model/oauth.py
+++ b/Allura/allura/model/oauth.py
@@ -96,6 +96,7 @@ class OAuthAccessToken(OAuthToken):
     consumer_token_id = ForeignIdProperty('OAuthConsumerToken')
     request_token_id = ForeignIdProperty('OAuthToken')
     user_id = ForeignIdProperty('User', if_missing=lambda:c.user._id)
+    is_bearer = FieldProperty(bool, if_missing=False)
 
     user = RelationProperty('User')
     consumer_token = RelationProperty('OAuthConsumerToken', via='consumer_token_id')
@@ -104,5 +105,5 @@ class OAuthAccessToken(OAuthToken):
     @classmethod
     def for_user(cls, user=None):
         if user is None: user = c.user
-        return cls.query.find(dict(user_id=user._id)).all()
+        return cls.query.find(dict(user_id=user._id, type='access')).all()
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/cee54911/Allura/allura/templates/oauth_applications.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/oauth_applications.html b/Allura/allura/templates/oauth_applications.html
index f31c27e..75b1436 100644
--- a/Allura/allura/templates/oauth_applications.html
+++ b/Allura/allura/templates/oauth_applications.html
@@ -23,20 +23,124 @@
 
 {% block header %}OAuth applications registered for {{c.user.username}}{% endblock %}
 
+{% block extra_css %}
+<style type="text/css">
+    table {
+        border: 1px solid #e5e5e5;
+    }
+    th {
+        text-align: left;
+        width: 10em;
+        padding: 5px;
+        border: 1px solid #e5e5e5;
+    }
+    tr.description p {
+        padding-left: 0;
+    }
+    tr.description p:last-child {
+        padding-bottom: 0;
+    }
+    tr.controls input[type="submit"] {
+        margin-bottom: 0;
+    }
+</style>
+{% endblock %}
+
+{% block extra_js %}
+<script type="text/javascript">
+    $(function() {
+        $('.deregister_consumer_token').submit(function(e) {
+            var ok = confirm('Deregister application and revoke all its access tokens?')
+            if(!ok) {
+                e.preventDefault();
+                return false;
+            }
+        });
+
+        $('.revoke_access_token').submit(function(e) {
+            var ok = confirm('Revoke access?')
+            if(!ok) {
+                e.preventDefault();
+                return false;
+            }
+        });
+    })
+</script>
+{% endblock %}
+
 {% block content %}
-{% for token in apps %}
-<h2>{{token.name}}</h2>
-{{token.description_html | safe }}
-<dl>
-  <dt>Consumer Key</dt><dd>{{token.api_key}}</dd>
-  <dt>Consumer Secret</dt><dd>{{token.secret_key}}</dd>
-</dl>
-<br>
-<form method="POST" action="delete"><input type="hidden" name="id" value="{{token._id}}">
-<input type="submit" value="Deregister {{token.name}}">
-</form>
-<br style="clear:both"/>
-{% endfor %}
-<h2>Register a new OAuth application</h2>
-{{ c.form.display() }}
+    <ul id="account-nav-menu" class="b-hornav droppy">
+    {% for item in menu -%}
+        <li id="{{ item.tabid }}">
+            <a href="{{ item.target }}">
+                {{ item.title }}
+                <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/')
%} current{% endif %}"></div>
+            </a>
+        </li>
+    {%- endfor %}
+    </ul>
+
+    <h2>Authorized Applications</h2>
+    <p>
+        These are applications you have authorized to act on your behalf.
+        They potentially have full access to your account, so if you are
+        no longer using an application listed here, you should revoke its
+        access.
+    </p>
+    {% for access_token in access_tokens %}
+    <table class="authorized_app">
+        <tr class="name">
+            <th>Name:</th><td>{{access_token.consumer_token.name}}</td>
+        </tr>
+        <tr class="description">
+            <th>Description:</th><td>{{access_token.consumer_token.description_html
| safe}}</td>
+        </tr>
+        {% if access_token.is_bearer %}
+        <tr class="bearer_token">
+            <th>Bearer Token:</th><td>{{access_token.api_key}}</td>
+        </tr>
+        {% endif %}
+        <tr class="controls">
+            <td colspan="2">
+                <form method="POST" action="revoke_access_token" class="revoke_access_token">
+                    <input type="hidden" name="id" value="{{access_token._id}}">
+                    <input type="submit" value="Revoke">
+                </form>
+            </td>
+        </tr>
+    </table>
+    {% endfor %}
+
+    <h2>My Applications</h2>
+    <p>
+    These are the applications you have registered.  They can request authorization
+    for a user using the Consumer Key and Consumer Secret via OAuth negotiation.
+    Alternatively, you can generate a bearer token to give your applicationa access
+    to your account without having to perform the OAuth negotiation.  Note, however,
+    that you must be careful with bearer tokens, since anyone who has them token can
+    access your account as that application.
+    </p>
+    {% for consumer_token in consumer_tokens %}
+    <table class="registered_app">
+        <tr><th>Name:</th><td>{{consumer_token.name}}</td></tr>
+        <tr class="description"><th>Description:</th><td>{{consumer_token.description_html
| safe}}</td></tr>
+        <tr class="consumer_key"><th>Consumer Key:</th><td>{{consumer_token.api_key}}</td></tr>
+        <tr class="consumer_secret"><th>Consumer Secret:</th><td>{{consumer_token.api_key}}</td></tr>
+        <tr class="controls">
+            <td colspan="2"
+                <form method="POST" action="deregister" class="deregister_consumer_token">
+                    <input type="hidden" name="id" value="{{consumer_token._id}}">
+                    <input type="submit" value="Deregister">
+                </form>
+                <form method="POST" action="generate_access_token" class="generate_access_token">
+                    <input type="hidden" name="id" value="{{consumer_token._id}}">
+                    <input type="submit" value="Generate Bearer Token">
+                </form>
+            </td>
+        </tr>
+    </table>
+    {% endfor %}
+
+    <h2>Register New Application</h2>
+    {{ c.form.display() }}
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/cee54911/Allura/allura/templates/oauth_authorize.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/oauth_authorize.html b/Allura/allura/templates/oauth_authorize.html
index eecb5fc..84b86a9 100644
--- a/Allura/allura/templates/oauth_authorize.html
+++ b/Allura/allura/templates/oauth_authorize.html
@@ -24,11 +24,19 @@
 {% block header %}Authorize third party application?{% endblock %}
 
 {% block content %}
-<p>The application {{ consumer.name }} wishes to access your account.  If you
-  grant them access, they will be able to perform any actions on the site as
-  though they were logged in as you.  Do you wish to grant them access? </p>
+<p>
+  {% if name %}
+    The application {{ name }} wishes to access your account using the {{ consumer.name }}
key.
+  {% else %}
+    The application {{ consumer.name }} wishes to access your account.
+  {% endif %}
+  If you grant them access, they will be able to perform any actions on
+  the site as though they were logged in as you.  Do you wish to grant
+  them access?
+</p>
 <form method="POST" action="do_authorize">
   <input type="hidden" name="oauth_token" value="{{oauth_token}}"/>
+  <input type="hidden" name="name" value="{{name}}"/>
   <input type="submit" name="no" value="No, do not authorize {{ consumer.name }}">
   <input type="submit" name="yes" value="Yes, authorize {{ consumer.name }}"><br>
 </form>

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/cee54911/Allura/allura/templates/user_subs.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_subs.html b/Allura/allura/templates/user_subs.html
index 19b9a6a..7e90a17 100644
--- a/Allura/allura/templates/user_subs.html
+++ b/Allura/allura/templates/user_subs.html
@@ -35,17 +35,6 @@
       {%- endfor %}
    </ul>
 
-   <h2>Authorized Third-party Applications</h2>
-   {% for access_tok in authorized_applications %}
-     <div>
-       <h3>{{access_tok.consumer_token.name}}</h3>
-       {{access_tok.consumer_token.description_html}}
-       {{ c.revoke_access.display(value=access_tok) }}
-       <br style="clear:both"/>
-   </div>
-  {% endfor %}
-     {% if not authorized_applications %}<p>No authorized third-party applications</p>{%
endif %}
-
   <h2>Subscriptions</h2>
   {% if subscriptions %}
     <p><em>Mark tools that you want to subscribe to. Unmark tools that you want
to unsubscribe from. Press 'Save' button.</em></p>

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/cee54911/Allura/allura/tests/functional/test_rest.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_rest.py b/Allura/allura/tests/functional/test_rest.py
index 8bb3a28..e814580 100644
--- a/Allura/allura/tests/functional/test_rest.py
+++ b/Allura/allura/tests/functional/test_rest.py
@@ -44,6 +44,32 @@ class TestRestHome(TestRestApiBase):
         r = self.api_post('/rest/p/test/wiki/', api_timestamp=(datetime.utcnow() + timedelta(days=1)).isoformat())
         assert r.status_int == 403
 
+    @mock.patch('allura.controllers.rest.M.OAuthAccessToken')
+    @mock.patch('allura.controllers.rest.request')
+    def test_bearer_token(self, request, OAuthAccessToken):
+        request.params = {'access_token': 'foo'}
+        request.scheme = 'http'
+        r = self.api_post('/rest/p/test/wiki', access_token='foo')
+        assert_equal(r.status_int, 403)
+        assert_equal(OAuthAccessToken.query.get.call_count, 0)
+
+        request.scheme = 'https'
+        access_token = OAuthAccessToken.query.get.return_value
+        access_token.is_bearer = False
+        r = self.api_post('/rest/p/test/wiki', access_token='foo')
+        assert_equal(r.status_int, 403)
+        OAuthAccessToken.query.get.assert_called_once_with(api_key='foo')
+
+        OAuthAccessToken.query.get.reset_mock()
+        access_token.is_bearer = True
+        r = self.api_post('/rest/p/test/wiki', access_token='foo')
+        assert_equal(r.status_int, 404)
+        OAuthAccessToken.query.get.assert_called_once_with(api_key='foo')
+
+        OAuthAccessToken.query.get.return_value = None
+        r = self.api_post('/rest/p/test/wiki', access_token='foo')
+        assert_equal(r.status_int, 403)
+
     def test_bad_path(self):
         r = self.api_post('/rest/1/test/wiki/')
         assert r.status_int == 404


Mime
View raw message