mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-01-27 07:48:16 -05:00
Preliminary OAuth2 for new API call authentication.
This commit is contained in:
@@ -224,6 +224,10 @@ def make_map(global_conf={}, app_conf={}):
|
||||
requirements=dict(action="new_captcha"))
|
||||
mc('/api/:action', controller='api')
|
||||
|
||||
mc("/api/v1/:action", controller="oauth2frontend", requirements=dict(action="authorize"))
|
||||
mc("/api/v1/:action", controller="oauth2access", requirements=dict(action="access_token"))
|
||||
mc("/api/v1/:action", controller="apiv1")
|
||||
|
||||
mc("/button_info", controller="api", action="info", limit = 1)
|
||||
|
||||
mc('/captcha/:iden', controller='captcha', action='captchaimg')
|
||||
|
||||
@@ -61,6 +61,9 @@ except ImportError:
|
||||
from api import ApiController
|
||||
|
||||
from api import ApiminimalController
|
||||
from apiv1 import APIv1Controller as Apiv1Controller
|
||||
from oauth2 import OAuth2FrontendController as Oauth2frontendController
|
||||
from oauth2 import OAuth2AccessController as Oauth2accessController
|
||||
from admin import AdminController
|
||||
from redirect import RedirectController
|
||||
from ipn import IpnController
|
||||
|
||||
33
r2/r2/controllers/apiv1.py
Normal file
33
r2/r2/controllers/apiv1.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# The contents of this file are subject to the Common Public Attribution
|
||||
# License Version 1.0. (the "License"); you may not use this file except in
|
||||
# compliance with the License. You may obtain a copy of the License at
|
||||
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
|
||||
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
|
||||
# software over a computer network and provide for limited attribution for the
|
||||
# Original Developer. In addition, Exhibit A has been modified to be consistent
|
||||
# with Exhibit B.
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
|
||||
# the specific language governing rights and limitations under the License.
|
||||
#
|
||||
# The Original Code is Reddit.
|
||||
#
|
||||
# The Original Developer is the Initial Developer. The Initial Developer of the
|
||||
# Original Code is CondeNet, Inc.
|
||||
#
|
||||
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
|
||||
# CondeNet, Inc. All Rights Reserved.
|
||||
################################################################################
|
||||
from pylons import c
|
||||
from r2.controllers.oauth2 import OAuth2ResourceController, require_oauth2_scope
|
||||
from r2.lib.jsontemplates import IdentityJsonTemplate
|
||||
|
||||
class APIv1Controller(OAuth2ResourceController):
|
||||
def try_pagecache(self):
|
||||
pass
|
||||
|
||||
@require_oauth2_scope("identity")
|
||||
def GET_me(self):
|
||||
resp = IdentityJsonTemplate().data(c.oauth_user)
|
||||
return self.api_wrapper(resp)
|
||||
@@ -85,6 +85,8 @@ error_list = dict((
|
||||
('TOO_OLD', _("that's a piece of history now; it's too late to reply to it")),
|
||||
('BAD_CSS_NAME', _('invalid css name')),
|
||||
('TOO_MUCH_FLAIR_CSS', _('too many flair css classes')),
|
||||
('OAUTH2_INVALID_CLIENT', _('invalid client id')),
|
||||
('OAUTH2_ACCESS_DENIED', _('access denied by the user')),
|
||||
))
|
||||
errors = Storage([(e, e) for e in error_list.keys()])
|
||||
|
||||
|
||||
215
r2/r2/controllers/oauth2.py
Normal file
215
r2/r2/controllers/oauth2.py
Normal file
@@ -0,0 +1,215 @@
|
||||
# The contents of this file are subject to the Common Public Attribution
|
||||
# License Version 1.0. (the "License"); you may not use this file except in
|
||||
# compliance with the License. You may obtain a copy of the License at
|
||||
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
|
||||
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
|
||||
# software over a computer network and provide for limited attribution for the
|
||||
# Original Developer. In addition, Exhibit A has been modified to be consistent
|
||||
# with Exhibit B.
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
|
||||
# the specific language governing rights and limitations under the License.
|
||||
#
|
||||
# The Original Code is Reddit.
|
||||
#
|
||||
# The Original Developer is the Initial Developer. The Initial Developer of the
|
||||
# Original Code is CondeNet, Inc.
|
||||
#
|
||||
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
|
||||
# CondeNet, Inc. All Rights Reserved.
|
||||
################################################################################
|
||||
from urllib import urlencode
|
||||
import base64
|
||||
import simplejson
|
||||
|
||||
from pylons import c, g, request
|
||||
from pylons.controllers.util import abort
|
||||
from pylons.i18n import _
|
||||
from r2.config.extensions import set_extension
|
||||
from reddit_base import RedditController, MinimalController, require_https
|
||||
from r2.lib.db.thing import NotFound
|
||||
from r2.models import Account
|
||||
from r2.models.oauth2 import OAuth2Client, OAuth2AuthorizationCode, OAuth2AccessToken
|
||||
from r2.controllers.errors import errors
|
||||
from validator import validate, VRequired, VOneOf, VUrl, VUser, VModhash
|
||||
from r2.lib.pages import OAuth2AuthorizationPage
|
||||
from r2.lib.require import RequirementException, require, require_split
|
||||
|
||||
scope_info = {
|
||||
"identity": {
|
||||
"id": "identity",
|
||||
"name": _("My Identity"),
|
||||
"description": _("Access my reddit username and signup date.")
|
||||
}
|
||||
}
|
||||
|
||||
class VClientID(VRequired):
|
||||
default_param = "client_id"
|
||||
def __init__(self, param=None, *a, **kw):
|
||||
VRequired.__init__(self, param, errors.OAUTH2_INVALID_CLIENT, *a, **kw)
|
||||
|
||||
def run(self, client_id):
|
||||
if not client_id:
|
||||
return self.error()
|
||||
|
||||
client = OAuth2Client.get_token(client_id)
|
||||
if client:
|
||||
return client
|
||||
else:
|
||||
return self.error()
|
||||
|
||||
class OAuth2FrontendController(RedditController):
|
||||
def pre(self):
|
||||
RedditController.pre(self)
|
||||
require_https()
|
||||
|
||||
def _check_redirect_uri(self, client, redirect_uri):
|
||||
if not redirect_uri or not client or redirect_uri != client.redirect_uri:
|
||||
abort(403)
|
||||
|
||||
def _error_response(self, resp):
|
||||
if (errors.OAUTH2_INVALID_CLIENT, "client_id") in c.errors:
|
||||
resp["error"] = "unauthorized_client"
|
||||
elif (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors:
|
||||
resp["error"] = "access_denied"
|
||||
elif (errors.BAD_HASH, None) in c.errors:
|
||||
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:
|
||||
resp["error"] = "invalid_scope"
|
||||
else:
|
||||
resp["error"] = "invalid_request"
|
||||
|
||||
@validate(VUser(),
|
||||
response_type = VOneOf("response_type", ("code",)),
|
||||
client = VClientID(),
|
||||
redirect_uri = VUrl("redirect_uri", allow_self=False, lookup=False),
|
||||
scope = VOneOf("scope", scope_info.keys()),
|
||||
state = VRequired("state", errors.NO_TEXT))
|
||||
def GET_authorize(self, response_type, client, redirect_uri, scope, state):
|
||||
self._check_redirect_uri(client, redirect_uri)
|
||||
|
||||
resp = {}
|
||||
if not c.errors:
|
||||
c.deny_frames = True
|
||||
return OAuth2AuthorizationPage(client, redirect_uri, scope_info[scope], state).render()
|
||||
else:
|
||||
self._error_response(resp)
|
||||
return self.redirect(redirect_uri+"?"+urlencode(resp), code=302)
|
||||
|
||||
@validate(VUser(),
|
||||
VModhash(fatal=False),
|
||||
client = VClientID(),
|
||||
redirect_uri = VUrl("redirect_uri", allow_self=False, lookup=False),
|
||||
scope = VOneOf("scope", scope_info.keys()),
|
||||
state = VRequired("state", errors.NO_TEXT),
|
||||
authorize = VRequired("authorize", errors.OAUTH2_ACCESS_DENIED))
|
||||
def POST_authorize(self, authorize, client, redirect_uri, scope, state):
|
||||
self._check_redirect_uri(client, redirect_uri)
|
||||
|
||||
resp = {}
|
||||
if state:
|
||||
resp["state"] = state
|
||||
|
||||
if not c.errors:
|
||||
code = OAuth2AuthorizationCode._new(client._id, redirect_uri, c.user._id, scope)
|
||||
resp["code"] = code._id
|
||||
else:
|
||||
self._error_response(resp)
|
||||
|
||||
return self.redirect(redirect_uri+"?"+urlencode(resp), code=302)
|
||||
|
||||
class OAuth2AccessController(MinimalController):
|
||||
def pre(self):
|
||||
set_extension(request.environ, "json")
|
||||
MinimalController.pre(self)
|
||||
require_https()
|
||||
c.oauth2_client = self._get_client_auth()
|
||||
|
||||
def _get_client_auth(self):
|
||||
auth = request.headers.get("Authorization")
|
||||
try:
|
||||
auth_scheme, auth_token = require_split(auth, 2)
|
||||
require(auth_scheme.lower() == "basic")
|
||||
try:
|
||||
auth_data = base64.b64decode(auth_token)
|
||||
except TypeError:
|
||||
raise RequirementException
|
||||
client_id, client_secret = require_split(auth_data, 2, ":")
|
||||
client = OAuth2Client.get_token(client_id)
|
||||
require(client)
|
||||
require(client.secret == client_secret)
|
||||
return client
|
||||
except RequirementException:
|
||||
abort(401, headers=[("WWW-Authenticate", 'Basic realm="reddit"')])
|
||||
|
||||
@validate(grant_type = VOneOf("grant_type", ("authorization_code",)),
|
||||
code = VRequired("code", errors.NO_TEXT),
|
||||
redirect_uri = VUrl("redirect_uri", allow_self=False, lookup=False))
|
||||
def POST_access_token(self, grant_type, code, redirect_uri):
|
||||
resp = {}
|
||||
if not c.errors:
|
||||
auth_token = OAuth2AuthorizationCode.use_token(code, c.oauth2_client._id, redirect_uri)
|
||||
if auth_token:
|
||||
access_token = OAuth2AccessToken._new(auth_token.user_id, auth_token.scope)
|
||||
resp["access_token"] = access_token._id
|
||||
resp["token_type"] = access_token.token_type
|
||||
resp["expires_in"] = access_token._ttl
|
||||
resp["scope"] = auth_token.scope
|
||||
else:
|
||||
resp["error"] = "invalid_grant"
|
||||
else:
|
||||
if (errors.INVALID_OPTION, "grant_type") in c.errors:
|
||||
resp["error"] = "unsupported_grant_type"
|
||||
elif (errors.INVALID_OPTION, "scope") in c.errors:
|
||||
resp["error"] = "invalid_scope"
|
||||
else:
|
||||
resp["error"] = "invalid_request"
|
||||
|
||||
return self.api_wrapper(resp)
|
||||
|
||||
class OAuth2ResourceController(MinimalController):
|
||||
def pre(self):
|
||||
set_extension(request.environ, "json")
|
||||
MinimalController.pre(self)
|
||||
require_https()
|
||||
|
||||
try:
|
||||
access_token = self._get_bearer_token()
|
||||
require(access_token)
|
||||
c.oauth2_access_token = access_token
|
||||
account = Account._byID(access_token.user_id, data=True)
|
||||
require(account)
|
||||
require(not account._deleted)
|
||||
c.oauth_user = account
|
||||
except RequirementException:
|
||||
self._auth_error(401, "invalid_token")
|
||||
|
||||
handler = self._get_action_handler()
|
||||
if handler:
|
||||
oauth2_perms = getattr(handler, "oauth2_perms", None)
|
||||
if oauth2_perms:
|
||||
if access_token.scope not in oauth2_perms["allowed_scopes"]:
|
||||
self._auth_error(403, "insufficient_scope")
|
||||
else:
|
||||
self._auth_error(400, "invalid_request")
|
||||
|
||||
def _auth_error(self, code, error):
|
||||
abort(code, headers=[("WWW-Authenticate", 'Bearer realm="reddit", error="%s"' % error)])
|
||||
|
||||
def _get_bearer_token(self):
|
||||
auth = request.headers.get("Authorization")
|
||||
try:
|
||||
auth_scheme, bearer_token = require_split(auth, 2)
|
||||
require(auth_scheme.lower() == "bearer")
|
||||
return OAuth2AccessToken.get_token(bearer_token)
|
||||
except RequirementException:
|
||||
self._auth_error(400, "invalid_request")
|
||||
|
||||
def require_oauth2_scope(*scopes):
|
||||
def oauth2_scope_wrap(fn):
|
||||
fn.oauth2_perms = {"allowed_scopes": scopes}
|
||||
return fn
|
||||
return oauth2_scope_wrap
|
||||
@@ -504,6 +504,10 @@ def cross_domain(origin_check=is_trusted_origin, **options):
|
||||
return cross_domain_handler
|
||||
return cross_domain_wrap
|
||||
|
||||
def require_https():
|
||||
if not c.secure:
|
||||
abort(403)
|
||||
|
||||
class MinimalController(BaseController):
|
||||
|
||||
allow_stylesheets = False
|
||||
@@ -539,6 +543,8 @@ class MinimalController(BaseController):
|
||||
|
||||
c.domain_prefix = request.environ.get("reddit-domain-prefix",
|
||||
g.domain_prefix)
|
||||
c.secure = request.host in g.secure_domains
|
||||
|
||||
#check if user-agent needs a dose of rate-limiting
|
||||
if not c.error_page:
|
||||
ratelimit_agents()
|
||||
@@ -697,7 +703,6 @@ class MinimalController(BaseController):
|
||||
return c.response
|
||||
|
||||
|
||||
|
||||
class RedditController(MinimalController):
|
||||
|
||||
@staticmethod
|
||||
@@ -728,7 +733,6 @@ class RedditController(MinimalController):
|
||||
#can't handle broken cookies
|
||||
request.environ['HTTP_COOKIE'] = ''
|
||||
|
||||
c.secure = request.host in g.secure_domains
|
||||
c.firsttime = firsttime()
|
||||
|
||||
# the user could have been logged in via one of the feeds
|
||||
|
||||
@@ -201,6 +201,7 @@ class ThingBase(object):
|
||||
# updated columns will be present.) This is an expected convention
|
||||
# and is not enforced.
|
||||
_ttl = None
|
||||
_warn_on_partial_ttl = True
|
||||
|
||||
# A per-class dictionary of default TTLs that new columns of this
|
||||
# class should have
|
||||
@@ -463,7 +464,7 @@ class ThingBase(object):
|
||||
if self._id is None:
|
||||
raise TdbException("Can't commit %r without an ID" % (self,))
|
||||
|
||||
if self._committed and self._ttl:
|
||||
if self._committed and self._ttl and self._warn_on_partial_ttl:
|
||||
log.warning("Using a full-TTL object %r in a mutable fashion"
|
||||
% (self,))
|
||||
|
||||
|
||||
@@ -211,16 +211,19 @@ class SubredditJsonTemplate(ThingJsonTemplate):
|
||||
else:
|
||||
return ThingJsonTemplate.thing_attr(self, thing, attr)
|
||||
|
||||
class AccountJsonTemplate(ThingJsonTemplate):
|
||||
class IdentityJsonTemplate(ThingJsonTemplate):
|
||||
_data_attrs_ = ThingJsonTemplate.data_attrs(name = "name",
|
||||
link_karma = "safe_karma",
|
||||
comment_karma = "comment_karma",
|
||||
has_mail = "has_mail",
|
||||
has_mod_mail = "has_mod_mail",
|
||||
is_mod = "is_mod",
|
||||
is_gold = "gold"
|
||||
)
|
||||
|
||||
class AccountJsonTemplate(IdentityJsonTemplate):
|
||||
_data_attrs_ = IdentityJsonTemplate.data_attrs(has_mail = "has_mail",
|
||||
has_mod_mail = "has_mod_mail",
|
||||
is_mod = "is_mod",
|
||||
)
|
||||
|
||||
def thing_attr(self, thing, attr):
|
||||
from r2.models import Subreddit
|
||||
if attr == "has_mail":
|
||||
|
||||
@@ -25,6 +25,7 @@ from r2.models import FakeSubreddit, Subreddit, Ad, AdSR
|
||||
from r2.models import Friends, All, Sub, NotFound, DomainSR, Random, Mod, RandomNSFW, MultiReddit
|
||||
from r2.models import Link, Printable, Trophy, bidding, PromotionWeights, Comment
|
||||
from r2.models import Flair, FlairTemplate, FlairTemplateBySubredditIndex
|
||||
from r2.models.oauth2 import OAuth2Client
|
||||
from r2.config import cache
|
||||
from r2.lib.tracking import AdframeInfo
|
||||
from r2.lib.jsonresponse import json_respond
|
||||
@@ -683,11 +684,20 @@ class LoginPage(BoringPage):
|
||||
title = _("login or register")
|
||||
BoringPage.__init__(self, title, **context)
|
||||
|
||||
if self.dest:
|
||||
u = UrlParser(self.dest)
|
||||
# Display a preview message for OAuth2 client authorizations
|
||||
if u.path == '/api/v1/authorize':
|
||||
client_id = u.query_dict.get("client_id")
|
||||
self.client = client_id and OAuth2Client.get_token(client_id)
|
||||
self.infobar = self.client and ClientInfoBar(self.client, strings.oauth_login_msg)
|
||||
|
||||
def content(self):
|
||||
kw = {}
|
||||
for x in ('user_login', 'user_reg'):
|
||||
kw[x] = getattr(self, x) if hasattr(self, x) else ''
|
||||
return self.login_template(dest = self.dest, **kw)
|
||||
login_content = self.login_template(dest = self.dest, **kw)
|
||||
return self.content_stack((self.infobar, login_content))
|
||||
|
||||
@classmethod
|
||||
def login_template(cls, **kw):
|
||||
@@ -707,7 +717,19 @@ class Login(Templated):
|
||||
|
||||
class Register(Login):
|
||||
pass
|
||||
|
||||
|
||||
class OAuth2AuthorizationPage(BoringPage):
|
||||
def __init__(self, client, redirect_uri, scope, state):
|
||||
content = OAuth2Authorization(client=client,
|
||||
redirect_uri=redirect_uri,
|
||||
scope=scope,
|
||||
state=state)
|
||||
BoringPage.__init__(self, _("request for permission"),
|
||||
show_sidebar=False, content=content)
|
||||
|
||||
class OAuth2Authorization(Templated):
|
||||
pass
|
||||
|
||||
class SearchPage(BoringPage):
|
||||
"""Search results page"""
|
||||
searchbox = False
|
||||
@@ -1213,6 +1235,12 @@ class InfoBar(Templated):
|
||||
def __init__(self, message = '', extra_class = ''):
|
||||
Templated.__init__(self, message = message, extra_class = extra_class)
|
||||
|
||||
class ClientInfoBar(InfoBar):
|
||||
"""Draws the message the top of a login page before OAuth2 authorization"""
|
||||
def __init__(self, client, *args, **kwargs):
|
||||
kwargs.setdefault("extra_class", "client-info")
|
||||
InfoBar.__init__(self, *args, **kwargs)
|
||||
self.client = client
|
||||
|
||||
class RedditError(BoringPage):
|
||||
site_tracking = False
|
||||
|
||||
19
r2/r2/lib/require.py
Normal file
19
r2/r2/lib/require.py
Normal file
@@ -0,0 +1,19 @@
|
||||
class RequirementException(Exception):
|
||||
pass
|
||||
|
||||
def require(val):
|
||||
"""A safe version of assert
|
||||
|
||||
Assert can be stripped out if python is run in an optimized
|
||||
mode. This function implements assertions in a way that is
|
||||
guaranteed to execute.
|
||||
"""
|
||||
if not val:
|
||||
raise RequirementException
|
||||
return val
|
||||
|
||||
def require_split(s, length, sep=None):
|
||||
require(s)
|
||||
res = s.split(sep)
|
||||
require(len(res) == length)
|
||||
return res
|
||||
@@ -73,6 +73,8 @@ string_dict = dict(
|
||||
cover_msg = _("you'll need to login or register to do that"),
|
||||
cover_disclaim = _("(don't worry, it only takes a few seconds)"),
|
||||
|
||||
oauth_login_msg = _("Log in or register to connect your reddit account with [%(app_name)s](%(app_about_url)s)."),
|
||||
|
||||
legal = _("I understand and agree that registration on or use of this site constitutes agreement to its %(user_agreement)s and %(privacy_policy)s."),
|
||||
|
||||
friends = _('to view reddit with only submissions from your friends, use [reddit.com/r/friends](%s)'),
|
||||
|
||||
@@ -35,3 +35,4 @@ from bidding import *
|
||||
from mail_queue import Email, has_opted_out, opt_count
|
||||
from gold import *
|
||||
from admintools import *
|
||||
from oauth2 import *
|
||||
|
||||
136
r2/r2/models/oauth2.py
Normal file
136
r2/r2/models/oauth2.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# The contents of this file are subject to the Common Public Attribution
|
||||
# License Version 1.0. (the "License"); you may not use this file except in
|
||||
# compliance with the License. You may obtain a copy of the License at
|
||||
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
|
||||
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
|
||||
# software over a computer network and provide for limited attribution for the
|
||||
# Original Developer. In addition, Exhibit A has been modified to be consistent
|
||||
# with Exhibit B.
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
|
||||
# the specific language governing rights and limitations under the License.
|
||||
#
|
||||
# The Original Code is Reddit.
|
||||
#
|
||||
# The Original Developer is the Initial Developer. The Initial Developer of the
|
||||
# Original Code is CondeNet, Inc.
|
||||
#
|
||||
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
|
||||
# CondeNet, Inc. All Rights Reserved.
|
||||
################################################################################
|
||||
from os import urandom
|
||||
from base64 import urlsafe_b64encode
|
||||
from r2.lib.db import tdb_cassandra
|
||||
|
||||
def generate_token(size):
|
||||
return urlsafe_b64encode(urandom(size)).rstrip('=')
|
||||
|
||||
class OAuth2Token(tdb_cassandra.Thing):
|
||||
"""An OAuth2 authorization code for completing authorization flow"""
|
||||
|
||||
@classmethod
|
||||
def _new(cls, **kwargs):
|
||||
if "_id" not in kwargs:
|
||||
kwargs["_id"] = cls._generate_unique_token()
|
||||
|
||||
client = cls(**kwargs)
|
||||
client._commit()
|
||||
return client
|
||||
|
||||
@classmethod
|
||||
def _generate_unique_token(cls):
|
||||
for i in range(3):
|
||||
token = generate_token(cls.token_size)
|
||||
try:
|
||||
cls._byID(token)
|
||||
except tdb_cassandra.NotFound:
|
||||
return token
|
||||
else:
|
||||
continue
|
||||
raise ValueError
|
||||
|
||||
@classmethod
|
||||
def get_token(cls, _id):
|
||||
try:
|
||||
return cls._byID(_id)
|
||||
except tdb_cassandra.NotFound:
|
||||
return False
|
||||
|
||||
class OAuth2Client(OAuth2Token):
|
||||
"""A client registered for OAuth2 access"""
|
||||
token_size = 10
|
||||
client_secret_size = 20
|
||||
_defaults = dict(name="",
|
||||
description="",
|
||||
about_url="",
|
||||
icon_url="",
|
||||
secret="",
|
||||
redirect_uri="",
|
||||
)
|
||||
_use_db = True
|
||||
_use_new_ring = True
|
||||
|
||||
@classmethod
|
||||
def _new(cls, **kwargs):
|
||||
if "secret" not in kwargs:
|
||||
kwargs["secret"] = generate_token(cls.client_secret_size)
|
||||
return super(OAuth2Client, cls)._new(**kwargs)
|
||||
|
||||
class OAuth2AuthorizationCode(OAuth2Token):
|
||||
"""An OAuth2 authorization code for completing authorization flow"""
|
||||
token_size = 20
|
||||
_ttl = 10*60
|
||||
_defaults = dict(client_id="",
|
||||
redirect_uri="",
|
||||
scope="",
|
||||
used=False,
|
||||
)
|
||||
_bool_props = ("used",)
|
||||
_int_props = ("user_id",)
|
||||
_warn_on_partial_ttl = False
|
||||
_use_db = True
|
||||
_use_new_ring = True
|
||||
|
||||
@classmethod
|
||||
def _new(cls, client_id, redirect_uri, user_id, scope):
|
||||
return super(OAuth2AuthorizationCode, cls)._new(
|
||||
client_id=client_id,
|
||||
redirect_uri=redirect_uri,
|
||||
user_id=user_id,
|
||||
scope=scope)
|
||||
|
||||
@classmethod
|
||||
def get_token(cls, _id):
|
||||
token = super(OAuth2AuthorizationCode, cls).get_token(_id)
|
||||
if token and not token.used:
|
||||
return token
|
||||
else:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def use_token(cls, _id, client_id, redirect_uri):
|
||||
token = cls.get_token(_id)
|
||||
if token and token.client_id == client_id and token.redirect_uri == redirect_uri:
|
||||
token.used = True
|
||||
token._commit()
|
||||
return token
|
||||
else:
|
||||
return False
|
||||
|
||||
class OAuth2AccessToken(OAuth2Token):
|
||||
"""An OAuth2 access token for accessing protected resources"""
|
||||
token_size = 20
|
||||
_ttl = 10*60
|
||||
_defaults = dict(scope="",
|
||||
token_type="bearer",
|
||||
)
|
||||
_int_props = ("user_id",)
|
||||
_use_db = True
|
||||
_use_new_ring = True
|
||||
|
||||
@classmethod
|
||||
def _new(cls, user_id, scope):
|
||||
return super(OAuth2AccessToken, cls)._new(
|
||||
user_id=user_id,
|
||||
scope=scope)
|
||||
@@ -4755,3 +4755,140 @@ tr.gold-accent + tr > td {
|
||||
box-shadow: inset 0px 1px 1px hsla(0,0%,0%,.3), 0px 1px 0px hsla(0,0%,100%,.6);
|
||||
}
|
||||
|
||||
.infobar.client-info {
|
||||
position: relative;
|
||||
margin: 10px 2%;
|
||||
width: 94%;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.infobar.client-info .icon img {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.infobar.client-info .md {
|
||||
line-height: 48px;
|
||||
margin-left: 56px;
|
||||
}
|
||||
|
||||
.infobar.client-info .md p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.oauth2-authorize {
|
||||
position: relative;
|
||||
background: url(/static/snoo-tray.png) no-repeat;
|
||||
width: 415px;
|
||||
height: 235px;
|
||||
margin: 40px auto 0;
|
||||
padding-left: 280px;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.oauth2-authorize h1 a {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
letter-spacing: -.04em;
|
||||
}
|
||||
|
||||
.oauth2-authorize .icon {
|
||||
position: absolute;
|
||||
left: 160px;
|
||||
top: 77px;
|
||||
}
|
||||
|
||||
.oauth2-authorize .access,
|
||||
.infobar.client-info {
|
||||
background: #f7f7f7;
|
||||
border: 1px solid #b3b3b3;
|
||||
}
|
||||
|
||||
.oauth2-authorize .access {
|
||||
position: relative;
|
||||
margin: 0 -12px;
|
||||
padding: 10px 15px;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.oauth2-authorize .access:before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
border-width: 9px;
|
||||
border-style: solid solid outset; /* mitigates firefox drawing a thicker arrow */
|
||||
border-color: transparent #b3b3b3 transparent transparent;
|
||||
left: -19px;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.oauth2-authorize .access:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
border: 9px solid;
|
||||
border-color: transparent #f7f7f7 transparent transparent;
|
||||
left: -18px;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.oauth2-authorize h2 {
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.oauth2-authorize ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
.oauth2-authorize .notice {
|
||||
color: #333;
|
||||
font-size: .85em;
|
||||
margin: .5em 0;
|
||||
}
|
||||
|
||||
.oauth2-authorize .fancybutton {
|
||||
margin: 0;
|
||||
margin-right: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.oauth2-authorize .fancybutton.allow {
|
||||
color: white;
|
||||
background: #ff4500;
|
||||
border-color: #541700;
|
||||
box-shadow: inset 0px 1px 0px rgba(255, 255, 255, .25);
|
||||
text-shadow: 0px 1px 0px rgba(0, 0, 0, .7);
|
||||
}
|
||||
|
||||
.oauth2-authorize .fancybutton.allow:hover {
|
||||
background: #ff571a;
|
||||
}
|
||||
|
||||
.oauth2-authorize .fancybutton.allow:active {
|
||||
background: #eb3f00;
|
||||
box-shadow: inset 0px -1px 0px rgba(255, 255, 255, .25);
|
||||
}
|
||||
|
||||
.oauth2-authorize .fancybutton.decline {
|
||||
color: black;
|
||||
background: #eee;
|
||||
border-color: #555;
|
||||
box-shadow: inset 0px 1px 0px rgba(255, 255, 255, .5);
|
||||
text-shadow: 0px 1px 0px rgba(255, 255, 255, .7);
|
||||
}
|
||||
|
||||
.oauth2-authorize .fancybutton.decline:hover {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.oauth2-authorize .fancybutton.decline:active {
|
||||
background: #e4e4e4;
|
||||
box-shadow: inset 0px -1px 0px white;
|
||||
}
|
||||
|
||||
BIN
r2/r2/public/static/snoo-tray.png
Normal file
BIN
r2/r2/public/static/snoo-tray.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
30
r2/r2/templates/clientinfobar.html
Normal file
30
r2/r2/templates/clientinfobar.html
Normal file
@@ -0,0 +1,30 @@
|
||||
## The contents of this file are subject to the Common Public Attribution
|
||||
## License Version 1.0. (the "License"); you may not use this file except in
|
||||
## compliance with the License. You may obtain a copy of the License at
|
||||
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
|
||||
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
|
||||
## software over a computer network and provide for limited attribution for the
|
||||
## Original Developer. In addition, Exhibit A has been modified to be consistent
|
||||
## with Exhibit B.
|
||||
##
|
||||
## Software distributed under the License is distributed on an "AS IS" basis,
|
||||
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
|
||||
## the specific language governing rights and limitations under the License.
|
||||
##
|
||||
## The Original Code is Reddit.
|
||||
##
|
||||
## The Original Developer is the Initial Developer. The Initial Developer of
|
||||
## the Original Code is CondeNet, Inc.
|
||||
##
|
||||
## All portions of the code written by CondeNet are Copyright (c) 2006-2010
|
||||
## CondeNet, Inc. All Rights Reserved.
|
||||
################################################################################
|
||||
<%!
|
||||
from r2.lib.template_helpers import s3_https_if_secure
|
||||
from r2.lib.filters import safemarkdown
|
||||
%>
|
||||
<%namespace file="utils.html" import="img_link"/>
|
||||
<div class="infobar ${thing.extra_class}">
|
||||
${img_link(thing.client.name, s3_https_if_secure(thing.client.icon_url), thing.client.about_url, _class="icon")}
|
||||
${unsafe(safemarkdown(thing.message % dict(app_name=thing.client.name, app_about_url=thing.client.about_url)))}
|
||||
</div>
|
||||
49
r2/r2/templates/oauth2authorization.html
Normal file
49
r2/r2/templates/oauth2authorization.html
Normal file
@@ -0,0 +1,49 @@
|
||||
## The contents of this file are subject to the Common Public Attribution
|
||||
## License Version 1.0. (the "License"); you may not use this file except in
|
||||
## compliance with the License. You may obtain a copy of the License at
|
||||
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
|
||||
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
|
||||
## software over a computer network and provide for limited attribution for the
|
||||
## Original Developer. In addition, Exhibit A has been modified to be consistent
|
||||
## with Exhibit B.
|
||||
##
|
||||
## Software distributed under the License is distributed on an "AS IS" basis,
|
||||
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
|
||||
## the specific language governing rights and limitations under the License.
|
||||
##
|
||||
## The Original Code is Reddit.
|
||||
##
|
||||
## The Original Developer is the Initial Developer. The Initial Developer of
|
||||
## the Original Code is CondeNet, Inc.
|
||||
##
|
||||
## All portions of the code written by CondeNet are Copyright (c) 2006-2010
|
||||
## CondeNet, Inc. All Rights Reserved.
|
||||
################################################################################
|
||||
<%!
|
||||
from r2.lib.template_helpers import static, s3_https_if_secure
|
||||
from r2.lib.filters import safemarkdown
|
||||
%>
|
||||
<div class="content oauth2-authorize">
|
||||
<img class="icon" src="${s3_https_if_secure(thing.client.icon_url)}" alt="${thing.client.name} icon" />
|
||||
${unsafe(safemarkdown(_("#[%(app_name)s](%(app_about_url)s) requests to connect with your reddit account.")
|
||||
% dict(app_name=thing.client.name, app_about_url=thing.client.about_url), wrap=False))}
|
||||
<div class="access">
|
||||
<h2>${_("Allow %(app_name)s to:") % dict(app_name=thing.client.name)}</h2>
|
||||
<ul>
|
||||
<li>${thing.scope["description"]}</li>
|
||||
</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="state" value="${thing.state}" />
|
||||
<input type="hidden" name="uh" value="${c.modhash}"/>
|
||||
<div>
|
||||
<input type="submit" class="fancybutton allow" name="authorize" value="${_("Allow")}" />
|
||||
<input type="submit" class="fancybutton decline" value="${_("Decline")}" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user