Add support for multiple scopes per OAuth2 access token.

This commit is contained in:
Max Goodman
2012-03-14 15:16:27 -07:00
committed by Logan Hanks
parent 35006858cb
commit 81caf1213d
6 changed files with 38 additions and 12 deletions

View File

@@ -93,6 +93,7 @@ error_list = dict((
('BAD_FLAIR_TARGET', _('not a valid flair target')),
('OAUTH2_INVALID_CLIENT', _('invalid client id')),
('OAUTH2_INVALID_REDIRECT_URI', _('invalid redirect_uri parameter')),
('OAUTH2_INVALID_SCOPE', _('invalid scope requested')),
('OAUTH2_ACCESS_DENIED', _('access denied by the user')),
('CONFIRM', _("please confirm the form")),
('NO_API', _('cannot perform this action via the API')),

View File

@@ -33,7 +33,7 @@ from r2.lib.db.thing import NotFound
from r2.models import Account
from r2.models.token import OAuth2Client, OAuth2AuthorizationCode, OAuth2AccessToken
from r2.controllers.errors import ForbiddenError, errors
from validator import validate, VRequired, VOneOf, VUser, VModhash, VOAuth2ClientID
from validator import validate, VRequired, VOneOf, VUser, VModhash, VOAuth2ClientID, VOAuth2Scope
from r2.lib.pages import OAuth2AuthorizationPage
from r2.lib.require import RequirementException, require, require_split
@@ -67,7 +67,7 @@ class OAuth2FrontendController(RedditController):
resp["error"] = "access_denied"
elif (errors.INVALID_OPTION, "response_type") in c.errors:
resp["error"] = "unsupported_response_type"
elif (errors.INVALID_OPTION, "scope") in c.errors:
elif (errors.OAUTH2_INVALID_SCOPE, "scope") in c.errors:
resp["error"] = "invalid_scope"
else:
resp["error"] = "invalid_request"
@@ -78,7 +78,7 @@ class OAuth2FrontendController(RedditController):
response_type = VOneOf("response_type", ("code",)),
client = VOAuth2ClientID(),
redirect_uri = VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI),
scope = VOneOf("scope", scope_info.keys()),
scope = VOAuth2Scope(),
state = VRequired("state", errors.NO_TEXT))
def GET_authorize(self, response_type, client, redirect_uri, scope, state):
"""
@@ -106,7 +106,7 @@ class OAuth2FrontendController(RedditController):
if not c.errors:
c.deny_frames = True
return OAuth2AuthorizationPage(client, redirect_uri, scope_info[scope], state).render()
return OAuth2AuthorizationPage(client, redirect_uri, scope, state).render()
else:
return self._error_response(state, redirect_uri)
@@ -114,7 +114,7 @@ class OAuth2FrontendController(RedditController):
VModhash(fatal=False),
client = VOAuth2ClientID(),
redirect_uri = VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI),
scope = VOneOf("scope", scope_info.keys()),
scope = VOAuth2Scope(),
state = VRequired("state", errors.NO_TEXT),
authorize = VRequired("authorize", errors.OAUTH2_ACCESS_DENIED))
def POST_authorize(self, authorize, client, redirect_uri, scope, state):
@@ -219,7 +219,7 @@ class OAuth2ResourceController(MinimalController):
if handler:
oauth2_perms = getattr(handler, "oauth2_perms", None)
if oauth2_perms:
if access_token.scope not in oauth2_perms["allowed_scopes"]:
if set(oauth2_perms["allowed_scopes"]).intersection(access_token.scope_list):
self._auth_error(403, "insufficient_scope")
else:
self._auth_error(400, "invalid_request")

View File

@@ -1855,3 +1855,18 @@ class VOAuth2ClientDeveloper(VOAuth2ClientID):
if not client or not client.has_developer(c.user):
return self.error()
return client
class VOAuth2Scope(VRequired):
default_param = "scope"
def __init__(self, param=None, *a, **kw):
VRequired.__init__(self, param, errors.OAUTH2_INVALID_SCOPE, *a, **kw)
def run(self, scope):
from r2.controllers.oauth2 import scope_info
scope = VRequired.run(self, scope)
if scope:
scope_list = scope.split(',')
if all(scope in scope_info for scope in scope_list):
return scope_list
else:
self.error()

View File

@@ -824,10 +824,12 @@ class Register(Login):
pass
class OAuth2AuthorizationPage(BoringPage):
def __init__(self, client, redirect_uri, scope, state):
def __init__(self, client, redirect_uri, scopes, state):
from r2.controllers.oauth2 import scope_info
scope_details = [scope_info[scope] for scope in scopes]
content = OAuth2Authorization(client=client,
redirect_uri=redirect_uri,
scope=scope,
scope_details=scope_details,
state=state)
BoringPage.__init__(self, _("request for permission"),
show_sidebar=False, content=content)

View File

@@ -245,7 +245,8 @@ class OAuth2AuthorizationCode(ConsumableToken):
_connection_pool = "main"
@classmethod
def _new(cls, client_id, redirect_uri, user_id, scope):
def _new(cls, client_id, redirect_uri, user_id, scope_list):
scope = ','.join(scope_list)
return super(OAuth2AuthorizationCode, cls)._new(
client_id=client_id,
redirect_uri=redirect_uri,
@@ -275,7 +276,8 @@ class OAuth2AccessToken(Token):
_connection_pool = "main"
@classmethod
def _new(cls, user_id, scope):
def _new(cls, user_id, scope_list):
scope = ','.join(scope_list)
return super(OAuth2AccessToken, cls)._new(
user_id=user_id,
scope=scope)
@@ -349,6 +351,10 @@ class OAuth2AccessToken(Token):
return tokens
@property
def scope_list(self):
return self.scope.split(',')
class OAuth2AccessTokensByUser(tdb_cassandra.View):
"""Index listing the outstanding access tokens for an account."""

View File

@@ -31,13 +31,15 @@
<div class="access">
<h2>${_("Allow %(app_name)s to:") % dict(app_name=thing.client.name)}</h2>
<ul>
<li>${thing.scope["description"]}</li>
%for scope_info in thing.scope_details:
<li>${scope_info["description"]}</li>
%endfor
</ul>
<p class="notice">${_("%(app_name)s will not be able to access your reddit password.") % dict(app_name=thing.client.name)}</p>
<form method="post" action="/api/v1/authorize" class="pretty-form">
<input type="hidden" name="client_id" value="${thing.client._id}" />
<input type="hidden" name="redirect_uri" value="${thing.redirect_uri}" />
<input type="hidden" name="scope" value="${thing.scope['id']}" />
<input type="hidden" name="scope" value="${','.join(scope['id'] for scope in thing.scope_details)}" />
<input type="hidden" name="state" value="${thing.state}" />
<input type="hidden" name="uh" value="${c.modhash}"/>
<div>